From 171cd6f58c78ec3ac0b8e06b19b681eb5cbf97df Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 26 Sep 2025 10:29:21 +0100 Subject: [PATCH 01/37] feat: `saexec` package --- blocks/blockstest/blockstest.go | 38 +++ blocks/export.go | 6 + cmputils/types.go | 35 +++ dummy/dummy.go | 35 +++ gastime/gastime.go | 14 +- go.mod | 10 +- go.sum | 20 +- hook/hook.go | 44 +++ params/params.go | 12 + saetest/genesis.go | 65 ++++ saetest/saetest.go | 78 +++++ saetest/weth/weth.go | 41 +++ saexec/execution.go | 184 +++++++++++ saexec/saexec.go | 139 +++++++++ saexec/saexec_test.go | 528 ++++++++++++++++++++++++++++++++ saexec/subscription.go | 43 +++ 16 files changed, 1284 insertions(+), 8 deletions(-) create mode 100644 blocks/blockstest/blockstest.go create mode 100644 cmputils/types.go create mode 100644 dummy/dummy.go create mode 100644 hook/hook.go create mode 100644 params/params.go create mode 100644 saetest/genesis.go create mode 100644 saetest/weth/weth.go create mode 100644 saexec/execution.go create mode 100644 saexec/saexec.go create mode 100644 saexec/saexec_test.go create mode 100644 saexec/subscription.go diff --git a/blocks/blockstest/blockstest.go b/blocks/blockstest/blockstest.go new file mode 100644 index 0000000..8ff2e8c --- /dev/null +++ b/blocks/blockstest/blockstest.go @@ -0,0 +1,38 @@ +// 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" + "testing" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/libevm/core/types" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/saetest" +) + +// NewEthBlock constructs a raw Ethereum block with the given arguments. +func NewEthBlock(parent *types.Block, time uint64, txs types.Transactions) *types.Block { + hdr := &types.Header{ + Number: new(big.Int).Add(parent.Number(), big.NewInt(1)), + Time: time, + ParentHash: parent.Hash(), + } + return types.NewBlock(hdr, txs, nil, nil, saetest.TrieHasher()) +} + +// 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, "New()") + return b +} diff --git a/blocks/export.go b/blocks/export.go index 80541e7..1f4e4c6 100644 --- a/blocks/export.go +++ b/blocks/export.go @@ -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() } @@ -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() } diff --git a/cmputils/types.go b/cmputils/types.go new file mode 100644 index 0000000..d82ae07 --- /dev/null +++ b/cmputils/types.go @@ -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 + }) +} diff --git a/dummy/dummy.go b/dummy/dummy.go new file mode 100644 index 0000000..8a2aed5 --- /dev/null +++ b/dummy/dummy.go @@ -0,0 +1,35 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package dummy provides dummy implementations of interfaces required, but not +// actually used by Ethereum transaction execution. Although these are used in +// production and aren't test doubles, they effectively fill the same role as a +// dummy as described in https://martinfowler.com/bliki/TestDouble.html. +package dummy + +import ( + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/consensus" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/types" +) + +// ChainContext returns a dummy that returns [Engine] when its Engine() method +// is called, and panics when its GetHeader() method is called. +func ChainContext() core.ChainContext { + return chainContext{} +} + +// Engine returns a dummy that panics when its Author() method is called. +func Engine() consensus.Engine { + return engine{} +} + +type ( + chainContext struct{} + engine struct{ consensus.Engine } +) + +func (chainContext) Engine() consensus.Engine { return engine{} } +func (chainContext) GetHeader(common.Hash, uint64) *types.Header { panic("unimplemented") } +func (engine) Author(h *types.Header) (common.Address, error) { panic("unimplemented") } diff --git a/gastime/gastime.go b/gastime/gastime.go index cbb2144..9ed9a5f 100644 --- a/gastime/gastime.go +++ b/gastime/gastime.go @@ -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) @@ -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 diff --git a/go.mod b/go.mod index e85eefc..d0d045b 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,16 @@ go 1.24.7 require ( github.com/ava-labs/avalanchego v1.13.2 github.com/ava-labs/libevm v1.13.14-0.3.0.rc.1 - 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 ) 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 @@ -23,6 +26,7 @@ require ( 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/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/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect @@ -31,6 +35,8 @@ require ( 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 @@ -45,7 +51,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/mod v0.25.0 // indirect golang.org/x/sync v0.15.0 // indirect + golang.org/x/tools v0.34.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index 6d4d354..29b2706 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/StephenButtolph/canoto v0.17.1/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqH github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/ava-labs/avalanchego v1.13.2 h1:Kx/T2a8vqLlgHde3DWT5zMF5yBIh1rqEd6nJQMMzV/Y= github.com/ava-labs/avalanchego v1.13.2/go.mod h1:s7W/kim5L6hiD2PB1v/Ozy1ZZyoLQ4H6mxVO0aMnxng= @@ -33,6 +35,7 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -99,6 +102,8 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc= github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= @@ -161,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= @@ -170,6 +175,8 @@ github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qA github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= @@ -185,6 +192,8 @@ github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iU github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= @@ -196,6 +205,8 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -323,6 +334,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -342,6 +355,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -478,6 +493,7 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= diff --git a/hook/hook.go b/hook/hook.go new file mode 100644 index 0000000..edccc4b --- /dev/null +++ b/hook/hook.go @@ -0,0 +1,44 @@ +// 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 ( + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/params" + + "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 +} + +// BeforeBlock is intended to be called before processing a block, with the gas +// target sourced from [Points]. +func BeforeBlock(clock *gastime.Time, block *types.Header, target gas.Gas) error { + clock.FastForwardTo(block.Time) + return clock.SetTarget(target) +} + +// AfterBlock is intended to be called after processing a block, with the gas +// sourced from [types.Block.GasUsed] or equivalent. +func AfterBlock(clock *gastime.Time, used gas.Gas) { + clock.Tick(used) +} + +// 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) +} diff --git a/params/params.go b/params/params.go new file mode 100644 index 0000000..6b650a6 --- /dev/null +++ b/params/params.go @@ -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 diff --git a/saetest/genesis.go b/saetest/genesis.go new file mode 100644 index 0000000..76bafae --- /dev/null +++ b/saetest/genesis.go @@ -0,0 +1,65 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saetest + +import ( + "crypto/ecdsa" + "testing" + + "github.com/ava-labs/libevm/common" + "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/crypto" + "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/params" + "github.com/ava-labs/libevm/triedb" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +// Genesis constructs a new [core.Genesis], writes it to the database, and +// returns [core.Genesis.ToBlock]. It assumes a nil [triedb.Config]. +func Genesis(tb testing.TB, db ethdb.Database, config *params.ChainConfig, alloc types.GenesisAlloc) *types.Block { + tb.Helper() + var _ *triedb.Config = nil // protect the import to allow linked function comment + + gen := &core.Genesis{ + Config: config, + Alloc: alloc, + } + + tdb := state.NewDatabase(db).TrieDB() + _, hash, err := core.SetupGenesisBlock(db, tdb, gen) + require.NoError(tb, err, "core.SetupGenesisBlock()") + require.NoErrorf(tb, tdb.Commit(hash, true), "%T.Commit(core.SetubGenesisBlock(...))", tdb) + + return gen.ToBlock() +} + +// MaxUint256 returns 2^256-1. +func MaxUint256() *uint256.Int { + return new(uint256.Int).SetAllOne() +} + +// MaxAllocFor returns a genesis allocation with [MaxUint256] as the balance for +// all addresses provided. +func MaxAllocFor(addrs ...common.Address) types.GenesisAlloc { + alloc := make(types.GenesisAlloc) + for _, a := range addrs { + alloc[a] = types.Account{ + Balance: MaxUint256().ToBig(), + } + } + return alloc +} + +// KeyWithMaxAlloc generates a new [crypto.S256] key and returns it along with +// a genesis alloc providing the key's address with a balance of [MaxUint256]. +func KeyWithMaxAlloc(tb testing.TB) (*ecdsa.PrivateKey, types.GenesisAlloc) { + tb.Helper() + key, err := crypto.GenerateKey() + require.NoError(tb, err, "crypto.GenerateKey()") + return key, MaxAllocFor(crypto.PubkeyToAddress(key.PublicKey)) +} diff --git a/saetest/saetest.go b/saetest/saetest.go index b0cc7d6..941b163 100644 --- a/saetest/saetest.go +++ b/saetest/saetest.go @@ -8,9 +8,16 @@ package saetest import ( + "slices" + "sync" + + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/event" "github.com/ava-labs/libevm/trie" "github.com/google/go-cmp/cmp" + + "github.com/ava-labs/strevm/hook" ) // TrieHasher returns an arbitrary trie hasher. @@ -27,3 +34,74 @@ func MerkleRootsEqual[T types.DerivableList](a, b T) bool { func CmpByMerkleRoots[T types.DerivableList]() cmp.Option { return cmp.Comparer(MerkleRootsEqual[T]) } + +// An EventCollector collects all events received from an [event.Subscription]. +// All methods are safe for concurrent use. +type EventCollector[T any] struct { + ch chan T + done chan struct{} + sub event.Subscription + + all []T + cond *sync.Cond +} + +// NewEventCollector returns a new [EventCollector], subscribing via the +// provided function. [EventCollector.Unsubscribe] must be called to release +// resources. +func NewEventCollector[T any](subscribe func(chan<- T) event.Subscription) *EventCollector[T] { + c := &EventCollector[T]{ + ch: make(chan T), + done: make(chan struct{}), + cond: sync.NewCond(&sync.Mutex{}), + } + c.sub = subscribe(c.ch) + go c.collect() + return c +} + +func (c *EventCollector[T]) collect() { + defer close(c.done) + for x := range c.ch { + c.cond.L.Lock() + c.all = append(c.all, x) + c.cond.L.Unlock() + c.cond.Broadcast() + } +} + +// All returns all events received thus far. +func (c *EventCollector[T]) All() []T { + c.cond.L.Lock() + defer c.cond.L.Unlock() + return slices.Clone(c.all) +} + +// Unsubscribe unsubscribes from the subscription and returns the error, +// possibly nil, received on [event.Subscription.Err]. +func (c *EventCollector[T]) Unsubscribe() error { + c.sub.Unsubscribe() + err := <-c.sub.Err() + close(c.ch) + <-c.done + return err +} + +// WaitForAtLeast blocks until at least `n` events have been received. +func (c *EventCollector[T]) WaitForAtLeast(n int) { + c.cond.L.Lock() + for len(c.all) < n { + c.cond.Wait() + } + c.cond.L.Unlock() +} + +// HookStub implements [hook.Points]. +type HookStub struct { + Target gas.Gas +} + +var _ hook.Points = (*HookStub)(nil) + +// GasTarget ignores its argument and always returns [HookStub.Target]. +func (s *HookStub) GasTarget(*types.Block) gas.Gas { return s.Target } diff --git a/saetest/weth/weth.go b/saetest/weth/weth.go new file mode 100644 index 0000000..8e11071 --- /dev/null +++ b/saetest/weth/weth.go @@ -0,0 +1,41 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// The above copyright and licensing exclude the original WETH9 contract and +// compiled artefacts, which are licensed under the following: +// +// Copyright (C) 2015, 2016, 2017 Dapphub +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +// Package weth provides bytecode and ABI bindings for the WETH9 contract, as +// deployed to 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 on Ethereum mainnet. +package weth + +import "github.com/ava-labs/libevm/common" + +const ( + creation = "0x60606040526040805190810160405280600d81526020017f57726170706564204574686572000000000000000000000000000000000000008152506000908051906020019061004f9291906100c8565b506040805190810160405280600481526020017f57455448000000000000000000000000000000000000000000000000000000008152506001908051906020019061009b9291906100c8565b506012600260006101000a81548160ff021916908360ff16021790555034156100c357600080fd5b61016d565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061010957805160ff1916838001178555610137565b82800160010185558215610137579182015b8281111561013657825182559160200191906001019061011b565b5b5090506101449190610148565b5090565b61016a91905b8082111561016657600081600090555060010161014e565b5090565b90565b610c348061017c6000396000f3006060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029" + deployed = "0x6060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029" +) + +// CreationCode returns the EVM bytecode for deploying the WETH9 contract. +func CreationCode() []byte { + return common.FromHex(creation) +} + +// ByteCode returns the deployed EVM bytecode of the WETH9 contract. +func ByteCode() []byte { + return common.FromHex(deployed) +} diff --git a/saexec/execution.go b/saexec/execution.go new file mode 100644 index 0000000..5cc8ed9 --- /dev/null +++ b/saexec/execution.go @@ -0,0 +1,184 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saexec + +import ( + "context" + "errors" + "fmt" + "math" + "runtime" + "time" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/state/snapshot" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/core/vm" + "go.uber.org/zap" + + "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/dummy" + "github.com/ava-labs/strevm/hook" +) + +var errExecutorClosed = errors.New("saexec.Executor closed") + +// Enqueue pushes a new block to the FIFO queue. If [Executor.Close] is called +// before [blocks.Block.Executed] returns true then there is no guarantee that +// the block will be executed. +func (e *Executor) Enqueue(ctx context.Context, block *blocks.Block) error { + for { + select { + case e.queue <- block: + return nil + case <-e.quit: + return errExecutorClosed + case <-ctx.Done(): + return ctx.Err() + default: + // If this happens then increase the channel's buffer size. + e.log.Warn("Execution queue buffer too small") + runtime.Gosched() + } + } +} + +func (e *Executor) processQueue() { + defer close(e.done) + + for { + select { + case <-e.quit: + return + + case block := <-e.queue: + logger := e.log.With( + zap.Uint64("block_height", block.Height()), + zap.Uint64("block_time", block.BuildTime()), + zap.Any("block_hash", block.Hash()), + zap.Int("tx_count", len(block.Transactions())), + ) + + if err := e.execute(block, logger); err != nil { + logger.Fatal("Block execution failed", zap.Error(err)) + return + } + } + } +} + +type executionScratchSpace struct { + snaps *snapshot.Tree + statedb *state.StateDB +} + +func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { + logger.Debug("Executing block") + + // If the VM were to encounter an error after enqueuing the block, we would + // receive the same block twice for execution should consensus retry + // acceptance. + if last, curr := e.lastExecuted.Load().Height(), b.Height(); curr != last+1 { + return fmt.Errorf("executing blocks out of order: %d then %d", last, curr) + } + + target := e.hooks.GasTarget(b.ParentBlock().EthBlock()) + if err := hook.BeforeBlock(e.gasClock, b.Header(), target); err != nil { + return fmt.Errorf("before-block hook: %v", err) + } + perTxClock := e.gasClock.Time.Clone() + + header := types.CopyHeader(b.Header()) + header.BaseFee = e.gasClock.BaseFee().ToBig() + + gasPool := core.GasPool(math.MaxUint64) // required by geth but irrelevant so max it out + var blockGasConsumed gas.Gas + + scratch := &e.executeScratchSpace + receipts := make(types.Receipts, len(b.Transactions())) + for ti, tx := range b.Transactions() { + scratch.statedb.SetTxContext(tx.Hash(), ti) + + receipt, err := core.ApplyTransaction( + e.chainConfig, + dummy.ChainContext(), + &header.Coinbase, + &gasPool, + scratch.statedb, + header, + tx, + (*uint64)(&blockGasConsumed), + vm.Config{}, + ) + if err != nil { + return fmt.Errorf("tx[%d]: %w", ti, err) + } + + perTxClock.Tick(gas.Gas(receipt.GasUsed)) + b.SetInterimExecutionTime(perTxClock) + // TODO(arr4n) investigate calling the same method on pending blocks in + // the queue. It's only worth it if [blocks.LastToSettleAt] regularly + // returns false, meaning that execution is blocking consensus. + + // The [types.Header] that we pass to [core.ApplyTransaction] is + // modified to reduce gas price from the worst-case value agreed by + // consensus. This changes the hash, which is what is copied to receipts + // and logs. + receipt.BlockHash = b.Hash() + for _, l := range receipt.Logs { + l.BlockHash = b.Hash() + } + + // TODO(arr4n) add a receipt cache to the [executor] to allow API calls + // to access them before the end of the block. + receipts[ti] = receipt + } + endTime := time.Now() + hook.AfterBlock(e.gasClock, blockGasConsumed) + if e.gasClock.Time.Compare(perTxClock) != 0 { + return fmt.Errorf("broken invariant: block-resolution clock @ %s does not match tx-resolution clock @ %s", e.gasClock.String(), perTxClock.String()) + } + + logger.Debug( + "Block execution complete", + zap.Uint64("gas_consumed", uint64(blockGasConsumed)), + zap.Time("gas_time", e.gasClock.AsTime()), + zap.Time("wall_time", endTime), + ) + + root, err := e.commitState(scratch, b.NumberU64()) + if err != nil { + return err + } + // The strict ordering of the next 3 calls guarantees invariants that MUST + // NOT be broken: + // + // 1. [blocks.Block.MarkExecuted] guarantees disk then in-memory changes. + // 2. Internal indicator of last executed MUST follow in-memory change. + // 3. External indicator of last executed MUST follow internal indicator. + if err := b.MarkExecuted(e.db, e.gasClock.Clone(), endTime, header.BaseFee, receipts, root); err != nil { + return err + } + e.lastExecuted.Store(b) // (2) + e.sendPostExecutionEvents(b.EthBlock(), receipts) // (3) + return nil +} + +func (e *Executor) commitState(scratch *executionScratchSpace, blockNum uint64) (common.Hash, error) { + root, err := scratch.statedb.Commit(blockNum, true) + if err != nil { + return common.Hash{}, fmt.Errorf("%T.Commit() at end of block %d: %w", scratch.statedb, blockNum, err) + } + + db, err := state.New(root, e.stateCache, scratch.snaps) + if err != nil { + return common.Hash{}, err + } + scratch.statedb = db + return root, nil +} diff --git a/saexec/saexec.go b/saexec/saexec.go new file mode 100644 index 0000000..a417641 --- /dev/null +++ b/saexec/saexec.go @@ -0,0 +1,139 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// Package saexec provides the execution module of [Streaming Asynchronous +// Execution] (SAE). +// +// [Streaming Asynchronous Execution]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution +package saexec + +import ( + "sync/atomic" + + "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/state/snapshot" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/event" + "github.com/ava-labs/libevm/params" + "github.com/ava-labs/libevm/triedb" + + "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/gastime" + "github.com/ava-labs/strevm/hook" +) + +// An Executor accepts and executes a [blocks.Block] FIFO queue. +type Executor struct { + quit, done chan struct{} + log logging.Logger + hooks hook.Points + + gasClock *gastime.Time + queue chan *blocks.Block + lastExecuted atomic.Pointer[blocks.Block] + + headEvents event.FeedOf[core.ChainHeadEvent] + chainEvents event.FeedOf[core.ChainEvent] + logEvents event.FeedOf[[]*types.Log] + + chainConfig *params.ChainConfig + db ethdb.Database + stateCache state.Database + // executeScratchSpace MUST NOT be accessed by any methods other than + // [Executor.init], [Executor.execute], and [Executor.Close]. + executeScratchSpace executionScratchSpace +} + +// New constructs and starts a new [Executor]. Call [Executor.Close] to release +// resources created by this constructor. +// +// The last-executed block MAY be the genesis block for an always-SAE chain, the +// last pre-SAE synchronous block during transition, or the last asynchronously +// executed block after shutdown and recovery. +func New( + lastExecuted *blocks.Block, + chainConfig *params.ChainConfig, + db ethdb.Database, + triedbConfig *triedb.Config, + hooks hook.Points, + log logging.Logger, +) (*Executor, error) { + e := &Executor{ + quit: make(chan struct{}), // closed by [Executor.Close] + done: make(chan struct{}), // closed by [Executor.processQueue] after `quit` is closed + log: log, + hooks: hooks, + queue: make(chan *blocks.Block, 4096), // arbitrarily sized + chainConfig: chainConfig, + db: db, + stateCache: state.NewDatabaseWithConfig(db, triedbConfig), + } + e.lastExecuted.Store(lastExecuted) + if err := e.init(); err != nil { + return nil, err + } + + go e.processQueue() + return e, nil +} + +func (e *Executor) init() error { + last := e.lastExecuted.Load() + e.gasClock = last.ExecutedByGasTime().Clone() + + root := last.PostExecutionStateRoot() + snapConf := snapshot.Config{ + CacheSize: 128, // MB + AsyncBuild: true, + } + snaps, err := snapshot.New(snapConf, e.db, e.stateCache.TrieDB(), root) + if err != nil { + return err + } + statedb, err := state.New(root, e.stateCache, snaps) + if err != nil { + return err + } + + e.executeScratchSpace = executionScratchSpace{ + snaps: snaps, + statedb: statedb, + } + return nil +} + +// Close shuts down the [Executor], waits for the currently executing block +// to complete, and then releases all resources. +func (e *Executor) Close() { + close(e.quit) + <-e.done + + snaps := e.executeScratchSpace.snaps + snaps.Disable() + snaps.Release() +} + +// ChainConfig returns the config originally passed to [New]. +func (e *Executor) ChainConfig() *params.ChainConfig { + return e.chainConfig +} + +// StateCache returns caching database underpinning execution. +func (e *Executor) StateCache() state.Database { + return e.stateCache +} + +// LastExecuted returns the last-executed block in a threadsafe manner. +func (e *Executor) LastExecuted() *blocks.Block { + return e.lastExecuted.Load() +} + +// TimeNotThreadsafe returns a clone of the gas clock that times execution. It +// is only safe to call when all blocks passed to [Executor.Enqueue] +// have been executed, and is only intended for use in tests. +func (e *Executor) TimeNotThreadsafe() *gastime.Time { + return e.gasClock.Clone() +} diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go new file mode 100644 index 0000000..a24ae2e --- /dev/null +++ b/saexec/saexec_test.go @@ -0,0 +1,528 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saexec + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math" + "math/big" + "math/rand/v2" + "testing" + "time" + + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/libevm/common" + "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/crypto" + "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm" + "github.com/ava-labs/libevm/libevm/hookstest" + "github.com/ava-labs/libevm/params" + "github.com/ava-labs/libevm/triedb" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/ava-labs/strevm/blocks" + "github.com/ava-labs/strevm/blocks/blockstest" + "github.com/ava-labs/strevm/cmputils" + "github.com/ava-labs/strevm/gastime" + "github.com/ava-labs/strevm/hook" + "github.com/ava-labs/strevm/proxytime" + "github.com/ava-labs/strevm/saetest" + "github.com/ava-labs/strevm/saetest/weth" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain( + m, + goleak.IgnoreCurrent(), + // Despite the call to [snapshot.Tree.Disable] in [Executor.Close], this + // still leaks at shutdown. This is acceptable as we only ever have one + // [Executor], which we expect to be running for the entire life of the + // process. + goleak.IgnoreTopFunction("github.com/ava-labs/libevm/core/state/snapshot.(*diskLayer).generate"), + ) +} + +func newExec(tb testing.TB, db ethdb.Database, hooks hook.Points, alloc types.GenesisAlloc) *Executor { + tb.Helper() + + config := params.AllDevChainProtocolChanges + ethGenesis := saetest.Genesis(tb, db, config, alloc) + genesis := blockstest.NewBlock(tb, ethGenesis, nil, nil) + require.NoErrorf(tb, genesis.MarkExecuted(db, gastime.New(0, 1, 0), time.Time{}, new(big.Int), nil, ethGenesis.Root()), "%T.MarkExecuted()", genesis) + require.NoErrorf(tb, genesis.MarkSynchronous(), "%T.MarkSynchronous()", genesis) + + e, err := New(genesis, config, db, (*triedb.Config)(nil), hooks, saetest.NewTBLogger(tb, logging.Warn)) + require.NoError(tb, err, "New()") + tb.Cleanup(e.Close) + return e +} + +func newExecWithMaxAlloc(tb testing.TB, db ethdb.Database, hooks hook.Points) (*Executor, *ecdsa.PrivateKey) { + tb.Helper() + key, alloc := saetest.KeyWithMaxAlloc(tb) + return newExec(tb, db, hooks, alloc), key +} + +func defaultHooks() *saetest.HookStub { + return &saetest.HookStub{Target: 1e6} +} + +func TestImmediateShutdownNonBlocking(t *testing.T) { + newExec(t, rawdb.NewMemoryDatabase(), defaultHooks(), nil) // calls [Executor.Close] in test cleanup +} + +func TestExecutionSynchronisation(t *testing.T) { + ctx := context.Background() + e := newExec(t, rawdb.NewMemoryDatabase(), defaultHooks(), nil) + + var chain []*blocks.Block + parent := e.LastExecuted() // genesis + for tm := range uint64(10) { + ethB := blockstest.NewEthBlock(parent.EthBlock(), tm, nil) + b := blockstest.NewBlock(t, ethB, parent, nil) + chain = append(chain, b) + parent = b + } + + for _, b := range chain { + require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") + } + + final := chain[len(chain)-1] + require.NoErrorf(t, final.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted() on last-enqueued block", final) + assert.Equal(t, final.NumberU64(), e.LastExecuted().NumberU64(), "Last-executed atomic pointer holds last-enqueued block") + + for _, b := range chain { + assert.Truef(t, b.Executed(), "%T[%d].Executed()", b, b.NumberU64()) + } +} + +func TestReceiptPropagation(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + e, key := newExecWithMaxAlloc(t, rawdb.NewMemoryDatabase(), defaultHooks()) + signer := types.LatestSigner(e.ChainConfig()) + + var ( + chain []*blocks.Block + want []types.Receipts + nonce uint64 + ) + parent := e.LastExecuted() + for range 10 { + var ( + txs types.Transactions + receipts types.Receipts + ) + + for range 5 { + tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ + Nonce: nonce, + Gas: 1e5, + GasPrice: big.NewInt(1), + }) + nonce++ + txs = append(txs, tx) + receipts = append(receipts, &types.Receipt{TxHash: tx.Hash()}) + } + want = append(want, receipts) + + ethB := blockstest.NewEthBlock(parent.EthBlock(), 0 /*time*/, txs) + b := blockstest.NewBlock(t, ethB, parent, nil) + chain = append(chain, b) + require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") + parent = b + } + require.NoErrorf(t, parent.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted() on last-enqueued block", parent) + + var got []types.Receipts + for _, b := range chain { + got = append(got, b.Receipts()) + } + if diff := cmp.Diff(want, got, cmputils.ReceiptsByTxHash()); diff != "" { + t.Errorf("%T diff (-want +got):\n%s", got, diff) + } +} + +func TestSubscriptions(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + e, key := newExecWithMaxAlloc(t, rawdb.NewMemoryDatabase(), defaultHooks()) + signer := types.LatestSigner(e.ChainConfig()) + + precompile := common.Address{'p', 'r', 'e'} + stub := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { + env.StateDB().AddLog(&types.Log{ + Address: precompile, + }) + return nil, nil + }), + }, + } + stub.Register(t) + + gotChainHeadEvents := saetest.NewEventCollector(e.SubscribeChainHeadEvent) + gotChainEvents := saetest.NewEventCollector(e.SubscribeChainEvent) + gotLogsEvents := saetest.NewEventCollector(e.SubscribeLogsEvent) + var ( + wantChainHeadEvents []core.ChainHeadEvent + wantChainEvents []core.ChainEvent + wantLogsEvents [][]*types.Log + ) + + parent := e.LastExecuted() + for nonce := uint64(0); nonce < 10; nonce++ { + tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ + Nonce: nonce, + To: &precompile, + GasPrice: big.NewInt(1), + Gas: 1e6, + }) + + ethB := blockstest.NewEthBlock(parent.EthBlock(), 0 /*time*/, types.Transactions{tx}) + b := blockstest.NewBlock(t, ethB, parent, nil) + require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") + parent = b + + wantChainHeadEvents = append(wantChainHeadEvents, core.ChainHeadEvent{ + Block: ethB, + }) + logs := []*types.Log{{ + Address: precompile, + BlockNumber: b.NumberU64(), + TxHash: tx.Hash(), + BlockHash: b.Hash(), + }} + wantChainEvents = append(wantChainEvents, core.ChainEvent{ + Block: ethB, + Hash: b.Hash(), + Logs: logs, + }) + wantLogsEvents = append(wantLogsEvents, logs) + } + + opt := cmputils.BlocksByHash() + t.Run("ChainHeadEvents", func(t *testing.T) { + testEvents(t, gotChainHeadEvents, wantChainHeadEvents, opt) + }) + t.Run("ChainEvents", func(t *testing.T) { + testEvents(t, gotChainEvents, wantChainEvents, opt) + }) + t.Run("LogsEvents", func(t *testing.T) { + testEvents(t, gotLogsEvents, wantLogsEvents) + }) +} + +func testEvents[T any](tb testing.TB, got *saetest.EventCollector[T], want []T, opts ...cmp.Option) { + tb.Helper() + // There is an invariant that stipulates [blocks.Block.MarkExecuted] MUST + // occur before sending external events, which means that we can't rely on + // [blocks.Block.WaitUntilExecuted] to avoid races. + got.WaitForAtLeast(len(want)) + + require.NoError(tb, got.Unsubscribe()) + if diff := cmp.Diff(want, got.All(), opts...); diff != "" { + tb.Errorf("Collecting %T from event.Subscription; diff (-want +got):\n%s", want, diff) + } +} + +func TestExecution(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + e, key := newExecWithMaxAlloc(t, rawdb.NewMemoryDatabase(), defaultHooks()) + eoa := crypto.PubkeyToAddress(key.PublicKey) + signer := types.LatestSigner(e.ChainConfig()) + + var ( + txs types.Transactions + want types.Receipts + ) + deploy := types.MustSignNewTx(key, signer, &types.LegacyTx{ + Nonce: 0, + Data: weth.CreationCode(), + GasPrice: big.NewInt(1), + Gas: 1e7, + }) + contract := crypto.CreateAddress(eoa, 0) + txs = append(txs, deploy) + want = append(want, &types.Receipt{ + TxHash: deploy.Hash(), + ContractAddress: contract, + }) + + var eoaAsHash common.Hash + copy(eoaAsHash[12:], eoa[:]) + + rng := rand.New(rand.NewPCG(0, 0)) //nolint:gosec // Reproducibility is useful for tests + var wantWethBalance uint64 + for nonce := uint64(1); nonce < 10; nonce++ { + val := rng.Uint64N(100_000) + tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ + Nonce: nonce, + To: &contract, + Value: new(big.Int).SetUint64(val), + GasPrice: big.NewInt(1), + Gas: 1e6, + }) + wantWethBalance += val + t.Logf("Depositing %d", val) + + txs = append(txs, tx) + want = append(want, &types.Receipt{ + TxHash: tx.Hash(), + Logs: []*types.Log{{ + Address: contract, + TxHash: tx.Hash(), + Topics: []common.Hash{ + crypto.Keccak256Hash([]byte("Deposit(address,uint256)")), + eoaAsHash, + }, + Data: tx.Value().FillBytes(make([]byte, 32)), + }}, + }) + } + + genesis := e.LastExecuted() + ethB := blockstest.NewEthBlock(genesis.EthBlock(), 0, txs) + b := blockstest.NewBlock(t, ethB, genesis, nil) + + var logIndex uint + for i, r := range want { + ui := uint(i) //nolint:gosec // Known to not overflow + + r.Status = 1 + r.TransactionIndex = ui + r.BlockHash = b.Hash() + r.BlockNumber = big.NewInt(1) + + for _, l := range r.Logs { + l.TxIndex = ui + l.BlockHash = b.Hash() + l.BlockNumber = 1 + l.Index = logIndex + logIndex++ + } + } + + require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") + require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) + + opts := cmp.Options{ + cmpopts.IgnoreFields( + types.Receipt{}, + "GasUsed", "CumulativeGasUsed", + "Bloom", + ), + cmputils.BigInts(), + } + if diff := cmp.Diff(want, b.Receipts(), opts); diff != "" { + t.Errorf("%T.Receipts() diff (-want +got):\n%s", b, diff) + } + + t.Run("committed_state", func(t *testing.T) { + sdb, err := state.New(b.PostExecutionStateRoot(), e.StateCache(), nil) + require.NoErrorf(t, err, "state.New(%T.PostExecutionStateRoot(), %T.StateCache(), nil)", b, e) + + if got, want := sdb.GetBalance(contract).ToBig(), new(big.Int).SetUint64(wantWethBalance); got.Cmp(want) != 0 { + t.Errorf("After WETH deposits, got contract balance %v; want %v", got, want) + } + + callData := append( + crypto.Keccak256([]byte("balanceOf(address)"))[:4], + eoaAsHash[:]..., + ) + evm := vm.NewEVM(vm.BlockContext{Transfer: core.Transfer}, vm.TxContext{}, sdb, e.ChainConfig(), vm.Config{}) + got, _, err := evm.Call(vm.AccountRef(eoa), contract, callData, 1e6, uint256.NewInt(0)) + require.NoErrorf(t, err, "%T.Call([weth contract], [balanceOf(eoa)])", evm) + if got, want := new(uint256.Int).SetBytes(got), uint256.NewInt(wantWethBalance); !got.Eq(want) { + t.Errorf("WETH9.balanceOf([eoa]) got %v; want %v", got, want) + } + }) +} + +func TestGasAccounting(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + hooks := &saetest.HookStub{} + e, key := newExecWithMaxAlloc(t, rawdb.NewMemoryDatabase(), hooks) + signer := types.LatestSigner(e.ChainConfig()) + + const gasPerTx = gas.Gas(params.TxGas) + at := func(blockTime, txs uint64, rate gas.Gas) *proxytime.Time[gas.Gas] { + tm := proxytime.New[gas.Gas](blockTime, rate) + tm.Tick(gas.Gas(txs) * gasPerTx) + return tm + } + + // If this fails then all of the tests need to be adjusted. This is cleaner + // than polluting the test cases with a repetitive identifier. + require.Equal(t, 2, gastime.TargetToRate, "gastime.TargetToRate assumption") + + // Steps are _not_ independent, so the execution time of one is the starting + // time of the next. + steps := []struct { + target gas.Gas + blockTime uint64 + numTxs int + wantExecutedBy *proxytime.Time[gas.Gas] + // Because of the 2:1 ratio between Rate and Target, gas consumption + // increases excess by half of the amount consumed, while + // fast-forwarding reduces excess by half of the amount skipped. + wantExcessAfter gas.Gas + wantPriceAfter gas.Price + }{ + { + target: 5 * gasPerTx, + blockTime: 2, + numTxs: 3, + wantExecutedBy: at(2, 3, 10*gasPerTx), + wantExcessAfter: 3 * gasPerTx / 2, + wantPriceAfter: 1, // Excess isn't high enough so price is effectively e^0 + }, + { + target: 5 * gasPerTx, + blockTime: 3, // fast-forward + numTxs: 12, + wantExecutedBy: at(4, 2, 10*gasPerTx), + wantExcessAfter: 12 * gasPerTx / 2, + wantPriceAfter: 1, + }, + { + target: 5 * gasPerTx, + blockTime: 4, // no fast-forward so starts at last execution time + numTxs: 20, + wantExecutedBy: at(6, 2, 10*gasPerTx), + wantExcessAfter: (12 + 20) * gasPerTx / 2, + wantPriceAfter: 1, + }, + { + target: 5 * gasPerTx, + blockTime: 7, // fast-forward equivalent of 8 txs + numTxs: 16, + wantExecutedBy: at(8, 6, 10*gasPerTx), + wantExcessAfter: (12 + 20 - 8 + 16) * gasPerTx / 2, + wantPriceAfter: 1, + }, + { + target: 10 * gasPerTx, // double gas/block --> halve ticking rate + blockTime: 8, // no fast-forward + numTxs: 4, + wantExecutedBy: at(8, (6*2)+4, 20*gasPerTx), // starting point scales + wantExcessAfter: (2*(12+20-8+16) + 4) * gasPerTx / 2, + wantPriceAfter: 1, + }, + { + target: 5 * gasPerTx, // back to original + blockTime: 8, + numTxs: 5, + wantExecutedBy: at(8, 6+(4/2)+5, 10*gasPerTx), + wantExcessAfter: ((12 + 20 - 8 + 16) + 4/2 + 5) * gasPerTx / 2, + wantPriceAfter: 1, + }, + { + target: 5 * gasPerTx, + blockTime: 20, // more than double the last executed-by time, reduces excess to 0 + numTxs: 1, + wantExecutedBy: at(20, 1, 10*gasPerTx), + wantExcessAfter: gasPerTx / 2, + wantPriceAfter: 1, + }, + { + target: 5 * gasPerTx, + blockTime: 21, // fast-forward so excess is 0 + numTxs: 30 * gastime.TargetToExcessScaling, // deliberate, see below + wantExecutedBy: at(21, 30*gastime.TargetToExcessScaling, 10*gasPerTx), + wantExcessAfter: 3 * ((5 * gasPerTx /*T*/) * gastime.TargetToExcessScaling /* == K */), + // Excess is now 3·K so the price is e^3 = 20.09 + wantPriceAfter: 20, + }, + { + target: 5 * gasPerTx, + blockTime: 22, // no fast-forward + numTxs: 10 * gastime.TargetToExcessScaling, + wantExecutedBy: at(21, 40*gastime.TargetToExcessScaling, 10*gasPerTx), + wantExcessAfter: 4 * ((5 * gasPerTx /*T*/) * gastime.TargetToExcessScaling /* == K */), + wantPriceAfter: gas.Price(math.Floor(math.Pow(math.E, 4 /* <----- NB */))), + }, + } + + parent := e.LastExecuted() + var nonce uint64 + for i, step := range steps { + hooks.Target = step.target + + txs := make(types.Transactions, step.numTxs) + for i := range txs { + txs[i] = types.MustSignNewTx(key, signer, &types.DynamicFeeTx{ + Nonce: nonce, + To: &common.Address{}, + Gas: params.TxGas, + GasTipCap: big.NewInt(0), + GasFeeCap: big.NewInt(100), + }) + nonce++ + } + + ethB := blockstest.NewEthBlock(parent.EthBlock(), step.blockTime, txs) + b := blockstest.NewBlock(t, ethB, parent, nil) + require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") + require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) + parent = b + + for desc, got := range map[string]*gastime.Time{ + fmt.Sprintf("%T.ExecutedByGasTime()", b): b.ExecutedByGasTime(), + fmt.Sprintf("%T.TimeNotThreadSafe()", e): e.TimeNotThreadsafe(), + } { + opt := proxytime.CmpOpt[gas.Gas](proxytime.IgnoreRateInvariants) + if diff := cmp.Diff(step.wantExecutedBy, got.Time, opt); diff != "" { + t.Errorf("%s diff (-want +got):\n%s", desc, diff) + } + } + + t.Run("CumulativeGasUsed", func(t *testing.T) { + for i, r := range b.Receipts() { + ui := uint64(i + 1) //nolint:gosec // Known to not overflow + assert.Equalf(t, ui*params.TxGas, r.CumulativeGasUsed, "%T.Receipts()[%d]", b, i) + } + }) + + if t.Failed() { + // Future steps / tests may be corrupted and false-positive errors + // aren't helpful. + break + } + + t.Run("gas_price", func(t *testing.T) { + tm := e.TimeNotThreadsafe() + assert.Equalf(t, step.wantExcessAfter, tm.Excess(), "%T.Excess()", tm) + assert.Equalf(t, step.wantPriceAfter, tm.Price(), "%T.Price()", tm) + + wantBaseFee := gas.Price(1) + if i > 0 { + wantBaseFee = steps[i-1].wantPriceAfter + } + require.Truef(t, b.BaseFee().IsUint64(), "%T.BaseFee().IsUint64()", b) + assert.Equalf(t, wantBaseFee, gas.Price(b.BaseFee().Uint64()), "%T.BaseFee().Uint64()", b) + }) + } +} diff --git a/saexec/subscription.go b/saexec/subscription.go new file mode 100644 index 0000000..24e6052 --- /dev/null +++ b/saexec/subscription.go @@ -0,0 +1,43 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saexec + +import ( + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/event" +) + +func (e *Executor) sendPostExecutionEvents(b *types.Block, receipts types.Receipts) { + e.headEvents.Send(core.ChainHeadEvent{Block: b}) + + var logs []*types.Log + for _, r := range receipts { + logs = append(logs, r.Logs...) + } + e.chainEvents.Send(core.ChainEvent{ + Block: b, + Hash: b.Hash(), + Logs: logs, + }) + e.logEvents.Send(logs) +} + +// SubscribeChainHeadEvent returns a new subscription for each +// [core.ChainHeadEvent] emitted after execution of a [blocks.Block]. +func (e *Executor) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { + return e.headEvents.Subscribe(ch) +} + +// SubscribeChainEvent returns a new subscription for each [core.ChainEvent] +// emitted after execution of a [blocks.Block]. +func (e *Executor) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { + return e.chainEvents.Subscribe(ch) +} + +// SubscribeLogsEvent returns a new subscription for logs emitted after +// execution of a [blocks.Block]. +func (e *Executor) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription { + return e.logEvents.Subscribe(ch) +} From 1fd6ecfafb1a0ec7b59f675225f0028db36823b2 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 11:54:48 +0000 Subject: [PATCH 02/37] refactor!: use MIT contract for testing --- saetest/escrow/escrow.go | 28 +++++++++++++++++++++++++++ saetest/weth/weth.go | 41 ---------------------------------------- saexec/saexec_test.go | 40 +++++++++++++++++++++++++-------------- 3 files changed, 54 insertions(+), 55 deletions(-) create mode 100644 saetest/escrow/escrow.go delete mode 100644 saetest/weth/weth.go diff --git a/saetest/escrow/escrow.go b/saetest/escrow/escrow.go new file mode 100644 index 0000000..19effb4 --- /dev/null +++ b/saetest/escrow/escrow.go @@ -0,0 +1,28 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// The above copyright and licensing exclude the original Escrow.sol contract +// and compiled artefacts, which are licensed under the following: +// +// Copyright 2024 Divergence Tech Ltd. + +// Package escrow provides bytecode for the Escrow.sol contract deployed to +// 0x370F21541173E8B773571c135e3b5617d7f38C54 on Ethereum mainnet. +package escrow + +import "github.com/ava-labs/libevm/common" + +const ( + creation = "0x6080806040523460155761029e908161001a8239f35b5f80fdfe6040608081526004361015610012575f80fd5b5f3560e01c80633ccfd60b1461017757806351cff8d914610148578063837b2d1d1461010e578063e3d670d7146100d35763f340fa0114610051575f80fd5b60203660031901126100cf576004356001600160a01b03811691908290036100cf57815f525f602052805f209182543481018091116100bb577fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c93558151908152346020820152a1005b634e487b7160e01b5f52601160045260245ffd5b5f80fd5b50346100cf5760203660031901126100cf576004356001600160a01b03811691908290036100cf576020915f525f8252805f20549051908152f35b50346100cf575f3660031901126100cf57602090517fe3f9c77ea5446c989d214acc27cefc902862791ee093b44540c8790a484451828152f35b346100cf5760203660031901126100cf576004356001600160a01b03811681036100cf576101759061018c565b005b346100cf575f3660031901126100cf57610175335b60018060a01b0316805f525f602052604090815f2054801561027a57815f525f6020525f83812055804710610263575f80808084865af13d1561025e5767ffffffffffffffff3d81811161024a57855191601f8201601f19908116603f011683019081118382101761024a57865281525f60203d92013e5b1561023957825191825260208201527f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b659190a1565b8251630a12f52160e11b8152600490fd5b634e487b7160e01b5f52604160045260245ffd5b610204565b825163cd78605960e01b8152306004820152602490fd5b5060249151906316b4356760e31b82526004820152fdfea164736f6c6343000819000a" + deployed = "0x6040608081526004361015610012575f80fd5b5f3560e01c80633ccfd60b1461017757806351cff8d914610148578063837b2d1d1461010e578063e3d670d7146100d35763f340fa0114610051575f80fd5b60203660031901126100cf576004356001600160a01b03811691908290036100cf57815f525f602052805f209182543481018091116100bb577fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c93558151908152346020820152a1005b634e487b7160e01b5f52601160045260245ffd5b5f80fd5b50346100cf5760203660031901126100cf576004356001600160a01b03811691908290036100cf576020915f525f8252805f20549051908152f35b50346100cf575f3660031901126100cf57602090517fe3f9c77ea5446c989d214acc27cefc902862791ee093b44540c8790a484451828152f35b346100cf5760203660031901126100cf576004356001600160a01b03811681036100cf576101759061018c565b005b346100cf575f3660031901126100cf57610175335b60018060a01b0316805f525f602052604090815f2054801561027a57815f525f6020525f83812055804710610263575f80808084865af13d1561025e5767ffffffffffffffff3d81811161024a57855191601f8201601f19908116603f011683019081118382101761024a57865281525f60203d92013e5b1561023957825191825260208201527f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b659190a1565b8251630a12f52160e11b8152600490fd5b634e487b7160e01b5f52604160045260245ffd5b610204565b825163cd78605960e01b8152306004820152602490fd5b5060249151906316b4356760e31b82526004820152fdfea164736f6c6343000819000a" +) + +// CreationCode returns the EVM bytecode for deploying the Escrow.sol contract. +func CreationCode() []byte { + return common.FromHex(creation) +} + +// ByteCode returns the deployed EVM bytecode of the Escrow.sol contract. +func ByteCode() []byte { + return common.FromHex(deployed) +} diff --git a/saetest/weth/weth.go b/saetest/weth/weth.go deleted file mode 100644 index 8e11071..0000000 --- a/saetest/weth/weth.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// The above copyright and licensing exclude the original WETH9 contract and -// compiled artefacts, which are licensed under the following: -// -// Copyright (C) 2015, 2016, 2017 Dapphub -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later -// version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -// details. -// -// You should have received a copy of the GNU General Public License along with -// this program. If not, see . - -// Package weth provides bytecode and ABI bindings for the WETH9 contract, as -// deployed to 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 on Ethereum mainnet. -package weth - -import "github.com/ava-labs/libevm/common" - -const ( - creation = "0x60606040526040805190810160405280600d81526020017f57726170706564204574686572000000000000000000000000000000000000008152506000908051906020019061004f9291906100c8565b506040805190810160405280600481526020017f57455448000000000000000000000000000000000000000000000000000000008152506001908051906020019061009b9291906100c8565b506012600260006101000a81548160ff021916908360ff16021790555034156100c357600080fd5b61016d565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061010957805160ff1916838001178555610137565b82800160010185558215610137579182015b8281111561013657825182559160200191906001019061011b565b5b5090506101449190610148565b5090565b61016a91905b8082111561016657600081600090555060010161014e565b5090565b90565b610c348061017c6000396000f3006060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029" - deployed = "0x6060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029" -) - -// CreationCode returns the EVM bytecode for deploying the WETH9 contract. -func CreationCode() []byte { - return common.FromHex(creation) -} - -// ByteCode returns the deployed EVM bytecode of the WETH9 contract. -func ByteCode() []byte { - return common.FromHex(deployed) -} diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index f8700ef..1574d63 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -39,7 +39,7 @@ import ( "github.com/ava-labs/strevm/hook" "github.com/ava-labs/strevm/proxytime" "github.com/ava-labs/strevm/saetest" - "github.com/ava-labs/strevm/saetest/weth" + "github.com/ava-labs/strevm/saetest/escrow" ) func TestMain(m *testing.M) { @@ -239,7 +239,7 @@ func TestExecution(t *testing.T) { ) deploy := wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ Nonce: 0, - Data: weth.CreationCode(), + Data: escrow.CreationCode(), GasPrice: big.NewInt(1), Gas: 1e7, }) @@ -254,7 +254,7 @@ func TestExecution(t *testing.T) { copy(eoaAsHash[12:], eoa[:]) rng := rand.New(rand.NewPCG(0, 0)) //nolint:gosec // Reproducibility is useful for tests - var wantWethBalance uint64 + var wantEscrowBalance uint64 for range 10 { val := rng.Uint64N(100_000) tx := wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ @@ -262,8 +262,12 @@ func TestExecution(t *testing.T) { Value: new(big.Int).SetUint64(val), GasPrice: big.NewInt(1), Gas: 1e6, + Data: append( + crypto.Keccak256([]byte("deposit(address)"))[:4], + eoaAsHash[:]..., + ), }) - wantWethBalance += val + wantEscrowBalance += val t.Logf("Depositing %d", val) txs = append(txs, tx) @@ -274,9 +278,11 @@ func TestExecution(t *testing.T) { TxHash: tx.Hash(), Topics: []common.Hash{ crypto.Keccak256Hash([]byte("Deposit(address,uint256)")), - eoaAsHash, }, - Data: tx.Value().FillBytes(make([]byte, 32)), + Data: append( + eoaAsHash[:], + tx.Value().FillBytes(make([]byte, 32))..., + ), }}, }) } @@ -321,19 +327,25 @@ func TestExecution(t *testing.T) { sdb, err := state.New(b.PostExecutionStateRoot(), e.StateCache(), nil) require.NoErrorf(t, err, "state.New(%T.PostExecutionStateRoot(), %T.StateCache(), nil)", b, e) - if got, want := sdb.GetBalance(contract).ToBig(), new(big.Int).SetUint64(wantWethBalance); got.Cmp(want) != 0 { - t.Errorf("After WETH deposits, got contract balance %v; want %v", got, want) + if got, want := sdb.GetBalance(contract).ToBig(), new(big.Int).SetUint64(wantEscrowBalance); got.Cmp(want) != 0 { + t.Errorf("After Escrow deposits, got contract balance %v; want %v", got, want) } + enablePUSH0 := vm.BlockContext{ + BlockNumber: big.NewInt(1), + Time: 1, + Random: &common.Hash{}, + } + evm := vm.NewEVM(enablePUSH0, vm.TxContext{}, sdb, e.ChainConfig(), vm.Config{}) + callData := append( - crypto.Keccak256([]byte("balanceOf(address)"))[:4], + crypto.Keccak256([]byte("balance(address)"))[:4], eoaAsHash[:]..., ) - evm := vm.NewEVM(vm.BlockContext{Transfer: core.Transfer}, vm.TxContext{}, sdb, e.ChainConfig(), vm.Config{}) - got, _, err := evm.Call(vm.AccountRef(eoa), contract, callData, 1e6, uint256.NewInt(0)) - require.NoErrorf(t, err, "%T.Call([weth contract], [balanceOf(eoa)])", evm) - if got, want := new(uint256.Int).SetBytes(got), uint256.NewInt(wantWethBalance); !got.Eq(want) { - t.Errorf("WETH9.balanceOf([eoa]) got %v; want %v", got, want) + got, _, err := evm.StaticCall(vm.AccountRef(eoa), contract, callData, 1e6) + require.NoErrorf(t, err, "%T.Call([Escrow contract], [balance(eoa)])", evm) + if got, want := new(uint256.Int).SetBytes(got), uint256.NewInt(wantEscrowBalance); !got.Eq(want) { + t.Errorf("Escrow.balance([eoa]) got %v; want %v", got, want) } }) } From bd0a0c3a0aa0eb03f9713db6f2c12b18909c3332 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 12:03:42 +0000 Subject: [PATCH 03/37] fix: log errored tx execution and continue --- saexec/execution.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/saexec/execution.go b/saexec/execution.go index 5cc8ed9..9477d95 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -65,7 +65,7 @@ func (e *Executor) processQueue() { ) if err := e.execute(block, logger); err != nil { - logger.Fatal("Block execution failed", zap.Error(err)) + logger.Error("Block execution failed", zap.Error(err)) return } } @@ -116,7 +116,17 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { vm.Config{}, ) if err != nil { - return fmt.Errorf("tx[%d]: %w", ti, err) + // This almost certainly means that the worst-case block inclusion + // has a bug. + logger.Error( + "Transaction execution errored (not reverted)", + zap.Stringer("block_hash", b.Hash()), + zap.Uint64("block_height", b.Height()), + zap.Int("tx_index", ti), + zap.Stringer("tx_hash", tx.Hash()), + zap.Error(err), + ) + continue } perTxClock.Tick(gas.Gas(receipt.GasUsed)) From f3ca5e78b403de2ca1b90c57afa90ad612dec55b Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 12:21:46 +0000 Subject: [PATCH 04/37] doc: fix misspelling --- blocks/blockstest/chain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/blockstest/chain.go b/blocks/blockstest/chain.go index 7de2362..c66cdd6 100644 --- a/blocks/blockstest/chain.go +++ b/blocks/blockstest/chain.go @@ -44,7 +44,7 @@ func (cb *ChainBuilder) Last() *blocks.Block { return cb.chain[len(cb.chain)-1] } -// AllBlocks returns all blocks, including the genesis past to +// AllBlocks returns all blocks, including the genesis passed to // [NewChainBuilder]. func (cb *ChainBuilder) AllBlocks() []*blocks.Block { return slices.Clone(cb.chain) From d04027f28cfc0f3e1456ed8a1e22e83bcf3931f1 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 12:39:43 +0000 Subject: [PATCH 05/37] refactor: `escrow` package provides contract helpers --- saetest/escrow/escrow.go | 44 +++++++++++++++++++++++++++++++++++++--- saexec/saexec_test.go | 29 ++++++-------------------- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/saetest/escrow/escrow.go b/saetest/escrow/escrow.go index 19effb4..e15e6e9 100644 --- a/saetest/escrow/escrow.go +++ b/saetest/escrow/escrow.go @@ -6,11 +6,18 @@ // // Copyright 2024 Divergence Tech Ltd. -// Package escrow provides bytecode for the Escrow.sol contract deployed to -// 0x370F21541173E8B773571c135e3b5617d7f38C54 on Ethereum mainnet. +// Package escrow provides bytecode and helpers for the Escrow.sol contract +// deployed to 0x370F21541173E8B773571c135e3b5617d7f38C54 on Ethereum mainnet. package escrow -import "github.com/ava-labs/libevm/common" +import ( + "slices" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/crypto" + "github.com/holiman/uint256" +) const ( creation = "0x6080806040523460155761029e908161001a8239f35b5f80fdfe6040608081526004361015610012575f80fd5b5f3560e01c80633ccfd60b1461017757806351cff8d914610148578063837b2d1d1461010e578063e3d670d7146100d35763f340fa0114610051575f80fd5b60203660031901126100cf576004356001600160a01b03811691908290036100cf57815f525f602052805f209182543481018091116100bb577fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c93558151908152346020820152a1005b634e487b7160e01b5f52601160045260245ffd5b5f80fd5b50346100cf5760203660031901126100cf576004356001600160a01b03811691908290036100cf576020915f525f8252805f20549051908152f35b50346100cf575f3660031901126100cf57602090517fe3f9c77ea5446c989d214acc27cefc902862791ee093b44540c8790a484451828152f35b346100cf5760203660031901126100cf576004356001600160a01b03811681036100cf576101759061018c565b005b346100cf575f3660031901126100cf57610175335b60018060a01b0316805f525f602052604090815f2054801561027a57815f525f6020525f83812055804710610263575f80808084865af13d1561025e5767ffffffffffffffff3d81811161024a57855191601f8201601f19908116603f011683019081118382101761024a57865281525f60203d92013e5b1561023957825191825260208201527f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b659190a1565b8251630a12f52160e11b8152600490fd5b634e487b7160e01b5f52604160045260245ffd5b610204565b825163cd78605960e01b8152306004820152602490fd5b5060249151906316b4356760e31b82526004820152fdfea164736f6c6343000819000a" @@ -26,3 +33,34 @@ func CreationCode() []byte { func ByteCode() []byte { return common.FromHex(deployed) } + +// CallDataToDeposit returns the transaction call data to deposit native token +// for the given recipient. +func CallDataToDeposit(recipient common.Address) []byte { + return callDataWithAddr("deposit(address)", recipient) +} + +// CallDataForBalance returns the transaction call data to retrieve the balance +// in escrow for the given beneficiary. +func CallDataForBalance(beneficiary common.Address) []byte { + return callDataWithAddr("balance(address)", beneficiary) +} + +func callDataWithAddr(sig string, addr common.Address) []byte { + return slices.Concat( + crypto.Keccak256([]byte(sig))[:4], + make([]byte, 12), addr[:], + ) +} + +// DepositEvent returns the [types.Log] emitted by a successful transaction with +// [CallDataToDeposit] data. +func DepositEvent(recipient common.Address, amount *uint256.Int) *types.Log { + return &types.Log{ + Topics: []common.Hash{crypto.Keccak256Hash([]byte("Deposit(address,uint256)"))}, + Data: slices.Concat( + make([]byte, 12), recipient[:], + amount.PaddedBytes(32), + ), + } +} diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 1574d63..df02955 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -250,9 +250,6 @@ func TestExecution(t *testing.T) { ContractAddress: contract, }) - var eoaAsHash common.Hash - copy(eoaAsHash[12:], eoa[:]) - rng := rand.New(rand.NewPCG(0, 0)) //nolint:gosec // Reproducibility is useful for tests var wantEscrowBalance uint64 for range 10 { @@ -262,28 +259,18 @@ func TestExecution(t *testing.T) { Value: new(big.Int).SetUint64(val), GasPrice: big.NewInt(1), Gas: 1e6, - Data: append( - crypto.Keccak256([]byte("deposit(address)"))[:4], - eoaAsHash[:]..., - ), + Data: escrow.CallDataToDeposit(eoa), }) wantEscrowBalance += val t.Logf("Depositing %d", val) txs = append(txs, tx) + ev := escrow.DepositEvent(eoa, uint256.NewInt(val)) + ev.Address = contract + ev.TxHash = tx.Hash() want = append(want, &types.Receipt{ TxHash: tx.Hash(), - Logs: []*types.Log{{ - Address: contract, - TxHash: tx.Hash(), - Topics: []common.Hash{ - crypto.Keccak256Hash([]byte("Deposit(address,uint256)")), - }, - Data: append( - eoaAsHash[:], - tx.Value().FillBytes(make([]byte, 32))..., - ), - }}, + Logs: []*types.Log{ev}, }) } @@ -338,11 +325,7 @@ func TestExecution(t *testing.T) { } evm := vm.NewEVM(enablePUSH0, vm.TxContext{}, sdb, e.ChainConfig(), vm.Config{}) - callData := append( - crypto.Keccak256([]byte("balance(address)"))[:4], - eoaAsHash[:]..., - ) - got, _, err := evm.StaticCall(vm.AccountRef(eoa), contract, callData, 1e6) + got, _, err := evm.StaticCall(vm.AccountRef(eoa), contract, escrow.CallDataForBalance(eoa), 1e6) require.NoErrorf(t, err, "%T.Call([Escrow contract], [balance(eoa)])", evm) if got, want := new(uint256.Int).SetBytes(got), uint256.NewInt(wantEscrowBalance); !got.Eq(want) { t.Errorf("Escrow.balance([eoa]) got %v; want %v", got, want) From f442e7606eddd882120f7c36406b22f8688649ee Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 12:41:11 +0000 Subject: [PATCH 06/37] fix: remove duplicated log fields --- saexec/execution.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/saexec/execution.go b/saexec/execution.go index 9477d95..2990d06 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -120,8 +120,6 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { // has a bug. logger.Error( "Transaction execution errored (not reverted)", - zap.Stringer("block_hash", b.Hash()), - zap.Uint64("block_height", b.Height()), zap.Int("tx_index", ti), zap.Stringer("tx_hash", tx.Hash()), zap.Error(err), From d74bccca46cee1d313064827c6875af37905f8a2 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 12:44:26 +0000 Subject: [PATCH 07/37] refactor: `newSUT()` constructs memory database --- saexec/saexec_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index df02955..f0b1010 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -21,7 +21,6 @@ import ( "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" - "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/libevm/hookstest" "github.com/ava-labs/libevm/params" @@ -61,10 +60,12 @@ type SUT struct { wallet *saetest.Wallet } -func newSUT(tb testing.TB, db ethdb.Database, hooks hook.Points) SUT { +func newSUT(tb testing.TB, hooks hook.Points) SUT { tb.Helper() config := params.AllDevChainProtocolChanges + db := rawdb.NewMemoryDatabase() + wallet := saetest.NewUNSAFEWallet(tb, 1, types.LatestSigner(config)) alloc := saetest.MaxAllocFor(wallet.Addresses()...) genesis := blockstest.NewGenesis(tb, db, config, alloc) @@ -84,12 +85,12 @@ func defaultHooks() *saetest.HookStub { } func TestImmediateShutdownNonBlocking(t *testing.T) { - newSUT(t, rawdb.NewMemoryDatabase(), defaultHooks()) // calls [Executor.Close] in test cleanup + newSUT(t, defaultHooks()) // calls [Executor.Close] in test cleanup } func TestExecutionSynchronisation(t *testing.T) { ctx := context.Background() - sut := newSUT(t, rawdb.NewMemoryDatabase(), defaultHooks()) + sut := newSUT(t, defaultHooks()) e, chain := sut.Executor, sut.chain for range uint64(10) { @@ -110,7 +111,7 @@ func TestReceiptPropagation(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - sut := newSUT(t, rawdb.NewMemoryDatabase(), defaultHooks()) + sut := newSUT(t, defaultHooks()) e, chain, wallet := sut.Executor, sut.chain, sut.wallet var want [][]*types.Receipt @@ -148,7 +149,7 @@ func TestSubscriptions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - sut := newSUT(t, rawdb.NewMemoryDatabase(), defaultHooks()) + sut := newSUT(t, defaultHooks()) e, chain, wallet := sut.Executor, sut.chain, sut.wallet precompile := common.Address{'p', 'r', 'e'} @@ -229,7 +230,7 @@ func TestExecution(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - sut := newSUT(t, rawdb.NewMemoryDatabase(), defaultHooks()) + sut := newSUT(t, defaultHooks()) wallet := sut.wallet eoa := wallet.Addresses()[0] @@ -437,7 +438,7 @@ func TestGasAccounting(t *testing.T) { } hooks := &saetest.HookStub{} - sut := newSUT(t, rawdb.NewMemoryDatabase(), hooks) + sut := newSUT(t, hooks) e, chain, wallet := sut.Executor, sut.chain, sut.wallet for i, step := range steps { From 90a49eb0bde18718c26612a57e551023eabc82a7 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 15:53:21 +0000 Subject: [PATCH 08/37] feat: `saetest.TBLogger` errors cancel context --- blocks/blockstest/blocks.go | 24 +++++++++++-- blocks/blockstest/blocks_test.go | 8 +++-- blocks/blockstest/chain.go | 49 +++++++++++++++++++++++++-- saetest/logging.go | 41 +++++++++++++++++----- saexec/saexec_test.go | 58 +++++++++++++++++--------------- 5 files changed, 137 insertions(+), 43 deletions(-) diff --git a/blocks/blockstest/blocks.go b/blocks/blockstest/blocks.go index 73360e0..fa952bd 100644 --- a/blocks/blockstest/blocks.go +++ b/blocks/blockstest/blocks.go @@ -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 diff --git a/blocks/blockstest/blocks_test.go b/blocks/blockstest/blocks_test.go index 53ef35f..cdf7978 100644 --- a/blocks/blockstest/blocks_test.go +++ b/blocks/blockstest/blocks_test.go @@ -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) diff --git a/blocks/blockstest/chain.go b/blocks/blockstest/chain.go index c66cdd6..e215037 100644 --- a/blocks/blockstest/chain.go +++ b/blocks/blockstest/chain.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/libevm/options" "github.com/ava-labs/strevm/blocks" ) @@ -19,6 +20,7 @@ import ( // A ChainBuilder builds a chain of blocks, maintaining necessary invariants. type ChainBuilder struct { chain []*blocks.Block + opts []ChainOption } // NewChainBuilder returns a new ChainBuilder starting from the provided block, @@ -29,12 +31,53 @@ 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.opts = opts +} + +type chainOptions struct { + ethOpts []EthBlockOption + saeOpts []BlockOption +} + +// WithEthBlockOptions wraps the options that [ChainBuilder.NewBlock] propagates +// to [NewEthBlock]. +func WithEthBlockOptions(opts ...EthBlockOption) ChainOption { + return options.Func[chainOptions](func(co *chainOptions) { + co.ethOpts = append(co.ethOpts, opts...) + }) +} + +// WithBlockOptions wraps the options that [ChainBuilder.NewBlock] propagates to +// [NewBlock]. +func WithBlockOptions(opts ...BlockOption) ChainOption { + return options.Func[chainOptions](func(co *chainOptions) { + co.saeOpts = append(co.saeOpts, opts...) + }) +} + +func ethBlockOptions(opts []ChainOption) []EthBlockOption { + return options.ApplyTo(&chainOptions{}, opts...).ethOpts +} + +func blockOptions(opts []ChainOption) []BlockOption { + return options.ApplyTo(&chainOptions{}, opts...).saeOpts +} + // 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() + opts = slices.Concat(cb.opts, 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 + eth := NewEthBlock(last.EthBlock(), txs, ethBlockOptions(opts)...) + cb.chain = append(cb.chain, NewBlock(tb, eth, last, nil, blockOptions(opts)...)) // TODO(arr4n) support last-settled blocks + return cb.Last() } diff --git a/saetest/logging.go b/saetest/logging.go index ca57e8b..eb7d2c8 100644 --- a/saetest/logging.go +++ b/saetest/logging.go @@ -4,6 +4,7 @@ package saetest import ( + "context" "runtime" "slices" "testing" @@ -104,24 +105,39 @@ func (l *LogRecorder) AtLeast(lvl logging.Level) []*LogRecord { return l.Filter(func(r *LogRecord) bool { return r.Level >= lvl }) } -// NewTBLogger constructs a logger that propagates logs to the [testing.TB]. -// WARNING and ERROR logs are sent to [testing.TB.Errorf] while FATAL is sent to +// NewTBLogger constructs a logger that propagates logs to [testing.TB]. WARNING +// and ERROR logs are sent to [testing.TB.Errorf] while FATAL is sent to // [testing.TB.Fatalf]. All other logs are sent to [testing.TB.Logf]. Although // the level can be configured, it is silently capped at [logging.Warn]. // //nolint:thelper // The outputs include the logging site while the TB site is most useful if here -func NewTBLogger(tb testing.TB, level logging.Level) logging.Logger { - return &logger{ +func NewTBLogger(tb testing.TB, level logging.Level) *TBLogger { + l := &TBLogger{tb: tb} + l.logger = &logger{ + handler: l, // TODO(arr4n) remove the recursion here and in [LogRecorder] level: min(level, logging.Warn), - handler: &tbLogger{tb: tb}, } + return l +} + +// TBLogger is a [logging.Logger] that propagates logs to [testing.TB]. +type TBLogger struct { + *logger + tb testing.TB + onError []context.CancelFunc } -type tbLogger struct { - tb testing.TB +// CancelOnError pipes `ctx` to and from [context.WithCancel], calling the +// [context.CancelFunc] after logs >= [logging.Error], and during [testing.TB] +// cleanup. +func (l *TBLogger) CancelOnError(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + l.onError = append(l.onError, cancel) + l.tb.Cleanup(cancel) + return ctx } -func (l *tbLogger) log(lvl logging.Level, msg string, fields ...zap.Field) { +func (l *TBLogger) log(lvl logging.Level, msg string, fields ...zap.Field) { var to func(string, ...any) switch { case lvl == logging.Warn || lvl == logging.Error: // because @ARR4N says warnings in tests are errors @@ -132,6 +148,15 @@ func (l *tbLogger) log(lvl logging.Level, msg string, fields ...zap.Field) { to = l.tb.Logf } + defer func() { + if lvl < logging.Error { + return + } + for _, fn := range l.onError { + fn() + } + }() + enc := zapcore.NewMapObjectEncoder() for _, f := range fields { f.AddTo(enc) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index f0b1010..68dd20b 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -10,7 +10,6 @@ import ( "math/big" "math/rand/v2" "testing" - "time" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/components/gas" @@ -58,25 +57,40 @@ type SUT struct { *Executor chain *blockstest.ChainBuilder wallet *saetest.Wallet + logger logging.Logger } -func newSUT(tb testing.TB, hooks hook.Points) SUT { +// newSUT returns a new SUT. Any >= [logging.Error] on the logger will also +// cancel the returned context, which is useful when waiting for blocks that +// can never finish execution because of an error. +func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { tb.Helper() + logger := saetest.NewTBLogger(tb, logging.Warn) + ctx := logger.CancelOnError(tb.Context()) + config := params.AllDevChainProtocolChanges db := rawdb.NewMemoryDatabase() + tdbConfig := &triedb.Config{} wallet := saetest.NewUNSAFEWallet(tb, 1, types.LatestSigner(config)) alloc := saetest.MaxAllocFor(wallet.Addresses()...) - genesis := blockstest.NewGenesis(tb, db, config, alloc) + genesis := blockstest.NewGenesis(tb, db, config, alloc, blockstest.WithTrieDBConfig(tdbConfig)) - e, err := New(genesis, config, db, (*triedb.Config)(nil), hooks, saetest.NewTBLogger(tb, logging.Warn)) + e, err := New(genesis, config, db, tdbConfig, hooks, logger) require.NoError(tb, err, "New()") tb.Cleanup(e.Close) - return SUT{ + + chain := blockstest.NewChainBuilder(e.LastExecuted()) + chain.SetDefaultOptions(blockstest.WithBlockOptions( + blockstest.WithLogger(logger)), + ) + + return ctx, SUT{ Executor: e, - chain: blockstest.NewChainBuilder(e.LastExecuted()), + chain: chain, wallet: wallet, + logger: logger, } } @@ -89,8 +103,7 @@ func TestImmediateShutdownNonBlocking(t *testing.T) { } func TestExecutionSynchronisation(t *testing.T) { - ctx := context.Background() - sut := newSUT(t, defaultHooks()) + ctx, sut := newSUT(t, defaultHooks()) e, chain := sut.Executor, sut.chain for range uint64(10) { @@ -108,10 +121,7 @@ func TestExecutionSynchronisation(t *testing.T) { } func TestReceiptPropagation(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - sut := newSUT(t, defaultHooks()) + ctx, sut := newSUT(t, defaultHooks()) e, chain, wallet := sut.Executor, sut.chain, sut.wallet var want [][]*types.Receipt @@ -146,10 +156,7 @@ func TestReceiptPropagation(t *testing.T) { } func TestSubscriptions(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - sut := newSUT(t, defaultHooks()) + ctx, sut := newSUT(t, defaultHooks()) e, chain, wallet := sut.Executor, sut.chain, sut.wallet precompile := common.Address{'p', 'r', 'e'} @@ -227,10 +234,7 @@ func testEvents[T any](tb testing.TB, got *saetest.EventCollector[T], want []T, } func TestExecution(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - sut := newSUT(t, defaultHooks()) + ctx, sut := newSUT(t, defaultHooks()) wallet := sut.wallet eoa := wallet.Addresses()[0] @@ -335,8 +339,8 @@ func TestExecution(t *testing.T) { } func TestGasAccounting(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() + hooks := &saetest.HookStub{} + ctx, sut := newSUT(t, hooks) const gasPerTx = gas.Gas(params.TxGas) at := func(blockTime, txs uint64, rate gas.Gas) *proxytime.Time[gas.Gas] { @@ -437,8 +441,6 @@ func TestGasAccounting(t *testing.T) { }, } - hooks := &saetest.HookStub{} - sut := newSUT(t, hooks) e, chain, wallet := sut.Executor, sut.chain, sut.wallet for i, step := range steps { @@ -454,9 +456,11 @@ func TestGasAccounting(t *testing.T) { }) } - b := chain.NewBlock(t, txs, blockstest.ModifyHeader(func(h *types.Header) { - h.Time = step.blockTime - })) + b := chain.NewBlock(t, txs, blockstest.WithEthBlockOptions( + blockstest.ModifyHeader(func(h *types.Header) { + h.Time = step.blockTime + }), + )) require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) From 0245a1b16e36ff438ea1b81fe3fc5fd67bb615b9 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 16:12:46 +0000 Subject: [PATCH 09/37] feat: user-defined before- and after-block hooks --- hook/hook.go | 17 +++++++++++++---- saetest/saetest.go | 14 +++++++++++++- saexec/execution.go | 8 +++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/hook/hook.go b/hook/hook.go index edccc4b..c8e7dfd 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -8,7 +8,10 @@ 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" @@ -20,19 +23,25 @@ import ( // Points define user-injected hook points. type Points interface { GasTarget(parent *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(clock *gastime.Time, block *types.Header, target gas.Gas) error { - clock.FastForwardTo(block.Time) - return clock.SetTarget(target) +func BeforeBlock(pts Points, rules params.Rules, sdb *state.StateDB, b *types.Block, clock *gastime.Time, target gas.Gas) error { + clock.FastForwardTo(b.Time()) + if err := clock.SetTarget(target); err != nil { + return fmt.Errorf("%T.SetTarget() before block: %w", clock, err) + } + return pts.BeforeBlock(rules, sdb, b) } // AfterBlock is intended to be called after processing a block, with the gas // sourced from [types.Block.GasUsed] or equivalent. -func AfterBlock(clock *gastime.Time, used gas.Gas) { +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 diff --git a/saetest/saetest.go b/saetest/saetest.go index 941b163..845d007 100644 --- a/saetest/saetest.go +++ b/saetest/saetest.go @@ -12,8 +12,10 @@ import ( "sync" "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/event" + "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/trie" "github.com/google/go-cmp/cmp" @@ -104,4 +106,14 @@ type HookStub struct { var _ hook.Points = (*HookStub)(nil) // GasTarget ignores its argument and always returns [HookStub.Target]. -func (s *HookStub) GasTarget(*types.Block) gas.Gas { return s.Target } +func (s *HookStub) GasTarget(*types.Block) gas.Gas { + return s.Target +} + +// BeforeBlock is a no-op that always returns nil. +func (*HookStub) BeforeBlock(params.Rules, *state.StateDB, *types.Block) error { + return nil +} + +// AfterBlock is a no-op. +func (*HookStub) AfterBlock(*state.StateDB, *types.Block, types.Receipts) {} diff --git a/saexec/execution.go b/saexec/execution.go index 2990d06..8a72514 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -87,8 +87,11 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { return fmt.Errorf("executing blocks out of order: %d then %d", last, curr) } + scratch := &e.executeScratchSpace + rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime()) + target := e.hooks.GasTarget(b.ParentBlock().EthBlock()) - if err := hook.BeforeBlock(e.gasClock, b.Header(), target); err != nil { + if err := hook.BeforeBlock(e.hooks, rules, scratch.statedb, b.EthBlock(), e.gasClock, target); err != nil { return fmt.Errorf("before-block hook: %v", err) } perTxClock := e.gasClock.Time.Clone() @@ -99,7 +102,6 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { gasPool := core.GasPool(math.MaxUint64) // required by geth but irrelevant so max it out var blockGasConsumed gas.Gas - scratch := &e.executeScratchSpace receipts := make(types.Receipts, len(b.Transactions())) for ti, tx := range b.Transactions() { scratch.statedb.SetTxContext(tx.Hash(), ti) @@ -147,7 +149,7 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { receipts[ti] = receipt } endTime := time.Now() - hook.AfterBlock(e.gasClock, blockGasConsumed) + hook.AfterBlock(e.hooks, scratch.statedb, b.EthBlock(), e.gasClock, blockGasConsumed, receipts) if e.gasClock.Time.Compare(perTxClock) != 0 { return fmt.Errorf("broken invariant: block-resolution clock @ %s does not match tx-resolution clock @ %s", e.gasClock.String(), perTxClock.String()) } From 0183d17faf835118ead7d0c8fbf7c074580cfa92 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 20:11:46 +0000 Subject: [PATCH 10/37] feat: `hook.Points` supports sub-second block times --- hook/hook.go | 12 +++++++++--- hook/hookstest/stub.go | 39 +++++++++++++++++++++++++++++++++++++++ saetest/saetest.go | 25 ------------------------- saexec/execution.go | 3 +-- saexec/saexec_test.go | 11 ++++++----- 5 files changed, 55 insertions(+), 35 deletions(-) create mode 100644 hook/hookstest/stub.go diff --git a/hook/hook.go b/hook/hook.go index c8e7dfd..aca21d8 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -15,6 +15,7 @@ import ( "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" @@ -23,18 +24,23 @@ import ( // 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 *types.Block, clock *gastime.Time, target gas.Gas) error { - clock.FastForwardTo(b.Time()) +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) + return pts.BeforeBlock(rules, sdb, b.EthBlock()) } // AfterBlock is intended to be called after processing a block, with the gas diff --git a/hook/hookstest/stub.go b/hook/hookstest/stub.go new file mode 100644 index 0000000..eeb481b --- /dev/null +++ b/hook/hookstest/stub.go @@ -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 +} + +// SubSecondBlock 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) {} diff --git a/saetest/saetest.go b/saetest/saetest.go index 845d007..15856de 100644 --- a/saetest/saetest.go +++ b/saetest/saetest.go @@ -11,15 +11,10 @@ import ( "slices" "sync" - "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/event" - "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/trie" "github.com/google/go-cmp/cmp" - - "github.com/ava-labs/strevm/hook" ) // TrieHasher returns an arbitrary trie hasher. @@ -97,23 +92,3 @@ func (c *EventCollector[T]) WaitForAtLeast(n int) { } c.cond.L.Unlock() } - -// HookStub implements [hook.Points]. -type HookStub struct { - Target gas.Gas -} - -var _ hook.Points = (*HookStub)(nil) - -// GasTarget ignores its argument and always returns [HookStub.Target]. -func (s *HookStub) GasTarget(*types.Block) gas.Gas { - return s.Target -} - -// BeforeBlock is a no-op that always returns nil. -func (*HookStub) BeforeBlock(params.Rules, *state.StateDB, *types.Block) error { - return nil -} - -// AfterBlock is a no-op. -func (*HookStub) AfterBlock(*state.StateDB, *types.Block, types.Receipts) {} diff --git a/saexec/execution.go b/saexec/execution.go index 8a72514..0c4c406 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -90,8 +90,7 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { scratch := &e.executeScratchSpace rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime()) - target := e.hooks.GasTarget(b.ParentBlock().EthBlock()) - if err := hook.BeforeBlock(e.hooks, rules, scratch.statedb, b.EthBlock(), e.gasClock, target); err != nil { + if err := hook.BeforeBlock(e.hooks, rules, scratch.statedb, b, e.gasClock); err != nil { return fmt.Errorf("before-block hook: %v", err) } perTxClock := e.gasClock.Time.Clone() diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 68dd20b..2a4d5c3 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -21,7 +21,7 @@ import ( "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm" - "github.com/ava-labs/libevm/libevm/hookstest" + libevmhookstest "github.com/ava-labs/libevm/libevm/hookstest" "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/triedb" "github.com/google/go-cmp/cmp" @@ -35,6 +35,7 @@ import ( "github.com/ava-labs/strevm/cmputils" "github.com/ava-labs/strevm/gastime" "github.com/ava-labs/strevm/hook" + saehookstest "github.com/ava-labs/strevm/hook/hookstest" "github.com/ava-labs/strevm/proxytime" "github.com/ava-labs/strevm/saetest" "github.com/ava-labs/strevm/saetest/escrow" @@ -94,8 +95,8 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { } } -func defaultHooks() *saetest.HookStub { - return &saetest.HookStub{Target: 1e6} +func defaultHooks() *saehookstest.Stub { + return &saehookstest.Stub{Target: 1e6} } func TestImmediateShutdownNonBlocking(t *testing.T) { @@ -160,7 +161,7 @@ func TestSubscriptions(t *testing.T) { e, chain, wallet := sut.Executor, sut.chain, sut.wallet precompile := common.Address{'p', 'r', 'e'} - stub := &hookstest.Stub{ + stub := &libevmhookstest.Stub{ PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { env.StateDB().AddLog(&types.Log{ @@ -339,7 +340,7 @@ func TestExecution(t *testing.T) { } func TestGasAccounting(t *testing.T) { - hooks := &saetest.HookStub{} + hooks := &saehookstest.Stub{} ctx, sut := newSUT(t, hooks) const gasPerTx = gas.Gas(params.TxGas) From e7772cedffcf23aa72afcedee9cbc3ff1f2dfeb1 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 20:16:43 +0000 Subject: [PATCH 11/37] chore: placate the linter --- hook/hookstest/stub.go | 2 +- saexec/saexec_test.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hook/hookstest/stub.go b/hook/hookstest/stub.go index eeb481b..611c70f 100644 --- a/hook/hookstest/stub.go +++ b/hook/hookstest/stub.go @@ -25,7 +25,7 @@ func (s *Stub) GasTarget(parent *types.Block) gas.Gas { return s.Target } -// SubSecondBlock time ignores its argument and always returns 0. +// SubSecondBlockTime time ignores its argument and always returns 0. func (*Stub) SubSecondBlockTime(*types.Block) gas.Gas { return 0 } diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 2a4d5c3..fe6479d 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -21,7 +21,6 @@ import ( "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm" - libevmhookstest "github.com/ava-labs/libevm/libevm/hookstest" "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/triedb" "github.com/google/go-cmp/cmp" @@ -35,10 +34,12 @@ import ( "github.com/ava-labs/strevm/cmputils" "github.com/ava-labs/strevm/gastime" "github.com/ava-labs/strevm/hook" - saehookstest "github.com/ava-labs/strevm/hook/hookstest" "github.com/ava-labs/strevm/proxytime" "github.com/ava-labs/strevm/saetest" "github.com/ava-labs/strevm/saetest/escrow" + + libevmhookstest "github.com/ava-labs/libevm/libevm/hookstest" + saehookstest "github.com/ava-labs/strevm/hook/hookstest" ) func TestMain(m *testing.M) { From 03aa6394d470da78f7bcbd024d007d1496cb818b Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 17 Nov 2025 20:22:19 +0000 Subject: [PATCH 12/37] chore: if `golangci-lint` just said what it wanted then this would all be much easier for everyone --- .golangci.yml | 1 - saexec/saexec_test.go | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index e221766..70ea23f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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: diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index fe6479d..2a4d5c3 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm" + libevmhookstest "github.com/ava-labs/libevm/libevm/hookstest" "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/triedb" "github.com/google/go-cmp/cmp" @@ -34,12 +35,10 @@ import ( "github.com/ava-labs/strevm/cmputils" "github.com/ava-labs/strevm/gastime" "github.com/ava-labs/strevm/hook" + saehookstest "github.com/ava-labs/strevm/hook/hookstest" "github.com/ava-labs/strevm/proxytime" "github.com/ava-labs/strevm/saetest" "github.com/ava-labs/strevm/saetest/escrow" - - libevmhookstest "github.com/ava-labs/libevm/libevm/hookstest" - saehookstest "github.com/ava-labs/strevm/hook/hookstest" ) func TestMain(m *testing.M) { From 472f9fbf04fc19beebb3ff0f73a13bdc62e3d93f Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 09:38:44 +0000 Subject: [PATCH 13/37] chore: Linty McLintface --- proxytime/proxytime_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxytime/proxytime_test.go b/proxytime/proxytime_test.go index eeb7523..8231bc7 100644 --- a/proxytime/proxytime_test.go +++ b/proxytime/proxytime_test.go @@ -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] { From 433494ff23b8a51f22da03047a97a7a4cb6fd6b7 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 12:16:43 +0000 Subject: [PATCH 14/37] fix: contextual opcodes, particularly `BLOCKHASH` --- blocks/blockstest/chain.go | 29 ++++-- dummy/dummy.go | 35 ------- saexec/context.go | 40 ++++++++ saexec/execution.go | 3 +- saexec/saexec.go | 25 ++--- saexec/saexec_test.go | 193 ++++++++++++++++++++++++++++++++++++- 6 files changed, 266 insertions(+), 59 deletions(-) delete mode 100644 dummy/dummy.go create mode 100644 saexec/context.go diff --git a/blocks/blockstest/chain.go b/blocks/blockstest/chain.go index e215037..2a7053b 100644 --- a/blocks/blockstest/chain.go +++ b/blocks/blockstest/chain.go @@ -11,6 +11,7 @@ import ( "slices" "testing" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/libevm/options" @@ -19,15 +20,18 @@ import ( // A ChainBuilder builds a chain of blocks, maintaining necessary invariants. type ChainBuilder struct { - chain []*blocks.Block - opts []ChainOption + chain []*blocks.Block + byHash map[common.Hash]*blocks.Block + + defaultOpts []ChainOption } // 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}, + chain: []*blocks.Block{genesis}, + byHash: make(map[common.Hash]*blocks.Block), } } @@ -37,7 +41,7 @@ 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.opts = opts + cb.defaultOpts = opts } type chainOptions struct { @@ -72,11 +76,13 @@ func blockOptions(opts []ChainOption) []BlockOption { // NewBlock constructs and returns a new block in the chain. func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts ...ChainOption) *blocks.Block { tb.Helper() - opts = slices.Concat(cb.opts, opts) + opts = slices.Concat(cb.defaultOpts, opts) last := cb.Last() eth := NewEthBlock(last.EthBlock(), txs, ethBlockOptions(opts)...) - cb.chain = append(cb.chain, NewBlock(tb, eth, last, nil, blockOptions(opts)...)) // TODO(arr4n) support last-settled blocks + b := NewBlock(tb, eth, last, nil, blockOptions(opts)...) // TODO(arr4n) support last-settled blocks + cb.chain = append(cb.chain, b) + cb.byHash[b.Hash()] = b return cb.Last() } @@ -97,3 +103,14 @@ func (cb *ChainBuilder) AllBlocks() []*blocks.Block { 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) { + b, ok := cb.byHash[h] + if !ok || b.NumberU64() != num { + return nil, false + } + return b, true +} diff --git a/dummy/dummy.go b/dummy/dummy.go deleted file mode 100644 index 8a2aed5..0000000 --- a/dummy/dummy.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -// Package dummy provides dummy implementations of interfaces required, but not -// actually used by Ethereum transaction execution. Although these are used in -// production and aren't test doubles, they effectively fill the same role as a -// dummy as described in https://martinfowler.com/bliki/TestDouble.html. -package dummy - -import ( - "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/consensus" - "github.com/ava-labs/libevm/core" - "github.com/ava-labs/libevm/core/types" -) - -// ChainContext returns a dummy that returns [Engine] when its Engine() method -// is called, and panics when its GetHeader() method is called. -func ChainContext() core.ChainContext { - return chainContext{} -} - -// Engine returns a dummy that panics when its Author() method is called. -func Engine() consensus.Engine { - return engine{} -} - -type ( - chainContext struct{} - engine struct{ consensus.Engine } -) - -func (chainContext) Engine() consensus.Engine { return engine{} } -func (chainContext) GetHeader(common.Hash, uint64) *types.Header { panic("unimplemented") } -func (engine) Author(h *types.Header) (common.Address, error) { panic("unimplemented") } diff --git a/saexec/context.go b/saexec/context.go new file mode 100644 index 0000000..ab462da --- /dev/null +++ b/saexec/context.go @@ -0,0 +1,40 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package saexec + +import ( + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/consensus" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/strevm/blocks" +) + +// A BlockSource returns a block that matches both a hash and number, or nil +// if not found. +type BlockSource func(hash common.Hash, number uint64) *blocks.Block + +var _ core.ChainContext = (*chainContext)(nil) + +type chainContext struct { + BlockSource + log logging.Logger +} + +func (c *chainContext) GetHeader(h common.Hash, n uint64) *types.Header { + b := c.BlockSource(h, n) + if b == nil { + return nil + } + return b.Header() +} + +func (c *chainContext) Engine() consensus.Engine { + // This is serious enough that it needs to be investigated immediately, but + // not enough to be fatal. It will also cause tests to fail if ever called, + // so we can catch it early. + c.log.Error("ChainContext.Engine() called unexpectedly") + return struct{ consensus.Engine }{} +} diff --git a/saexec/execution.go b/saexec/execution.go index 0c4c406..fa7f91a 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -22,7 +22,6 @@ import ( "go.uber.org/zap" "github.com/ava-labs/strevm/blocks" - "github.com/ava-labs/strevm/dummy" "github.com/ava-labs/strevm/hook" ) @@ -107,7 +106,7 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { receipt, err := core.ApplyTransaction( e.chainConfig, - dummy.ChainContext(), + e.chainContext, &header.Coinbase, &gasPool, scratch.statedb, diff --git a/saexec/saexec.go b/saexec/saexec.go index a417641..90a4439 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -39,9 +39,10 @@ type Executor struct { chainEvents event.FeedOf[core.ChainEvent] logEvents event.FeedOf[[]*types.Log] - chainConfig *params.ChainConfig - db ethdb.Database - stateCache state.Database + chainContext core.ChainContext + chainConfig *params.ChainConfig + db ethdb.Database + stateCache state.Database // executeScratchSpace MUST NOT be accessed by any methods other than // [Executor.init], [Executor.execute], and [Executor.Close]. executeScratchSpace executionScratchSpace @@ -55,6 +56,7 @@ type Executor struct { // executed block after shutdown and recovery. func New( lastExecuted *blocks.Block, + blockSrc BlockSource, chainConfig *params.ChainConfig, db ethdb.Database, triedbConfig *triedb.Config, @@ -62,14 +64,15 @@ func New( log logging.Logger, ) (*Executor, error) { e := &Executor{ - quit: make(chan struct{}), // closed by [Executor.Close] - done: make(chan struct{}), // closed by [Executor.processQueue] after `quit` is closed - log: log, - hooks: hooks, - queue: make(chan *blocks.Block, 4096), // arbitrarily sized - chainConfig: chainConfig, - db: db, - stateCache: state.NewDatabaseWithConfig(db, triedbConfig), + quit: make(chan struct{}), // closed by [Executor.Close] + done: make(chan struct{}), // closed by [Executor.processQueue] after `quit` is closed + log: log, + hooks: hooks, + queue: make(chan *blocks.Block, 4096), // arbitrarily sized + chainContext: &chainContext{blockSrc, log}, + chainConfig: chainConfig, + db: db, + stateCache: state.NewDatabaseWithConfig(db, triedbConfig), } e.lastExecuted.Store(lastExecuted) if err := e.init(); err != nil { diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 2a4d5c3..7f81fb2 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -9,6 +9,7 @@ import ( "math" "math/big" "math/rand/v2" + "slices" "testing" "github.com/ava-labs/avalanchego/utils/logging" @@ -31,6 +32,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/goleak" + "github.com/ava-labs/strevm/blocks" "github.com/ava-labs/strevm/blocks/blockstest" "github.com/ava-labs/strevm/cmputils" "github.com/ava-labs/strevm/gastime" @@ -78,14 +80,21 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { alloc := saetest.MaxAllocFor(wallet.Addresses()...) genesis := blockstest.NewGenesis(tb, db, config, alloc, blockstest.WithTrieDBConfig(tdbConfig)) - e, err := New(genesis, config, db, tdbConfig, hooks, logger) - require.NoError(tb, err, "New()") - tb.Cleanup(e.Close) - - chain := blockstest.NewChainBuilder(e.LastExecuted()) + chain := blockstest.NewChainBuilder(genesis) chain.SetDefaultOptions(blockstest.WithBlockOptions( blockstest.WithLogger(logger)), ) + src := BlockSource(func(h common.Hash, n uint64) *blocks.Block { + b, ok := chain.GetBlock(h, n) + if !ok { + return nil + } + return b + }) + + e, err := New(genesis, src, config, db, tdbConfig, hooks, logger) + require.NoError(tb, err, "New()") + tb.Cleanup(e.Close) return ctx, SUT{ Executor: e, @@ -502,3 +511,177 @@ func TestGasAccounting(t *testing.T) { }) } } + +func TestContextualOpCodes(t *testing.T) { + ctx, sut := newSUT(t, defaultHooks()) + + chain := sut.chain + for range 5 { + // Historical blocks, required to already be in `chain`, for testing + // BLOCKHASH. + b := chain.NewBlock(t, nil) + require.NoErrorf(t, sut.Enqueue(ctx, b), "Enqueue([empty block])") + } + + // log1 returns contract bytecode that logs the value on the top of the + // stack after executing `pre`. + log1 := func(pre ...vm.OpCode) []vm.OpCode { + return slices.Concat(pre, []vm.OpCode{vm.PUSH0, vm.PUSH0, vm.LOG1}) + } + + bigToHash := func(b *big.Int) common.Hash { + return uint256.MustFromBig(b).Bytes32() + } + + // For specific tests. + const txValueSend = 42 + saveBlockNum := &blockNumSaver{} + + tests := []struct { + name string + code []vm.OpCode + header func(*types.Header) + wantTopic common.Hash + wantTopicFn func() common.Hash // if non-nil, overrides `wantTopic` + }{ + { + name: "BALANCE", + code: log1(vm.ADDRESS, vm.BALANCE), + wantTopic: common.Hash{31: txValueSend}, + }, + { + name: "CALLVALUE", + code: log1(vm.CALLVALUE), + wantTopic: common.Hash{31: txValueSend}, + }, + { + name: "SELFBALANCE", + code: log1(vm.SELFBALANCE), + wantTopic: common.Hash{31: txValueSend}, + }, + { + name: "ORIGIN", + code: log1(vm.ORIGIN), + wantTopic: common.BytesToHash( + sut.wallet.Addresses()[0].Bytes(), + ), + }, + { + name: "BLOCKHASH_genesis", + code: log1(vm.PUSH0, vm.BLOCKHASH), + wantTopic: chain.AllBlocks()[0].Hash(), + }, + { + name: "BLOCKHASH_arbitrary", + code: log1(vm.PUSH1, 3, vm.BLOCKHASH), + wantTopic: chain.AllBlocks()[3].Hash(), + }, + { + name: "NUMBER", + code: log1(vm.NUMBER), + header: saveBlockNum.store, + wantTopicFn: func() common.Hash { + return bigToHash(saveBlockNum.num) + }, + }, + { + name: "COINBASE_arbitrary", + code: log1(vm.COINBASE), + header: func(h *types.Header) { + h.Coinbase = common.Address{17: 0xC0, 18: 0xFF, 19: 0xEE} + }, + wantTopic: common.BytesToHash([]byte{0xC0, 0xFF, 0xEE}), + }, + { + name: "COINBASE_zero", + code: log1(vm.COINBASE), + }, + { + name: "TIMESTAMP", + code: log1(vm.TIMESTAMP), + header: func(h *types.Header) { + h.Time = 0xDECAFBAD + }, + wantTopic: common.BytesToHash([]byte{0xDE, 0xCA, 0xFB, 0xAD}), + }, + { + name: "PREVRANDAO", + code: log1(vm.PREVRANDAO), + }, + { + name: "CHAINID", + code: log1(vm.CHAINID), + wantTopic: bigToHash(sut.ChainConfig().ChainID), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := make([]byte, len(tt.code)) + for i, op := range tt.code { + data[i] = byte(op) + } + tx := sut.wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ + To: nil, // contract creation runs the call data (one sneaky trick blockchain developers don't want you to know) + GasPrice: big.NewInt(1), + Gas: 100e6, + Data: data, + Value: big.NewInt(txValueSend), + }) + + var opts []blockstest.ChainOption + if tt.header != nil { + opts = append(opts, blockstest.WithEthBlockOptions( + blockstest.ModifyHeader(tt.header), + )) + } + + b := sut.chain.NewBlock(t, types.Transactions{tx}, opts...) + require.NoError(t, sut.Enqueue(ctx, b), "Enqueue()") + require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) + require.Lenf(t, b.Receipts(), 1, "%T.Receipts()", b) + + got := b.Receipts()[0] + diffopts := cmp.Options{ + cmpopts.IgnoreFields( + types.Receipt{}, + "Bloom", "ContractAddress", "CumulativeGasUsed", "GasUsed", + ), + cmpopts.IgnoreFields( + types.Log{}, + "Address", + ), + cmputils.BigInts(), + } + wantTopic := tt.wantTopic + if tt.wantTopicFn != nil { + wantTopic = tt.wantTopicFn() + } + want := &types.Receipt{ + Status: types.ReceiptStatusSuccessful, + BlockHash: b.Hash(), + BlockNumber: b.Number(), + TxHash: tx.Hash(), + Logs: []*types.Log{{ + Topics: []common.Hash{wantTopic}, + BlockHash: b.Hash(), + BlockNumber: b.NumberU64(), + TxHash: tx.Hash(), + }}, + } + if diff := cmp.Diff(want, got, diffopts); diff != "" { + t.Errorf("%T diff (-want +got):\n%s", got, diff) + } + }) + } +} + +type blockNumSaver struct { + num *big.Int +} + +var _ = blockstest.ModifyHeader((*blockNumSaver)(nil).store) + +func (e *blockNumSaver) store(h *types.Header) { + e.num = new(big.Int).Set(h.Number) +} From d56614deb122a358ade99cb0adfc9d9d1b683c96 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:30:48 +0000 Subject: [PATCH 15/37] refactor: `defer` unlocking `CondVar.L` Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saetest/saetest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/saetest/saetest.go b/saetest/saetest.go index 15856de..e7125c9 100644 --- a/saetest/saetest.go +++ b/saetest/saetest.go @@ -87,8 +87,8 @@ func (c *EventCollector[T]) Unsubscribe() error { // WaitForAtLeast blocks until at least `n` events have been received. func (c *EventCollector[T]) WaitForAtLeast(n int) { c.cond.L.Lock() + defer c.cond.L.Unlock() for len(c.all) < n { c.cond.Wait() } - c.cond.L.Unlock() } From f7bcfb1225775ab945a94d41498a290232d06c6f Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 12:27:35 +0000 Subject: [PATCH 16/37] chore: no points for guessing --- saexec/context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/saexec/context.go b/saexec/context.go index ab462da..8754cec 100644 --- a/saexec/context.go +++ b/saexec/context.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/libevm/consensus" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/strevm/blocks" ) From 476e73c20f193a62edda0dfca34988477533e74b Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:32:56 +0000 Subject: [PATCH 17/37] refactor: use `zap.Stringer` for block hash Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saexec/execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/saexec/execution.go b/saexec/execution.go index fa7f91a..2c834d3 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -59,7 +59,7 @@ func (e *Executor) processQueue() { logger := e.log.With( zap.Uint64("block_height", block.Height()), zap.Uint64("block_time", block.BuildTime()), - zap.Any("block_hash", block.Hash()), + zap.Stringer("block_hash", block.Hash()), zap.Int("tx_count", len(block.Transactions())), ) From 18ee4b50cf5814e2b08682655c03bd6cefc479cf Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 12:52:45 +0000 Subject: [PATCH 18/37] refactor: move `TimeNotThreadsafe` to test file and un-export --- saexec/saexec.go | 7 ------- saexec/saexec_test.go | 11 +++++++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/saexec/saexec.go b/saexec/saexec.go index 90a4439..8feca0d 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -133,10 +133,3 @@ func (e *Executor) StateCache() state.Database { func (e *Executor) LastExecuted() *blocks.Block { return e.lastExecuted.Load() } - -// TimeNotThreadsafe returns a clone of the gas clock that times execution. It -// is only safe to call when all blocks passed to [Executor.Enqueue] -// have been executed, and is only intended for use in tests. -func (e *Executor) TimeNotThreadsafe() *gastime.Time { - return e.gasClock.Clone() -} diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 7f81fb2..af373a7 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -104,6 +104,13 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { } } +// timeNotThreadsafe returns a clone of the gas clock that times execution. It +// is only safe to call when all blocks passed to [Executor.Enqueue] +// have been executed. +func (e *Executor) timeNotThreadsafe() *gastime.Time { + return e.gasClock.Clone() +} + func defaultHooks() *saehookstest.Stub { return &saehookstest.Stub{Target: 1e6} } @@ -476,7 +483,7 @@ func TestGasAccounting(t *testing.T) { for desc, got := range map[string]*gastime.Time{ fmt.Sprintf("%T.ExecutedByGasTime()", b): b.ExecutedByGasTime(), - fmt.Sprintf("%T.TimeNotThreadSafe()", e): e.TimeNotThreadsafe(), + fmt.Sprintf("%T.TimeNotThreadSafe()", e): e.timeNotThreadsafe(), } { opt := proxytime.CmpOpt[gas.Gas](proxytime.IgnoreRateInvariants) if diff := cmp.Diff(step.wantExecutedBy, got.Time, opt); diff != "" { @@ -498,7 +505,7 @@ func TestGasAccounting(t *testing.T) { } t.Run("gas_price", func(t *testing.T) { - tm := e.TimeNotThreadsafe() + tm := e.timeNotThreadsafe() assert.Equalf(t, step.wantExcessAfter, tm.Excess(), "%T.Excess()", tm) assert.Equalf(t, step.wantPriceAfter, tm.Price(), "%T.Price()", tm) From 6feac2aef476a8c350dab571041e681de8e2cb2f Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 13:02:19 +0000 Subject: [PATCH 19/37] refactor: simplify `ChainBuilder.NewBlock` options handling --- blocks/blockstest/chain.go | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/blocks/blockstest/chain.go b/blocks/blockstest/chain.go index 2a7053b..7ecb2c4 100644 --- a/blocks/blockstest/chain.go +++ b/blocks/blockstest/chain.go @@ -45,15 +45,15 @@ func (cb *ChainBuilder) SetDefaultOptions(opts ...ChainOption) { } type chainOptions struct { - ethOpts []EthBlockOption - saeOpts []BlockOption + 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.ethOpts = append(co.ethOpts, opts...) + co.eth = append(co.eth, opts...) }) } @@ -61,26 +61,21 @@ func WithEthBlockOptions(opts ...EthBlockOption) ChainOption { // [NewBlock]. func WithBlockOptions(opts ...BlockOption) ChainOption { return options.Func[chainOptions](func(co *chainOptions) { - co.saeOpts = append(co.saeOpts, opts...) + co.sae = append(co.sae, opts...) }) } -func ethBlockOptions(opts []ChainOption) []EthBlockOption { - return options.ApplyTo(&chainOptions{}, opts...).ethOpts -} - -func blockOptions(opts []ChainOption) []BlockOption { - return options.ApplyTo(&chainOptions{}, opts...).saeOpts -} - // NewBlock constructs and returns a new block in the chain. func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts ...ChainOption) *blocks.Block { tb.Helper() - opts = slices.Concat(cb.defaultOpts, opts) + + allOpts := new(chainOptions) + options.ApplyTo(allOpts, cb.defaultOpts...) + options.ApplyTo(allOpts, opts...) last := cb.Last() - eth := NewEthBlock(last.EthBlock(), txs, ethBlockOptions(opts)...) - b := NewBlock(tb, eth, last, nil, blockOptions(opts)...) // TODO(arr4n) support last-settled blocks + 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.byHash[b.Hash()] = b From eb40cdd923f52d8571e2eff3d367e16db2f7344d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 14:17:06 +0000 Subject: [PATCH 20/37] test: `BASEFEE` op code --- saexec/saexec_test.go | 78 ++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index af373a7..453c451 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -5,6 +5,7 @@ package saexec import ( "context" + "encoding/binary" "fmt" "math" "math/big" @@ -517,6 +518,45 @@ func TestGasAccounting(t *testing.T) { assert.Equalf(t, wantBaseFee, gas.Price(b.BaseFee().Uint64()), "%T.BaseFee().Uint64()", b) }) } + + t.Run("BASEFEE_op_code", func(t *testing.T) { + if t.Failed() { + t.Skip("Chain in unexpected state") + } + + finalPrice := uint64(steps[len(steps)-1].wantPriceAfter) + + tx := wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ + To: nil, // runs call data as a constructor + Gas: 100e6, + GasPrice: new(big.Int).SetUint64(finalPrice), + Data: asBytes(logTopOfStackAfter(vm.BASEFEE)...), + }) + + b := chain.NewBlock(t, types.Transactions{tx}) + require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") + require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) + require.Lenf(t, b.Receipts(), 1, "%T.Receipts()", b) + require.Lenf(t, b.Receipts()[0].Logs, 1, "%T.Receipts()[0].Logs", b) + + got := b.Receipts()[0].Logs[0].Topics[0] + want := common.BytesToHash(binary.BigEndian.AppendUint64(nil, finalPrice)) + assert.Equal(t, want, got) + }) +} + +// logTopOfStackAfter returns contract bytecode that logs the value on the top +// of the stack after executing `pre`. +func logTopOfStackAfter(pre ...vm.OpCode) []vm.OpCode { + return slices.Concat(pre, []vm.OpCode{vm.PUSH0, vm.PUSH0, vm.LOG1}) +} + +func asBytes(ops ...vm.OpCode) []byte { + buf := make([]byte, len(ops)) + for i, op := range ops { + buf[i] = byte(op) + } + return buf } func TestContextualOpCodes(t *testing.T) { @@ -530,12 +570,6 @@ func TestContextualOpCodes(t *testing.T) { require.NoErrorf(t, sut.Enqueue(ctx, b), "Enqueue([empty block])") } - // log1 returns contract bytecode that logs the value on the top of the - // stack after executing `pre`. - log1 := func(pre ...vm.OpCode) []vm.OpCode { - return slices.Concat(pre, []vm.OpCode{vm.PUSH0, vm.PUSH0, vm.LOG1}) - } - bigToHash := func(b *big.Int) common.Hash { return uint256.MustFromBig(b).Bytes32() } @@ -553,39 +587,39 @@ func TestContextualOpCodes(t *testing.T) { }{ { name: "BALANCE", - code: log1(vm.ADDRESS, vm.BALANCE), + code: logTopOfStackAfter(vm.ADDRESS, vm.BALANCE), wantTopic: common.Hash{31: txValueSend}, }, { name: "CALLVALUE", - code: log1(vm.CALLVALUE), + code: logTopOfStackAfter(vm.CALLVALUE), wantTopic: common.Hash{31: txValueSend}, }, { name: "SELFBALANCE", - code: log1(vm.SELFBALANCE), + code: logTopOfStackAfter(vm.SELFBALANCE), wantTopic: common.Hash{31: txValueSend}, }, { name: "ORIGIN", - code: log1(vm.ORIGIN), + code: logTopOfStackAfter(vm.ORIGIN), wantTopic: common.BytesToHash( sut.wallet.Addresses()[0].Bytes(), ), }, { name: "BLOCKHASH_genesis", - code: log1(vm.PUSH0, vm.BLOCKHASH), + code: logTopOfStackAfter(vm.PUSH0, vm.BLOCKHASH), wantTopic: chain.AllBlocks()[0].Hash(), }, { name: "BLOCKHASH_arbitrary", - code: log1(vm.PUSH1, 3, vm.BLOCKHASH), + code: logTopOfStackAfter(vm.PUSH1, 3, vm.BLOCKHASH), wantTopic: chain.AllBlocks()[3].Hash(), }, { name: "NUMBER", - code: log1(vm.NUMBER), + code: logTopOfStackAfter(vm.NUMBER), header: saveBlockNum.store, wantTopicFn: func() common.Hash { return bigToHash(saveBlockNum.num) @@ -593,7 +627,7 @@ func TestContextualOpCodes(t *testing.T) { }, { name: "COINBASE_arbitrary", - code: log1(vm.COINBASE), + code: logTopOfStackAfter(vm.COINBASE), header: func(h *types.Header) { h.Coinbase = common.Address{17: 0xC0, 18: 0xFF, 19: 0xEE} }, @@ -601,11 +635,11 @@ func TestContextualOpCodes(t *testing.T) { }, { name: "COINBASE_zero", - code: log1(vm.COINBASE), + code: logTopOfStackAfter(vm.COINBASE), }, { name: "TIMESTAMP", - code: log1(vm.TIMESTAMP), + code: logTopOfStackAfter(vm.TIMESTAMP), header: func(h *types.Header) { h.Time = 0xDECAFBAD }, @@ -613,26 +647,24 @@ func TestContextualOpCodes(t *testing.T) { }, { name: "PREVRANDAO", - code: log1(vm.PREVRANDAO), + code: logTopOfStackAfter(vm.PREVRANDAO), }, { name: "CHAINID", - code: log1(vm.CHAINID), + code: logTopOfStackAfter(vm.CHAINID), wantTopic: bigToHash(sut.ChainConfig().ChainID), }, + // BASEFEE is tested in [TestGasAccounting] because getting the clock + // excess to a specific value is complicated. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - data := make([]byte, len(tt.code)) - for i, op := range tt.code { - data[i] = byte(op) - } tx := sut.wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ To: nil, // contract creation runs the call data (one sneaky trick blockchain developers don't want you to know) GasPrice: big.NewInt(1), Gas: 100e6, - Data: data, + Data: asBytes(tt.code...), Value: big.NewInt(txValueSend), }) From 9e314c42365ea4d31af2bb0283490e30617bceb5 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 14:25:43 +0000 Subject: [PATCH 21/37] test: additional contextual op codes --- saexec/saexec_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 453c451..f01ab62 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -586,7 +586,7 @@ func TestContextualOpCodes(t *testing.T) { wantTopicFn func() common.Hash // if non-nil, overrides `wantTopic` }{ { - name: "BALANCE", + name: "BALANCE_of_ADDRESS", code: logTopOfStackAfter(vm.ADDRESS, vm.BALANCE), wantTopic: common.Hash{31: txValueSend}, }, @@ -607,6 +607,13 @@ func TestContextualOpCodes(t *testing.T) { sut.wallet.Addresses()[0].Bytes(), ), }, + { + name: "CALLER", + code: logTopOfStackAfter(vm.CALLER), + wantTopic: common.BytesToHash( + sut.wallet.Addresses()[0].Bytes(), + ), + }, { name: "BLOCKHASH_genesis", code: logTopOfStackAfter(vm.PUSH0, vm.BLOCKHASH), @@ -649,6 +656,14 @@ func TestContextualOpCodes(t *testing.T) { name: "PREVRANDAO", code: logTopOfStackAfter(vm.PREVRANDAO), }, + { + name: "GASLIMIT", + code: logTopOfStackAfter(vm.GASLIMIT), + header: func(h *types.Header) { + h.GasLimit = 0xA11CEB0B + }, + wantTopic: common.BytesToHash([]byte{0xA1, 0x1C, 0xEB, 0x0B}), + }, { name: "CHAINID", code: logTopOfStackAfter(vm.CHAINID), From e042828df0dc82d1fd6df27779ee6fc316c27918 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 19:40:06 +0000 Subject: [PATCH 22/37] refactor: exponential backoff for `Enqueue()` wait warning --- saexec/execution.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/saexec/execution.go b/saexec/execution.go index 2c834d3..f77e16d 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "math" - "runtime" "time" "github.com/ava-labs/avalanchego/utils/logging" @@ -31,6 +30,7 @@ var errExecutorClosed = errors.New("saexec.Executor closed") // before [blocks.Block.Executed] returns true then there is no guarantee that // the block will be executed. func (e *Executor) Enqueue(ctx context.Context, block *blocks.Block) error { + warnAfter := time.Millisecond for { select { case e.queue <- block: @@ -39,10 +39,14 @@ func (e *Executor) Enqueue(ctx context.Context, block *blocks.Block) error { return errExecutorClosed case <-ctx.Done(): return ctx.Err() - default: + case <-time.After(warnAfter): // If this happens then increase the channel's buffer size. - e.log.Warn("Execution queue buffer too small") - runtime.Gosched() + e.log.Warn( + "Execution queue buffer too small", + zap.Duration("wait", warnAfter), + zap.Uint64("block_height", block.Height()), + ) + warnAfter *= 2 } } } From 894b190de47f67c3517efa2be064f2e470114533 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 19:44:03 +0000 Subject: [PATCH 23/37] doc: permalink to `Escrow.sol` --- saetest/escrow/escrow.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/saetest/escrow/escrow.go b/saetest/escrow/escrow.go index e15e6e9..0f4efd1 100644 --- a/saetest/escrow/escrow.go +++ b/saetest/escrow/escrow.go @@ -6,8 +6,10 @@ // // Copyright 2024 Divergence Tech Ltd. -// Package escrow provides bytecode and helpers for the Escrow.sol contract +// Package escrow provides bytecode and helpers for the [Escrow.sol] contract // deployed to 0x370F21541173E8B773571c135e3b5617d7f38C54 on Ethereum mainnet. +// +// [Escrow.sol]: https://github.com/ARR4N/SWAP2/blob/fe724e87bdc998c3b497c16e35fed354e53dc3e9/src/Escrow.sol package escrow import ( From 88d07286d707ac1dd27769f6532178672fa82c39 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 18 Nov 2025 19:48:21 +0000 Subject: [PATCH 24/37] refactor: return `consensus.Engine(nil)` Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saexec/context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/saexec/context.go b/saexec/context.go index 8754cec..c9af510 100644 --- a/saexec/context.go +++ b/saexec/context.go @@ -37,5 +37,5 @@ func (c *chainContext) Engine() consensus.Engine { // not enough to be fatal. It will also cause tests to fail if ever called, // so we can catch it early. c.log.Error("ChainContext.Engine() called unexpectedly") - return struct{ consensus.Engine }{} + return nil } From f55b3b91b9ec053dbea8c7f88d1126a24dacbecf Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:03:59 +0000 Subject: [PATCH 25/37] refactor: correct parenthesis matching Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saexec/saexec_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index f01ab62..4cd574d 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -83,8 +83,8 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { chain := blockstest.NewChainBuilder(genesis) chain.SetDefaultOptions(blockstest.WithBlockOptions( - blockstest.WithLogger(logger)), - ) + blockstest.WithLogger(logger), + )) src := BlockSource(func(h common.Hash, n uint64) *blocks.Block { b, ok := chain.GetBlock(h, n) if !ok { From 00e5417c9952e514c8e36b5ce044bb554598328a Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 19:50:06 +0000 Subject: [PATCH 26/37] refactor: stop embedding `BlockSource` in `chainContext` --- saexec/context.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/saexec/context.go b/saexec/context.go index c9af510..eca9dd5 100644 --- a/saexec/context.go +++ b/saexec/context.go @@ -20,12 +20,12 @@ type BlockSource func(hash common.Hash, number uint64) *blocks.Block var _ core.ChainContext = (*chainContext)(nil) type chainContext struct { - BlockSource - log logging.Logger + blocks BlockSource + log logging.Logger } func (c *chainContext) GetHeader(h common.Hash, n uint64) *types.Header { - b := c.BlockSource(h, n) + b := c.blocks(h, n) if b == nil { return nil } From c1f12b87b5a48a31122299aca83f67502caa3c8b Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 19:59:26 +0000 Subject: [PATCH 27/37] fix: `ChainBuilder` uses `sync.Map` for blocks stored by hash --- blocks/blockstest/chain.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/blocks/blockstest/chain.go b/blocks/blockstest/chain.go index 7ecb2c4..9a5a569 100644 --- a/blocks/blockstest/chain.go +++ b/blocks/blockstest/chain.go @@ -9,6 +9,7 @@ package blockstest import ( "slices" + "sync" "testing" "github.com/ava-labs/libevm/common" @@ -20,8 +21,8 @@ import ( // A ChainBuilder builds a chain of blocks, maintaining necessary invariants. type ChainBuilder struct { - chain []*blocks.Block - byHash map[common.Hash]*blocks.Block + chain []*blocks.Block + blocksByHash sync.Map defaultOpts []ChainOption } @@ -30,8 +31,7 @@ type ChainBuilder struct { // which MUST NOT be nil. func NewChainBuilder(genesis *blocks.Block) *ChainBuilder { return &ChainBuilder{ - chain: []*blocks.Block{genesis}, - byHash: make(map[common.Hash]*blocks.Block), + chain: []*blocks.Block{genesis}, } } @@ -77,9 +77,9 @@ func (cb *ChainBuilder) NewBlock(tb testing.TB, txs []*types.Transaction, opts . 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.byHash[b.Hash()] = b + cb.blocksByHash.Store(b.Hash(), b) - return cb.Last() + return b } // Last returns the last block to be built by the builder, which MAY be the @@ -103,7 +103,8 @@ func (cb *ChainBuilder) AllExceptGenesis() []*blocks.Block { // 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) { - b, ok := cb.byHash[h] + ifc, _ := cb.blocksByHash.Load(h) + b, ok := ifc.(*blocks.Block) if !ok || b.NumberU64() != num { return nil, false } From 4fe05c1a77b7261ddb0b0de05ef05fd66ef2be63 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:06:53 +0000 Subject: [PATCH 28/37] refactor: remove specific type from unindexed `for range` Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saexec/saexec_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 4cd574d..aa2f189 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -124,7 +124,7 @@ func TestExecutionSynchronisation(t *testing.T) { ctx, sut := newSUT(t, defaultHooks()) e, chain := sut.Executor, sut.chain - for range uint64(10) { + for range 10 { b := chain.NewBlock(t, nil) require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") } From 0fafec482c3377dc0c824f93fabba0a9c0e426b2 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:08:25 +0000 Subject: [PATCH 29/37] refactor: determine contract address from `Nonce()` value of tx Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saexec/saexec_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index aa2f189..69801d6 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -261,12 +261,11 @@ func TestExecution(t *testing.T) { want types.Receipts ) deploy := wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ - Nonce: 0, Data: escrow.CreationCode(), GasPrice: big.NewInt(1), Gas: 1e7, }) - contract := crypto.CreateAddress(eoa, 0) + contract := crypto.CreateAddress(eoa, deploy.Nonce()) txs = append(txs, deploy) want = append(want, &types.Receipt{ TxHash: deploy.Hash(), From e64de9c93002a64d5f74724bd56b87ebc001d7b6 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 20:12:14 +0000 Subject: [PATCH 30/37] refactor: use `math.Pow()` for calculating gas price in test --- saexec/saexec_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 69801d6..643cddf 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -445,8 +445,8 @@ func TestGasAccounting(t *testing.T) { numTxs: 30 * gastime.TargetToExcessScaling, // deliberate, see below wantExecutedBy: at(21, 30*gastime.TargetToExcessScaling, 10*gasPerTx), wantExcessAfter: 3 * ((5 * gasPerTx /*T*/) * gastime.TargetToExcessScaling /* == K */), - // Excess is now 3·K so the price is e^3 = 20.09 - wantPriceAfter: 20, + // Excess is now 3·K so the price is e^3 + wantPriceAfter: gas.Price(math.Floor(math.Pow(math.E, 3 /* <----- NB */))), }, { target: 5 * gasPerTx, From beed9cae104a7fa5c0be86b2c2fbf7c37b906c50 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 20:31:37 +0000 Subject: [PATCH 31/37] refactor: remove `executionScratchSpace` and `Executor.init()` --- saexec/execution.go | 39 ++++++++++----------------------- saexec/saexec.go | 53 +++++++++++++++------------------------------ 2 files changed, 29 insertions(+), 63 deletions(-) diff --git a/saexec/execution.go b/saexec/execution.go index f77e16d..dd0b960 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -12,10 +12,8 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/components/gas" - "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/state" - "github.com/ava-labs/libevm/core/state/snapshot" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" "go.uber.org/zap" @@ -75,11 +73,6 @@ func (e *Executor) processQueue() { } } -type executionScratchSpace struct { - snaps *snapshot.Tree - statedb *state.StateDB -} - func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { logger.Debug("Executing block") @@ -90,10 +83,14 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { return fmt.Errorf("executing blocks out of order: %d then %d", last, curr) } - scratch := &e.executeScratchSpace rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime()) - if err := hook.BeforeBlock(e.hooks, rules, scratch.statedb, b, e.gasClock); err != nil { + stateDB, err := state.New(b.ParentBlock().PostExecutionStateRoot(), e.stateCache, e.snaps) + if err != nil { + return fmt.Errorf("state.New(%#x, ...): %v", b.ParentBlock().PostExecutionStateRoot(), err) + } + + if err := hook.BeforeBlock(e.hooks, rules, stateDB, b, e.gasClock); err != nil { return fmt.Errorf("before-block hook: %v", err) } perTxClock := e.gasClock.Time.Clone() @@ -106,14 +103,14 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { receipts := make(types.Receipts, len(b.Transactions())) for ti, tx := range b.Transactions() { - scratch.statedb.SetTxContext(tx.Hash(), ti) + stateDB.SetTxContext(tx.Hash(), ti) receipt, err := core.ApplyTransaction( e.chainConfig, e.chainContext, &header.Coinbase, &gasPool, - scratch.statedb, + stateDB, header, tx, (*uint64)(&blockGasConsumed), @@ -151,7 +148,7 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { receipts[ti] = receipt } endTime := time.Now() - hook.AfterBlock(e.hooks, scratch.statedb, b.EthBlock(), e.gasClock, blockGasConsumed, receipts) + hook.AfterBlock(e.hooks, stateDB, b.EthBlock(), e.gasClock, blockGasConsumed, receipts) if e.gasClock.Time.Compare(perTxClock) != 0 { return fmt.Errorf("broken invariant: block-resolution clock @ %s does not match tx-resolution clock @ %s", e.gasClock.String(), perTxClock.String()) } @@ -163,9 +160,9 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { zap.Time("wall_time", endTime), ) - root, err := e.commitState(scratch, b.NumberU64()) + root, err := stateDB.Commit(b.NumberU64(), true) if err != nil { - return err + return fmt.Errorf("%T.Commit() at end of block %d: %w", stateDB, b.NumberU64(), err) } // The strict ordering of the next 3 calls guarantees invariants that MUST // NOT be broken: @@ -180,17 +177,3 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { e.sendPostExecutionEvents(b.EthBlock(), receipts) // (3) return nil } - -func (e *Executor) commitState(scratch *executionScratchSpace, blockNum uint64) (common.Hash, error) { - root, err := scratch.statedb.Commit(blockNum, true) - if err != nil { - return common.Hash{}, fmt.Errorf("%T.Commit() at end of block %d: %w", scratch.statedb, blockNum, err) - } - - db, err := state.New(root, e.stateCache, scratch.snaps) - if err != nil { - return common.Hash{}, err - } - scratch.statedb = db - return root, nil -} diff --git a/saexec/saexec.go b/saexec/saexec.go index 8feca0d..17219fa 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -43,9 +43,9 @@ type Executor struct { chainConfig *params.ChainConfig db ethdb.Database stateCache state.Database - // executeScratchSpace MUST NOT be accessed by any methods other than - // [Executor.init], [Executor.execute], and [Executor.Close]. - executeScratchSpace executionScratchSpace + // snaps MUST NOT be accessed by any methods other than [Executor.execute] + // and [Executor.Close]. + snaps *snapshot.Tree } // New constructs and starts a new [Executor]. Call [Executor.Close] to release @@ -63,60 +63,43 @@ func New( hooks hook.Points, log logging.Logger, ) (*Executor, error) { + cache := state.NewDatabaseWithConfig(db, triedbConfig) + snapConf := snapshot.Config{ + CacheSize: 128, // MB + AsyncBuild: true, + } + snaps, err := snapshot.New(snapConf, db, cache.TrieDB(), lastExecuted.PostExecutionStateRoot()) + if err != nil { + return nil, err + } + e := &Executor{ quit: make(chan struct{}), // closed by [Executor.Close] done: make(chan struct{}), // closed by [Executor.processQueue] after `quit` is closed log: log, hooks: hooks, + gasClock: lastExecuted.ExecutedByGasTime().Clone(), queue: make(chan *blocks.Block, 4096), // arbitrarily sized chainContext: &chainContext{blockSrc, log}, chainConfig: chainConfig, db: db, - stateCache: state.NewDatabaseWithConfig(db, triedbConfig), + stateCache: cache, + snaps: snaps, } e.lastExecuted.Store(lastExecuted) - if err := e.init(); err != nil { - return nil, err - } go e.processQueue() return e, nil } -func (e *Executor) init() error { - last := e.lastExecuted.Load() - e.gasClock = last.ExecutedByGasTime().Clone() - - root := last.PostExecutionStateRoot() - snapConf := snapshot.Config{ - CacheSize: 128, // MB - AsyncBuild: true, - } - snaps, err := snapshot.New(snapConf, e.db, e.stateCache.TrieDB(), root) - if err != nil { - return err - } - statedb, err := state.New(root, e.stateCache, snaps) - if err != nil { - return err - } - - e.executeScratchSpace = executionScratchSpace{ - snaps: snaps, - statedb: statedb, - } - return nil -} - // Close shuts down the [Executor], waits for the currently executing block // to complete, and then releases all resources. func (e *Executor) Close() { close(e.quit) <-e.done - snaps := e.executeScratchSpace.snaps - snaps.Disable() - snaps.Release() + e.snaps.Disable() + e.snaps.Release() } // ChainConfig returns the config originally passed to [New]. From c6d1ef2cc5282467eee56f2779f35fbb39dd879c Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 18 Nov 2025 20:53:04 +0000 Subject: [PATCH 32/37] refactor: make gas clock internal variable in `Executor.execute()` --- saexec/execution.go | 25 +++++++++++++++---------- saexec/saexec.go | 3 --- saexec/saexec_test.go | 28 +++++++--------------------- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/saexec/execution.go b/saexec/execution.go index dd0b960..18d6c19 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -85,18 +85,23 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime()) - stateDB, err := state.New(b.ParentBlock().PostExecutionStateRoot(), e.stateCache, e.snaps) + // Since `b` hasn't been executed, it definitely hasn't been settled, so we + // are guaranteed to have a non-nil parent available. + parent := b.ParentBlock() + + stateDB, err := state.New(parent.PostExecutionStateRoot(), e.stateCache, e.snaps) if err != nil { - return fmt.Errorf("state.New(%#x, ...): %v", b.ParentBlock().PostExecutionStateRoot(), err) + return fmt.Errorf("state.New(%#x, ...): %v", parent.PostExecutionStateRoot(), err) } + gasClock := parent.ExecutedByGasTime().Clone() - if err := hook.BeforeBlock(e.hooks, rules, stateDB, b, e.gasClock); err != nil { + if err := hook.BeforeBlock(e.hooks, rules, stateDB, b, gasClock); err != nil { return fmt.Errorf("before-block hook: %v", err) } - perTxClock := e.gasClock.Time.Clone() + perTxClock := gasClock.Time.Clone() header := types.CopyHeader(b.Header()) - header.BaseFee = e.gasClock.BaseFee().ToBig() + header.BaseFee = gasClock.BaseFee().ToBig() gasPool := core.GasPool(math.MaxUint64) // required by geth but irrelevant so max it out var blockGasConsumed gas.Gas @@ -148,15 +153,15 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { receipts[ti] = receipt } endTime := time.Now() - hook.AfterBlock(e.hooks, stateDB, b.EthBlock(), e.gasClock, blockGasConsumed, receipts) - if e.gasClock.Time.Compare(perTxClock) != 0 { - return fmt.Errorf("broken invariant: block-resolution clock @ %s does not match tx-resolution clock @ %s", e.gasClock.String(), perTxClock.String()) + hook.AfterBlock(e.hooks, stateDB, b.EthBlock(), gasClock, blockGasConsumed, receipts) + if gasClock.Time.Compare(perTxClock) != 0 { + return fmt.Errorf("broken invariant: block-resolution clock @ %s does not match tx-resolution clock @ %s", gasClock.String(), perTxClock.String()) } logger.Debug( "Block execution complete", zap.Uint64("gas_consumed", uint64(blockGasConsumed)), - zap.Time("gas_time", e.gasClock.AsTime()), + zap.Time("gas_time", gasClock.AsTime()), zap.Time("wall_time", endTime), ) @@ -170,7 +175,7 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { // 1. [blocks.Block.MarkExecuted] guarantees disk then in-memory changes. // 2. Internal indicator of last executed MUST follow in-memory change. // 3. External indicator of last executed MUST follow internal indicator. - if err := b.MarkExecuted(e.db, e.gasClock.Clone(), endTime, header.BaseFee, receipts, root); err != nil { + if err := b.MarkExecuted(e.db, gasClock.Clone(), endTime, header.BaseFee, receipts, root); err != nil { return err } e.lastExecuted.Store(b) // (2) diff --git a/saexec/saexec.go b/saexec/saexec.go index 17219fa..5df915f 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -21,7 +21,6 @@ import ( "github.com/ava-labs/libevm/triedb" "github.com/ava-labs/strevm/blocks" - "github.com/ava-labs/strevm/gastime" "github.com/ava-labs/strevm/hook" ) @@ -31,7 +30,6 @@ type Executor struct { log logging.Logger hooks hook.Points - gasClock *gastime.Time queue chan *blocks.Block lastExecuted atomic.Pointer[blocks.Block] @@ -78,7 +76,6 @@ func New( done: make(chan struct{}), // closed by [Executor.processQueue] after `quit` is closed log: log, hooks: hooks, - gasClock: lastExecuted.ExecutedByGasTime().Clone(), queue: make(chan *blocks.Block, 4096), // arbitrarily sized chainContext: &chainContext{blockSrc, log}, chainConfig: chainConfig, diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 643cddf..0f7daa4 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -6,7 +6,6 @@ package saexec import ( "context" "encoding/binary" - "fmt" "math" "math/big" "math/rand/v2" @@ -105,13 +104,6 @@ func newSUT(tb testing.TB, hooks hook.Points) (context.Context, SUT) { } } -// timeNotThreadsafe returns a clone of the gas clock that times execution. It -// is only safe to call when all blocks passed to [Executor.Enqueue] -// have been executed. -func (e *Executor) timeNotThreadsafe() *gastime.Time { - return e.gasClock.Clone() -} - func defaultHooks() *saehookstest.Stub { return &saehookstest.Stub{Target: 1e6} } @@ -481,14 +473,9 @@ func TestGasAccounting(t *testing.T) { require.NoError(t, e.Enqueue(ctx, b), "Enqueue()") require.NoErrorf(t, b.WaitUntilExecuted(ctx), "%T.WaitUntilExecuted()", b) - for desc, got := range map[string]*gastime.Time{ - fmt.Sprintf("%T.ExecutedByGasTime()", b): b.ExecutedByGasTime(), - fmt.Sprintf("%T.TimeNotThreadSafe()", e): e.timeNotThreadsafe(), - } { - opt := proxytime.CmpOpt[gas.Gas](proxytime.IgnoreRateInvariants) - if diff := cmp.Diff(step.wantExecutedBy, got.Time, opt); diff != "" { - t.Errorf("%s diff (-want +got):\n%s", desc, diff) - } + opt := proxytime.CmpOpt[gas.Gas](proxytime.IgnoreRateInvariants) + if diff := cmp.Diff(step.wantExecutedBy, b.ExecutedByGasTime().Time, opt); diff != "" { + t.Errorf("%T.ExecutedByGasTime().Time diff (-want +got):\n%s", b, diff) } t.Run("CumulativeGasUsed", func(t *testing.T) { @@ -505,7 +492,7 @@ func TestGasAccounting(t *testing.T) { } t.Run("gas_price", func(t *testing.T) { - tm := e.timeNotThreadsafe() + tm := b.ExecutedByGasTime().Clone() assert.Equalf(t, step.wantExcessAfter, tm.Excess(), "%T.Excess()", tm) assert.Equalf(t, step.wantPriceAfter, tm.Price(), "%T.Price()", tm) @@ -517,12 +504,11 @@ func TestGasAccounting(t *testing.T) { assert.Equalf(t, wantBaseFee, gas.Price(b.BaseFee().Uint64()), "%T.BaseFee().Uint64()", b) }) } + if t.Failed() { + t.Skip("Chain in unexpected state") + } t.Run("BASEFEE_op_code", func(t *testing.T) { - if t.Failed() { - t.Skip("Chain in unexpected state") - } - finalPrice := uint64(steps[len(steps)-1].wantPriceAfter) tx := wallet.SetNonceAndSign(t, 0, &types.LegacyTx{ From 48342cd02bd87397f473d4f4539c83ed8d54c121 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 19 Nov 2025 09:24:59 +0000 Subject: [PATCH 33/37] fix: `ApplyTransaction` errors are `FATAL` --- saexec/execution.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/saexec/execution.go b/saexec/execution.go index 18d6c19..ce6f4f3 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -33,10 +33,16 @@ func (e *Executor) Enqueue(ctx context.Context, block *blocks.Block) error { select { case e.queue <- block: return nil - case <-e.quit: - return errExecutorClosed + case <-ctx.Done(): return ctx.Err() + + case <-e.quit: + return errExecutorClosed + case <-e.done: + // `e.done` can also close due to [Executor.execute] errors. + return errExecutorClosed + case <-time.After(warnAfter): // If this happens then increase the channel's buffer size. e.log.Warn( @@ -66,7 +72,11 @@ func (e *Executor) processQueue() { ) if err := e.execute(block, logger); err != nil { - logger.Error("Block execution failed", zap.Error(err)) + logger.Fatal( + "Block execution failed; see emergency playbook", + zap.Error(err), + zap.String("playbook", "https://github.com/ava-labs/strevm/issues/28"), + ) return } } @@ -122,15 +132,14 @@ func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { vm.Config{}, ) if err != nil { - // This almost certainly means that the worst-case block inclusion - // has a bug. - logger.Error( - "Transaction execution errored (not reverted)", + logger.Fatal( + "Transaction execution errored (not reverted); see emergency playbook", zap.Int("tx_index", ti), zap.Stringer("tx_hash", tx.Hash()), + zap.String("playbook", "https://github.com/ava-labs/strevm/issues/28"), zap.Error(err), ) - continue + return err } perTxClock.Tick(gas.Gas(receipt.GasUsed)) From caf324246895c92354b7b65ca85c3ccae4122cf4 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 19 Nov 2025 10:24:10 +0000 Subject: [PATCH 34/37] feat: `snapshot.Tree.Cap(..., 0)` on shutdown to persist top layer --- saexec/saexec.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/saexec/saexec.go b/saexec/saexec.go index 5df915f..1acdaae 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -19,6 +19,7 @@ import ( "github.com/ava-labs/libevm/event" "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/triedb" + "go.uber.org/zap" "github.com/ava-labs/strevm/blocks" "github.com/ava-labs/strevm/hook" @@ -95,6 +96,18 @@ func (e *Executor) Close() { close(e.quit) <-e.done + // We don't use [snapshot.Tree.Journal] because re-orgs are impossible under + // SAE so we don't mind flattening all snapshot layers to disk. Note that + // calling `Cap([disk root], 0)` returns an error when it's actually a + // no-op, so we ignore it. + root := e.LastExecuted().PostExecutionStateRoot() + if err := e.snaps.Cap(root, 0); err != nil && root != e.snaps.DiskRoot() { + e.log.Warn( + "snapshot.Tree.Cap([last post-execution state root], 0)", + zap.Error(err), + ) + } + e.snaps.Disable() e.snaps.Release() } From 0e3a778d9c9a39fd3f953977142cfcd8b0468ee0 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:33:12 +0000 Subject: [PATCH 35/37] refactor: review suggestions Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saexec/saexec.go | 14 ++++++++------ saexec/saexec_test.go | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/saexec/saexec.go b/saexec/saexec.go index 1acdaae..879f2d3 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -100,12 +100,14 @@ func (e *Executor) Close() { // SAE so we don't mind flattening all snapshot layers to disk. Note that // calling `Cap([disk root], 0)` returns an error when it's actually a // no-op, so we ignore it. - root := e.LastExecuted().PostExecutionStateRoot() - if err := e.snaps.Cap(root, 0); err != nil && root != e.snaps.DiskRoot() { - e.log.Warn( - "snapshot.Tree.Cap([last post-execution state root], 0)", - zap.Error(err), - ) + if root := e.LastExecuted().PostExecutionStateRoot(); root != e.snaps.DiskRoot() { + if err := e.snaps.Cap(root, 0); err != nil { + e.log.Warn( + "snapshot.Tree.Cap([last post-execution state root], 0)", + zap.Stringer("root", root), + zap.Error(err), + ) + } } e.snaps.Disable() diff --git a/saexec/saexec_test.go b/saexec/saexec_test.go index 0f7daa4..617c7b5 100644 --- a/saexec/saexec_test.go +++ b/saexec/saexec_test.go @@ -505,7 +505,7 @@ func TestGasAccounting(t *testing.T) { }) } if t.Failed() { - t.Skip("Chain in unexpected state") + t.Fatal("Chain in unexpected state") } t.Run("BASEFEE_op_code", func(t *testing.T) { From 06256a9ce55c76b3d4402c98fc82a0bfcf3d74cc Mon Sep 17 00:00:00 2001 From: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:33:39 +0000 Subject: [PATCH 36/37] refactor!: don't disable snapshot generation on shutdown Co-authored-by: Stephen Buttolph Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> --- saexec/saexec.go | 1 - 1 file changed, 1 deletion(-) diff --git a/saexec/saexec.go b/saexec/saexec.go index 879f2d3..a07deb9 100644 --- a/saexec/saexec.go +++ b/saexec/saexec.go @@ -110,7 +110,6 @@ func (e *Executor) Close() { } } - e.snaps.Disable() e.snaps.Release() } From f41f1a7de9f03909fad255cbb035e331160b414b Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 19 Nov 2025 17:41:05 +0000 Subject: [PATCH 37/37] refactor: base block-ordering assertion on parent hash --- saexec/execution.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/saexec/execution.go b/saexec/execution.go index ce6f4f3..68df2b7 100644 --- a/saexec/execution.go +++ b/saexec/execution.go @@ -86,25 +86,23 @@ func (e *Executor) processQueue() { func (e *Executor) execute(b *blocks.Block, logger logging.Logger) error { logger.Debug("Executing block") + // Since `b` hasn't been executed, it definitely hasn't been settled, so we + // are guaranteed to have a non-nil parent available. + parent := b.ParentBlock() // If the VM were to encounter an error after enqueuing the block, we would // receive the same block twice for execution should consensus retry // acceptance. - if last, curr := e.lastExecuted.Load().Height(), b.Height(); curr != last+1 { - return fmt.Errorf("executing blocks out of order: %d then %d", last, curr) + if last := e.lastExecuted.Load().Hash(); last != parent.Hash() { + return fmt.Errorf("executing block built on parent %#x when last executed %#x", parent.Hash(), last) } - rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime()) - - // Since `b` hasn't been executed, it definitely hasn't been settled, so we - // are guaranteed to have a non-nil parent available. - parent := b.ParentBlock() - stateDB, err := state.New(parent.PostExecutionStateRoot(), e.stateCache, e.snaps) if err != nil { return fmt.Errorf("state.New(%#x, ...): %v", parent.PostExecutionStateRoot(), err) } - gasClock := parent.ExecutedByGasTime().Clone() + rules := e.chainConfig.Rules(b.Number(), true /*isMerge*/, b.BuildTime()) + gasClock := parent.ExecutedByGasTime().Clone() if err := hook.BeforeBlock(e.hooks, rules, stateDB, b, gasClock); err != nil { return fmt.Errorf("before-block hook: %v", err) }