From fd542db13cd24bd43530c1b486751b046062ce35 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 15 Sep 2025 13:10:52 +0100 Subject: [PATCH 01/18] feat: `parallel` package for precompile pre-processing --- go.mod | 1 + go.sum | 2 + libevm/precompiles/parallel/parallel.go | 170 +++++++++++++++++++ libevm/precompiles/parallel/parallel_test.go | 148 ++++++++++++++++ 4 files changed, 321 insertions(+) create mode 100644 libevm/precompiles/parallel/parallel.go create mode 100644 libevm/precompiles/parallel/parallel_test.go diff --git a/go.mod b/go.mod index 7a814eec1ec..e7dc2f5b1c0 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.25.7 go.uber.org/automaxprocs v1.5.2 + go.uber.org/goleak v1.3.0 golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/mod v0.14.0 diff --git a/go.sum b/go.sum index 2156147c18f..87821192c5e 100644 --- a/go.sum +++ b/go.sum @@ -622,6 +622,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME= go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go new file mode 100644 index 00000000000..026b488ef30 --- /dev/null +++ b/libevm/precompiles/parallel/parallel.go @@ -0,0 +1,170 @@ +package parallel + +import ( + "fmt" + "sync" + + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/types" +) + +// A Handler is responsible for processing [types.Transactions] in an +// embarrassingly parallel fashion. It is the responsibility of the Handler to +// determine whether this is possible, typically only so if one of the following +// is true with respect to a precompile associated with the Handler: +// +// 1. The destination address is that of the precompile; or +// +// 2. At least one [types.AccessTuple] references the precompile's address. +// +// Scenario (2) allows precompile access to be determined through inspection of +// the [types.Transaction] alone, without the need for execution. +type Handler[Result any] interface { + Gas(*types.Transaction) (gas uint64, process bool) + Process(index int, tx *types.Transaction) Result +} + +// A Processor orchestrates dispatch and collection of results from a [Handler]. +type Processor[R any] struct { + handler Handler[R] + workers sync.WaitGroup + work chan *job + results [](chan *R) +} + +type job struct { + index int + tx *types.Transaction +} + +// New constructs a new [Processor] with the specified number of concurrent +// workers. [Processor.Close] must be called after the final call to +// [Processor.FinishBlock] to avoid leaking goroutines. +func New[R any](h Handler[R], workers int) *Processor[R] { + p := &Processor[R]{ + handler: h, + work: make(chan *job), + } + + workers = max(workers, 1) + p.workers.Add(workers) + for range workers { + go p.worker() + } + return p +} + +func (p *Processor[R]) worker() { + defer p.workers.Done() + for { + w, ok := <-p.work + if !ok { + return + } + + r := p.handler.Process(w.index, w.tx) + p.results[w.index] <- &r + } +} + +// Close shuts down the [Processor], after which it can no longer be used. +func (p *Processor[R]) Close() { + close(p.work) + p.workers.Wait() +} + +// StartBlock dispatches transactions to the [Handler] and returns immediately. +// It MUST be paired with a call to [Processor.FinishBlock], without overlap of +// blocks. +func (p *Processor[R]) StartBlock(b *types.Block) error { + txs := b.Transactions() + jobs := make([]*job, 0, len(txs)) + + // We can reuse the channels already in the results slice because they're + // emptied by [Processor.FinishBlock]. + for i, n := len(p.results), len(txs); i < n; i++ { + p.results = append(p.results, make(chan *R, 1)) + } + + for i, tx := range txs { + switch do, err := p.shouldProcess(tx); { + case err != nil: + return err + + case do: + jobs = append(jobs, &job{ + index: i, + tx: tx, + }) + + default: + p.results[i] <- nil + } + } + + go func() { + // This goroutine is guaranteed to have returned by the time + // [Processor.FinishBlock] does. + for _, j := range jobs { + p.work <- j + } + }() + return nil +} + +// FinishBlock returns the [Processor] to a state ready for the next block. A +// return from FinishBlock guarantees that all dispatched work from the +// respective call to [Processor.StartBlock] has been completed. +func (p *Processor[R]) FinishBlock(b *types.Block) { + for i := range len(b.Transactions()) { + // Every result channel is guaranteed to have some value in its buffer + // because [Processor.BeforeBlock] either sends a nil *R or it + // dispatches a job that will send a non-nil *R. + <-p.results[i] + } +} + +// Result blocks until the i'th transaction passed to [Processor.StartBlock] has +// had its result processed, and then returns the value returned by the +// [Handler]. The returned boolean will be false if no processing occurred, +// either because the [Handler] indicated as such or because the transaction +// supplied insufficient gas. +func (p *Processor[R]) Result(i int) (R, bool) { + ch := p.results[i] + r := <-ch + defer func() { + ch <- r + }() + + if r == nil { + // TODO(arr4n) if we're here then the implementoor might have a bug in + // their [Handler], so logging a warning is probably a good idea. + var zero R + return zero, false + } + return *r, true +} + +func (p *Processor[R]) shouldProcess(tx *types.Transaction) (bool, error) { + cost, ok := p.handler.Gas(tx) + if !ok { + return false, nil + } + + spent, err := core.IntrinsicGas( + tx.Data(), + tx.AccessList(), + tx.To() == nil, + true, // Homestead + true, // EIP-2028 (Istanbul) + true, // EIP-3860 (Shanghai) + ) + if err != nil { + return false, fmt.Errorf("calculating intrinsic gas of %v: %v", tx.Hash(), err) + } + + // This could only overflow if the gas limit was insufficient to cover + // the intrinsic cost, which would have invalidated it for inclusion. + left := tx.Gas() - spent + return left >= cost, nil +} diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go new file mode 100644 index 00000000000..9f71d3c7d8b --- /dev/null +++ b/libevm/precompiles/parallel/parallel_test.go @@ -0,0 +1,148 @@ +package parallel + +import ( + "crypto/sha256" + "encoding/binary" + "math/rand/v2" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/trie" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m, goleak.IgnoreCurrent()) +} + +type shaHandler struct { + addr common.Address + gas uint64 +} + +func (h *shaHandler) Gas(tx *types.Transaction) (uint64, bool) { + if to := tx.To(); to == nil || *to != h.addr { + return 0, false + } + return h.gas, true +} + +func (*shaHandler) Process(i int, tx *types.Transaction) [sha256.Size]byte { + return sha256.Sum256(tx.Data()) +} + +func TestProcessor(t *testing.T) { + handler := &shaHandler{ + addr: common.Address{'s', 'h', 'a', 2, 5, 6}, + gas: 1e6, + } + p := New(handler, 8) + t.Cleanup(p.Close) + + type blockParams struct { + numTxs int + sendToAddrEvery, sufficientGasEvery int + } + + // Each set of params is effectively a test case, but they are all run on + // the same [Processor]. + params := []blockParams{ + { + numTxs: 0, + }, + { + numTxs: 500, + sendToAddrEvery: 7, + sufficientGasEvery: 5, + }, + { + numTxs: 1_000, + sendToAddrEvery: 7, + sufficientGasEvery: 5, + }, + { + numTxs: 1_000, + sendToAddrEvery: 11, + sufficientGasEvery: 3, + }, + { + numTxs: 100, + sendToAddrEvery: 1, + sufficientGasEvery: 1, + }, + { + numTxs: 0, + }, + } + + rng := rand.New(rand.NewPCG(0, 0)) + for range 100 { + params = append(params, blockParams{ + numTxs: rng.IntN(1000), + sendToAddrEvery: 1 + rng.IntN(30), + sufficientGasEvery: 1 + rng.IntN(30), + }) + } + + for _, tc := range params { + t.Run("", func(t *testing.T) { + t.Logf("%+v", tc) + + txs := make(types.Transactions, tc.numTxs) + wantProcessed := make([]bool, tc.numTxs) + for i := range len(txs) { + var ( + to common.Address + extraGas uint64 + ) + + wantProcessed[i] = true + if i%tc.sendToAddrEvery == 0 { + to = handler.addr + } else { + wantProcessed[i] = false + } + if i%tc.sufficientGasEvery == 0 { + extraGas = handler.gas + } else { + wantProcessed[i] = false + } + + data := binary.BigEndian.AppendUint64(nil, uint64(i)) + gas, err := core.IntrinsicGas(data, nil, false, true, true, true) + require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, true, true, true)", data) + + txs[i] = types.NewTx(&types.LegacyTx{ + To: &to, + Data: data, + Gas: gas + extraGas, + }) + } + + block := types.NewBlock(&types.Header{}, txs, nil, nil, trie.NewStackTrie(nil)) + require.NoError(t, p.StartBlock(block), "BeforeBlock()") + defer p.FinishBlock(block) + + for i, tx := range txs { + wantOK := wantProcessed[i] + + var want [sha256.Size]byte + if wantOK { + want = handler.Process(i, tx) + } + + got, gotOK := p.Result(i) + if got != want || gotOK != wantOK { + t.Errorf("Result(%d) got (%#x, %t); want (%#x, %t)", i, got, gotOK, want, wantOK) + } + } + }) + + if t.Failed() { + break + } + } +} From 3669967032de1e647ac45d7a9388f15d6e568450 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 15 Sep 2025 13:16:58 +0100 Subject: [PATCH 02/18] chore: copyright headers + package comment --- libevm/precompiles/parallel/parallel.go | 18 ++++++++++++++++++ libevm/precompiles/parallel/parallel_test.go | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index 026b488ef30..d8ab335afde 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -1,3 +1,21 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +// Package parallel provides functionality for precompiled contracts that can +// pre-process their results in an embarrassingly parallel fashion. package parallel import ( diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index 9f71d3c7d8b..a1b1e949942 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -1,3 +1,19 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + package parallel import ( From 9780de0edd00065814ed35c6ec212747b36d8f30 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 15 Sep 2025 13:40:22 +0100 Subject: [PATCH 03/18] chore: placate the linter --- libevm/precompiles/parallel/parallel_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index a1b1e949942..c50bfb666ed 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -22,12 +22,13 @@ import ( "math/rand/v2" "testing" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/trie" - "github.com/stretchr/testify/require" - "go.uber.org/goleak" ) func TestMain(m *testing.M) { @@ -94,7 +95,7 @@ func TestProcessor(t *testing.T) { }, } - rng := rand.New(rand.NewPCG(0, 0)) + rng := rand.New(rand.NewPCG(0, 0)) //nolint:gosec // Reproducibility is useful for testing for range 100 { params = append(params, blockParams{ numTxs: rng.IntN(1000), From 686cd69fedca0a822379aeaf95f9012b0fb4aaae Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 15 Sep 2025 17:57:26 +0100 Subject: [PATCH 04/18] feat: integration of `parallel.Processor` with `EVM.Call` --- core/vm/evm.go | 2 +- core/vm/evm.libevm.go | 24 +++ core/vm/evm.libevm_test.go | 3 + core/vm/hooks.libevm.go | 9 ++ core/vm/interface.go | 2 + core/vm/interface.libevm.go | 27 ++++ libevm/precompiles/parallel/parallel.go | 52 ++++-- libevm/precompiles/parallel/parallel_test.go | 160 +++++++++++++++++++ 8 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 core/vm/interface.libevm.go diff --git a/core/vm/evm.go b/core/vm/evm.go index b9fd682b9a7..618dd5ec60d 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -187,7 +187,7 @@ func (evm *EVM) Interpreter() *EVMInterpreter { // parameters. It also handles any necessary value transfer required and takes // the necessary steps to create accounts and reverses the state in case of an // execution error or failed value transfer. -func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { +func (evm *EVM) call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth diff --git a/core/vm/evm.libevm.go b/core/vm/evm.libevm.go index c2c807c1378..d9a9ada19aa 100644 --- a/core/vm/evm.libevm.go +++ b/core/vm/evm.libevm.go @@ -20,6 +20,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/log" + "github.com/holiman/uint256" ) // canCreateContract is a convenience wrapper for calling the @@ -52,6 +53,29 @@ func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Ad return gas, err } +// Call executes the contract associated with the addr with the given input as +// parameters. It also handles any necessary value transfer required and takes +// the necessary steps to create accounts and reverses the state in case of an +// execution error or failed value transfer. +func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { + gas, err = evm.spendPreprocessingGas(gas) + if err != nil { + return nil, gas, err + } + return evm.call(caller, addr, input, gas, value) +} + +func (evm *EVM) spendPreprocessingGas(gas uint64) (uint64, error) { + if evm.depth > 0 || !libevmHooks.Registered() { + return gas, nil + } + c := libevmHooks.Get().PreprocessingGasCharge(evm.StateDB.TxHash()) + if c > gas { + return 0, ErrOutOfGas + } + return gas - c, nil +} + // InvalidateExecution sets the error that will be returned by // [EVM.ExecutionInvalidated] for the length of the current transaction; i.e. // until [EVM.Reset] is called. This is honoured by state-transition logic to diff --git a/core/vm/evm.libevm_test.go b/core/vm/evm.libevm_test.go index c0a33718e3d..6e3fa290fe9 100644 --- a/core/vm/evm.libevm_test.go +++ b/core/vm/evm.libevm_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/params" ) @@ -46,6 +47,8 @@ func (o *evmArgOverrider) OverrideEVMResetArgs(r params.Rules, _ *EVMResetArgs) } } +func (o *evmArgOverrider) PreprocessingGasCharge(common.Hash) uint64 { return 0 } + func (o *evmArgOverrider) register(t *testing.T) { t.Helper() TestOnlyClearRegisteredHooks() diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go index 1e5acd49db9..c3cfd1bd981 100644 --- a/core/vm/hooks.libevm.go +++ b/core/vm/hooks.libevm.go @@ -17,6 +17,7 @@ package vm import ( + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/libevm/register" "github.com/ava-labs/libevm/params" ) @@ -40,6 +41,14 @@ var libevmHooks register.AtMostOnce[Hooks] type Hooks interface { OverrideNewEVMArgs(*NewEVMArgs) *NewEVMArgs OverrideEVMResetArgs(params.Rules, *EVMResetArgs) *EVMResetArgs + Preprocessor +} + +// A Preprocessor performs computation on a transaction before the +// [EVMInterpreter] is invoked and reports its gas charge for spending at the +// beginning of [EVM.Call]. +type Preprocessor interface { + PreprocessingGasCharge(tx common.Hash) uint64 } // NewEVMArgs are the arguments received by [NewEVM], available for override diff --git a/core/vm/interface.go b/core/vm/interface.go index 4a9e15a6d3c..25ef393e863 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -82,6 +82,8 @@ type StateDB interface { AddLog(*types.Log) AddPreimage(common.Hash, []byte) + + StateDBRemainder } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/core/vm/interface.libevm.go b/core/vm/interface.libevm.go new file mode 100644 index 00000000000..ee999fcc8c5 --- /dev/null +++ b/core/vm/interface.libevm.go @@ -0,0 +1,27 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package vm + +import "github.com/ava-labs/libevm/common" + +// StateDBRemainder defines methods not included in the geth definition of +// [StateDB] but present on the concrete type and exposed for libevm +// functionality. +type StateDBRemainder interface { + TxHash() common.Hash + TxIndex() int +} diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index d8ab335afde..5e70f5f9851 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -22,8 +22,10 @@ import ( "fmt" "sync" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/core/vm" ) // A Handler is responsible for processing [types.Transactions] in an @@ -47,7 +49,8 @@ type Processor[R any] struct { handler Handler[R] workers sync.WaitGroup work chan *job - results [](chan *R) + results [](chan result[R]) + txGas map[common.Hash]uint64 } type job struct { @@ -55,6 +58,11 @@ type job struct { tx *types.Transaction } +type result[T any] struct { + tx common.Hash + val *T +} + // New constructs a new [Processor] with the specified number of concurrent // workers. [Processor.Close] must be called after the final call to // [Processor.FinishBlock] to avoid leaking goroutines. @@ -62,6 +70,7 @@ func New[R any](h Handler[R], workers int) *Processor[R] { p := &Processor[R]{ handler: h, work: make(chan *job), + txGas: make(map[common.Hash]uint64), } workers = max(workers, 1) @@ -81,7 +90,10 @@ func (p *Processor[R]) worker() { } r := p.handler.Process(w.index, w.tx) - p.results[w.index] <- &r + p.results[w.index] <- result[R]{ + tx: w.tx.Hash(), + val: &r, + } } } @@ -101,7 +113,7 @@ func (p *Processor[R]) StartBlock(b *types.Block) error { // We can reuse the channels already in the results slice because they're // emptied by [Processor.FinishBlock]. for i, n := len(p.results), len(txs); i < n; i++ { - p.results = append(p.results, make(chan *R, 1)) + p.results = append(p.results, make(chan result[R], 1)) } for i, tx := range txs { @@ -116,7 +128,10 @@ func (p *Processor[R]) StartBlock(b *types.Block) error { }) default: - p.results[i] <- nil + p.results[i] <- result[R]{ + tx: tx.Hash(), + val: nil, + } } } @@ -138,7 +153,7 @@ func (p *Processor[R]) FinishBlock(b *types.Block) { // Every result channel is guaranteed to have some value in its buffer // because [Processor.BeforeBlock] either sends a nil *R or it // dispatches a job that will send a non-nil *R. - <-p.results[i] + delete(p.txGas, (<-p.results[i]).tx) } } @@ -147,27 +162,38 @@ func (p *Processor[R]) FinishBlock(b *types.Block) { // [Handler]. The returned boolean will be false if no processing occurred, // either because the [Handler] indicated as such or because the transaction // supplied insufficient gas. +// +// Multiple calls to Result with the same argument are allowed. Callers MUST NOT +// charge the gas price for preprocessing as this is handled by +// [Processor.PreprocessingGasCharge] if registered as a [vm.Preprocessor]. +// The same value will be returned by each call with the same argument, such +// that if R is a pointer then modifications will persist between calls. func (p *Processor[R]) Result(i int) (R, bool) { ch := p.results[i] - r := <-ch + r := (<-ch) defer func() { ch <- r }() - if r == nil { + if r.val == nil { // TODO(arr4n) if we're here then the implementoor might have a bug in // their [Handler], so logging a warning is probably a good idea. var zero R return zero, false } - return *r, true + return *r.val, true } -func (p *Processor[R]) shouldProcess(tx *types.Transaction) (bool, error) { +func (p *Processor[R]) shouldProcess(tx *types.Transaction) (ok bool, err error) { cost, ok := p.handler.Gas(tx) if !ok { return false, nil } + defer func() { + if ok && err == nil { + p.txGas[tx.Hash()] = cost + } + }() spent, err := core.IntrinsicGas( tx.Data(), @@ -186,3 +212,11 @@ func (p *Processor[R]) shouldProcess(tx *types.Transaction) (bool, error) { left := tx.Gas() - spent return left >= cost, nil } + +var _ vm.Preprocessor = (*Processor[struct{}])(nil) + +// PreprocessingGasCharge implements the [vm.Preprocessor] interface and MUST be +// registered via [vm.RegisterHooks] to ensure proper gas accounting. +func (p *Processor[R]) PreprocessingGasCharge(tx common.Hash) uint64 { + return p.txGas[tx] +} diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index c50bfb666ed..e535cb1b798 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -19,15 +19,26 @@ package parallel import ( "crypto/sha256" "encoding/binary" + "math" "math/rand/v2" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/holiman/uint256" "github.com/stretchr/testify/require" "go.uber.org/goleak" "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/libevm/core/vm" + "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/libevm" + "github.com/ava-labs/libevm/libevm/ethtest" + "github.com/ava-labs/libevm/libevm/hookstest" + "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/trie" ) @@ -163,3 +174,152 @@ func TestProcessor(t *testing.T) { } } } + +type noopHooks struct{} + +func (noopHooks) OverrideNewEVMArgs(a *vm.NewEVMArgs) *vm.NewEVMArgs { + return a +} + +func (noopHooks) OverrideEVMResetArgs(_ params.Rules, a *vm.EVMResetArgs) *vm.EVMResetArgs { + return a +} + +type vmHooks struct { + vm.Preprocessor // the [Processor] + noopHooks +} + +func TestIntegration(t *testing.T) { + const handlerGas = 500 + handler := &shaHandler{ + addr: common.Address{'s', 'h', 'a', 2, 5, 6}, + gas: handlerGas, + } + sut := New(handler, 8) + t.Cleanup(sut.Close) + + vm.RegisterHooks(vmHooks{Preprocessor: sut}) + t.Cleanup(vm.TestOnlyClearRegisteredHooks) + + stub := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + handler.addr: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { + sdb := env.StateDB() + txi, txh := sdb.TxIndex(), sdb.TxHash() + + // Precompiles MUST NOT charge gas for the preprocessing as it + // would then be double-counted. + got, ok := sut.Result(txi) + if !ok { + t.Errorf("no result for tx[%d] %v", txi, txh) + } + env.StateDB().AddLog(&types.Log{ + Data: got[:], + }) + return nil, nil + }), + }, + } + stub.Register(t) + + state, evm := ethtest.NewZeroEVM(t) + + key, err := crypto.GenerateKey() + require.NoErrorf(t, err, "crypto.GenerateKey()") + eoa := crypto.PubkeyToAddress(key.PublicKey) + state.CreateAccount(eoa) + state.AddBalance(eoa, uint256.NewInt(10*params.Ether)) + + var ( + txs types.Transactions + want []*types.Receipt + ) + ignore := cmp.Options{ + cmpopts.IgnoreFields( + types.Receipt{}, + "PostState", "CumulativeGasUsed", "BlockNumber", "BlockHash", "Bloom", + ), + cmpopts.IgnoreFields(types.Log{}, "BlockHash"), + } + + signer := types.LatestSigner(evm.ChainConfig()) + for i, addr := range []common.Address{ + {'o', 't', 'h', 'e', 'r'}, + handler.addr, + } { + ui := uint(i) //nolint:gosec // Known value that won't overflow + data := []byte("hello, world") + + // Having all arguments `false` is equivalent to what + // [core.ApplyTransaction] will do. + gas, err := core.IntrinsicGas(data, types.AccessList{}, false, false, false, false) + require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, false, false, false)", data) + if addr == handler.addr { + gas += handlerGas + } + + tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ + Nonce: uint64(ui), + To: &addr, + Data: data, + Gas: gas, + }) + txs = append(txs, tx) + + wantR := &types.Receipt{ + Status: types.ReceiptStatusSuccessful, + TxHash: tx.Hash(), + GasUsed: gas, + TransactionIndex: ui, + } + if addr == handler.addr { + res := handler.Process(i, tx) + wantR.Logs = []*types.Log{{ + TxHash: tx.Hash(), + TxIndex: ui, + Data: res[:], + }} + } + want = append(want, wantR) + } + + block := types.NewBlock(&types.Header{}, txs, nil, nil, trie.NewStackTrie(nil)) + require.NoError(t, sut.StartBlock(block), "StartBlock()") + defer sut.FinishBlock(block) + + pool := core.GasPool(math.MaxUint64) + var got []*types.Receipt + for i, tx := range txs { + state.SetTxContext(tx.Hash(), i) + + var usedGas uint64 + receipt, err := core.ApplyTransaction( + evm.ChainConfig(), + chainContext{}, + &block.Header().Coinbase, + &pool, + state, + block.Header(), + tx, + &usedGas, + vm.Config{}, + ) + require.NoError(t, err, "ApplyTransaction([%d])", i) + got = append(got, receipt) + } + + if diff := cmp.Diff(want, got, ignore); diff != "" { + t.Errorf("%T diff (-want +got):\n%s", got, diff) + } +} + +// Dummy implementations of interfaces required by [core.ApplyTransaction]. +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) { return common.Address{}, nil } From 81c80a8f7e6d20678be19cf90b989af03148ad9b Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 15 Sep 2025 18:08:02 +0100 Subject: [PATCH 05/18] feat: integration of `parallel.Processor` with `EVM.Create` --- core/vm/evm.go | 4 ++-- core/vm/evm.libevm.go | 13 ++++++++++++- core/vm/hooks.libevm.go | 2 +- libevm/precompiles/parallel/parallel_test.go | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 618dd5ec60d..b9eca8cfb7f 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -433,8 +433,8 @@ func (c *codeAndHash) Hash() common.Hash { return c.hash } -// create creates a new contract using code as deployment code. -func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { +// createCommon creates a new contract using code as deployment code. +func (evm *EVM) createCommon(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { // Depth check execution. Fail if we're trying to execute above the // limit. if evm.depth > int(params.CallCreateDepth) { diff --git a/core/vm/evm.libevm.go b/core/vm/evm.libevm.go index d9a9ada19aa..0bda196c869 100644 --- a/core/vm/evm.libevm.go +++ b/core/vm/evm.libevm.go @@ -17,10 +17,11 @@ package vm import ( + "github.com/holiman/uint256" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/log" - "github.com/holiman/uint256" ) // canCreateContract is a convenience wrapper for calling the @@ -65,6 +66,16 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas return evm.call(caller, addr, input, gas, value) } +// create wraps the original geth method of the same name, now name +// [EVM.createCommon], first spending preprocessing gas. +func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *uint256.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { + gas, err := evm.spendPreprocessingGas(gas) + if err != nil { + return nil, common.Address{}, gas, err + } + return evm.createCommon(caller, codeAndHash, gas, value, address, typ) +} + func (evm *EVM) spendPreprocessingGas(gas uint64) (uint64, error) { if evm.depth > 0 || !libevmHooks.Registered() { return gas, nil diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go index c3cfd1bd981..fbd9be090b2 100644 --- a/core/vm/hooks.libevm.go +++ b/core/vm/hooks.libevm.go @@ -46,7 +46,7 @@ type Hooks interface { // A Preprocessor performs computation on a transaction before the // [EVMInterpreter] is invoked and reports its gas charge for spending at the -// beginning of [EVM.Call]. +// beginning of [EVM.Call] or [EVM.Create]. type Preprocessor interface { PreprocessingGasCharge(tx common.Hash) uint64 } diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index e535cb1b798..094163db9b9 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -248,7 +248,7 @@ func TestIntegration(t *testing.T) { {'o', 't', 'h', 'e', 'r'}, handler.addr, } { - ui := uint(i) //nolint:gosec // Known value that won't overflow + ui := uint(i) data := []byte("hello, world") // Having all arguments `false` is equivalent to what From 04c4cbcb69f494e887cb9e4d089d8fb52e713c2c Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 15 Sep 2025 19:09:40 +0100 Subject: [PATCH 06/18] fix: use `params.Rules` for `core.IntrinsicGas()` args --- libevm/precompiles/parallel/parallel.go | 13 ++++++----- libevm/precompiles/parallel/parallel_test.go | 24 +++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index 5e70f5f9851..761d51d9b1a 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -26,6 +26,7 @@ import ( "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/params" ) // A Handler is responsible for processing [types.Transactions] in an @@ -106,7 +107,7 @@ func (p *Processor[R]) Close() { // StartBlock dispatches transactions to the [Handler] and returns immediately. // It MUST be paired with a call to [Processor.FinishBlock], without overlap of // blocks. -func (p *Processor[R]) StartBlock(b *types.Block) error { +func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules) error { txs := b.Transactions() jobs := make([]*job, 0, len(txs)) @@ -117,7 +118,7 @@ func (p *Processor[R]) StartBlock(b *types.Block) error { } for i, tx := range txs { - switch do, err := p.shouldProcess(tx); { + switch do, err := p.shouldProcess(tx, rules); { case err != nil: return err @@ -184,7 +185,7 @@ func (p *Processor[R]) Result(i int) (R, bool) { return *r.val, true } -func (p *Processor[R]) shouldProcess(tx *types.Transaction) (ok bool, err error) { +func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) (ok bool, err error) { cost, ok := p.handler.Gas(tx) if !ok { return false, nil @@ -199,9 +200,9 @@ func (p *Processor[R]) shouldProcess(tx *types.Transaction) (ok bool, err error) tx.Data(), tx.AccessList(), tx.To() == nil, - true, // Homestead - true, // EIP-2028 (Istanbul) - true, // EIP-3860 (Shanghai) + rules.IsHomestead, + rules.IsIstanbul, // EIP-2028 + rules.IsShanghai, // EIP-3860 ) if err != nil { return false, fmt.Errorf("calculating intrinsic gas of %v: %v", tx.Hash(), err) diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index 094163db9b9..4c24a73939a 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -77,7 +77,7 @@ func TestProcessor(t *testing.T) { // Each set of params is effectively a test case, but they are all run on // the same [Processor]. - params := []blockParams{ + tests := []blockParams{ { numTxs: 0, }, @@ -108,19 +108,20 @@ func TestProcessor(t *testing.T) { rng := rand.New(rand.NewPCG(0, 0)) //nolint:gosec // Reproducibility is useful for testing for range 100 { - params = append(params, blockParams{ + tests = append(tests, blockParams{ numTxs: rng.IntN(1000), sendToAddrEvery: 1 + rng.IntN(30), sufficientGasEvery: 1 + rng.IntN(30), }) } - for _, tc := range params { + for _, tt := range tests { t.Run("", func(t *testing.T) { - t.Logf("%+v", tc) + t.Logf("%+v", tt) - txs := make(types.Transactions, tc.numTxs) - wantProcessed := make([]bool, tc.numTxs) + var rules params.Rules + txs := make(types.Transactions, tt.numTxs) + wantProcessed := make([]bool, tt.numTxs) for i := range len(txs) { var ( to common.Address @@ -128,19 +129,19 @@ func TestProcessor(t *testing.T) { ) wantProcessed[i] = true - if i%tc.sendToAddrEvery == 0 { + if i%tt.sendToAddrEvery == 0 { to = handler.addr } else { wantProcessed[i] = false } - if i%tc.sufficientGasEvery == 0 { + if i%tt.sufficientGasEvery == 0 { extraGas = handler.gas } else { wantProcessed[i] = false } data := binary.BigEndian.AppendUint64(nil, uint64(i)) - gas, err := core.IntrinsicGas(data, nil, false, true, true, true) + gas, err := core.IntrinsicGas(data, nil, false, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, true, true, true)", data) txs[i] = types.NewTx(&types.LegacyTx{ @@ -151,7 +152,7 @@ func TestProcessor(t *testing.T) { } block := types.NewBlock(&types.Header{}, txs, nil, nil, trie.NewStackTrie(nil)) - require.NoError(t, p.StartBlock(block), "BeforeBlock()") + require.NoError(t, p.StartBlock(block, rules), "StartBlock()") defer p.FinishBlock(block) for i, tx := range txs { @@ -285,7 +286,8 @@ func TestIntegration(t *testing.T) { } block := types.NewBlock(&types.Header{}, txs, nil, nil, trie.NewStackTrie(nil)) - require.NoError(t, sut.StartBlock(block), "StartBlock()") + rules := evm.ChainConfig().Rules(block.Number(), true, block.Time()) + require.NoError(t, sut.StartBlock(block, rules), "StartBlock()") defer sut.FinishBlock(block) pool := core.GasPool(math.MaxUint64) From e26b0246815c7cc4ed99ae20a6352a92faa748c8 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 16 Sep 2025 03:54:38 +0100 Subject: [PATCH 07/18] feat: `Processor.PreprocessingGasCharge` errors on unknown tx --- core/vm/evm.libevm.go | 5 ++++- core/vm/evm.libevm_test.go | 4 +++- core/vm/hooks.libevm.go | 2 +- libevm/precompiles/parallel/parallel.go | 29 +++++++++++++++++++------ 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/core/vm/evm.libevm.go b/core/vm/evm.libevm.go index 0bda196c869..0e902bec427 100644 --- a/core/vm/evm.libevm.go +++ b/core/vm/evm.libevm.go @@ -80,7 +80,10 @@ func (evm *EVM) spendPreprocessingGas(gas uint64) (uint64, error) { if evm.depth > 0 || !libevmHooks.Registered() { return gas, nil } - c := libevmHooks.Get().PreprocessingGasCharge(evm.StateDB.TxHash()) + c, err := libevmHooks.Get().PreprocessingGasCharge(evm.StateDB.TxHash()) + if err != nil { + return gas, err + } if c > gas { return 0, ErrOutOfGas } diff --git a/core/vm/evm.libevm_test.go b/core/vm/evm.libevm_test.go index 6e3fa290fe9..deb7a0c6770 100644 --- a/core/vm/evm.libevm_test.go +++ b/core/vm/evm.libevm_test.go @@ -47,7 +47,9 @@ func (o *evmArgOverrider) OverrideEVMResetArgs(r params.Rules, _ *EVMResetArgs) } } -func (o *evmArgOverrider) PreprocessingGasCharge(common.Hash) uint64 { return 0 } +func (o *evmArgOverrider) PreprocessingGasCharge(common.Hash) (uint64, error) { + return 0, nil +} func (o *evmArgOverrider) register(t *testing.T) { t.Helper() diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go index fbd9be090b2..cef960dfe28 100644 --- a/core/vm/hooks.libevm.go +++ b/core/vm/hooks.libevm.go @@ -48,7 +48,7 @@ type Hooks interface { // [EVMInterpreter] is invoked and reports its gas charge for spending at the // beginning of [EVM.Call] or [EVM.Create]. type Preprocessor interface { - PreprocessingGasCharge(tx common.Hash) uint64 + PreprocessingGasCharge(tx common.Hash) (uint64, error) } // NewEVMArgs are the arguments received by [NewEVM], available for override diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index 761d51d9b1a..f12f7362044 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -19,6 +19,7 @@ package parallel import ( + "errors" "fmt" "sync" @@ -154,7 +155,8 @@ func (p *Processor[R]) FinishBlock(b *types.Block) { // Every result channel is guaranteed to have some value in its buffer // because [Processor.BeforeBlock] either sends a nil *R or it // dispatches a job that will send a non-nil *R. - delete(p.txGas, (<-p.results[i]).tx) + tx := (<-p.results[i]).tx + delete(p.txGas, tx) } } @@ -171,7 +173,7 @@ func (p *Processor[R]) FinishBlock(b *types.Block) { // that if R is a pointer then modifications will persist between calls. func (p *Processor[R]) Result(i int) (R, bool) { ch := p.results[i] - r := (<-ch) + r := <-ch defer func() { ch <- r }() @@ -185,13 +187,17 @@ func (p *Processor[R]) Result(i int) (R, bool) { return *r.val, true } -func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) (ok bool, err error) { +func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) (process bool, err error) { + // An explicit 0 is necessary to avoid [Processor.PreprocessingGasCharge] + // returning [ErrTxUnknown]. + p.txGas[tx.Hash()] = 0 + cost, ok := p.handler.Gas(tx) if !ok { return false, nil } defer func() { - if ok && err == nil { + if process && err == nil { p.txGas[tx.Hash()] = cost } }() @@ -214,10 +220,19 @@ func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) return left >= cost, nil } -var _ vm.Preprocessor = (*Processor[struct{}])(nil) +// ErrTxUnknown is returned by [Processor.PreprocessingGasCharge] if it is +// called with a transaction hash that wasn't in the last block passed to +// [Processor.StartBlock]. +var ErrTxUnknown = errors.New("transaction unknown by parallel preprocessor") // PreprocessingGasCharge implements the [vm.Preprocessor] interface and MUST be // registered via [vm.RegisterHooks] to ensure proper gas accounting. -func (p *Processor[R]) PreprocessingGasCharge(tx common.Hash) uint64 { - return p.txGas[tx] +func (p *Processor[R]) PreprocessingGasCharge(tx common.Hash) (uint64, error) { + g, ok := p.txGas[tx] + if !ok { + return 0, fmt.Errorf("%w: %v", ErrTxUnknown, tx) + } + return g, nil } + +var _ vm.Preprocessor = (*Processor[struct{}])(nil) From 53f6df3ab71074e3444cb8167aa65d43d729f83c Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 23 Sep 2025 09:53:53 +0100 Subject: [PATCH 08/18] feat: `Handler.BeforeBlock()` --- libevm/precompiles/parallel/parallel.go | 27 +++++++--- libevm/precompiles/parallel/parallel_test.go | 53 +++++++++++++------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index f12f7362044..3f0e3f29a9d 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -42,6 +42,7 @@ import ( // Scenario (2) allows precompile access to be determined through inspection of // the [types.Transaction] alone, without the need for execution. type Handler[Result any] interface { + BeforeBlock(*types.Header) Gas(*types.Transaction) (gas uint64, process bool) Process(index int, tx *types.Transaction) Result } @@ -109,6 +110,7 @@ func (p *Processor[R]) Close() { // It MUST be paired with a call to [Processor.FinishBlock], without overlap of // blocks. func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules) error { + p.handler.BeforeBlock(types.CopyHeader(b.Header())) txs := b.Transactions() jobs := make([]*job, 0, len(txs)) @@ -202,14 +204,7 @@ func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) } }() - spent, err := core.IntrinsicGas( - tx.Data(), - tx.AccessList(), - tx.To() == nil, - rules.IsHomestead, - rules.IsIstanbul, // EIP-2028 - rules.IsShanghai, // EIP-3860 - ) + spent, err := txIntrinsicGas(tx, &rules) if err != nil { return false, fmt.Errorf("calculating intrinsic gas of %v: %v", tx.Hash(), err) } @@ -220,6 +215,22 @@ func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) return left >= cost, nil } +func txIntrinsicGas(tx *types.Transaction, rules *params.Rules) (uint64, error) { + return intrinsicGas(tx.Data(), tx.AccessList(), tx.To(), rules) +} + +func intrinsicGas(data []byte, access types.AccessList, txTo *common.Address, rules *params.Rules) (uint64, error) { + create := txTo == nil + return core.IntrinsicGas( + data, + access, + create, + rules.IsHomestead, + rules.IsIstanbul, // EIP-2028 + rules.IsShanghai, // EIP-3860 + ) +} + // ErrTxUnknown is returned by [Processor.PreprocessingGasCharge] if it is // called with a transaction hash that wasn't in the last block passed to // [Processor.StartBlock]. diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index 4c24a73939a..f12c9bfcfd3 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -17,10 +17,12 @@ package parallel import ( - "crypto/sha256" + "bytes" "encoding/binary" "math" + "math/big" "math/rand/v2" + "slices" "testing" "github.com/google/go-cmp/cmp" @@ -46,25 +48,36 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m, goleak.IgnoreCurrent()) } -type shaHandler struct { - addr common.Address - gas uint64 +type reverser struct { + extra []byte + addr common.Address + gas uint64 } -func (h *shaHandler) Gas(tx *types.Transaction) (uint64, bool) { - if to := tx.To(); to == nil || *to != h.addr { +func (r *reverser) BeforeBlock(h *types.Header) { + r.extra = h.Extra +} + +func (r *reverser) Gas(tx *types.Transaction) (uint64, bool) { + if to := tx.To(); to == nil || *to != r.addr { return 0, false } - return h.gas, true + return r.gas, true +} + +func reverserOutput(data, extra []byte) []byte { + out := append(data, extra...) + slices.Reverse(out) + return out } -func (*shaHandler) Process(i int, tx *types.Transaction) [sha256.Size]byte { - return sha256.Sum256(tx.Data()) +func (r *reverser) Process(i int, tx *types.Transaction) []byte { + return reverserOutput(tx.Data(), r.extra) } func TestProcessor(t *testing.T) { - handler := &shaHandler{ - addr: common.Address{'s', 'h', 'a', 2, 5, 6}, + handler := &reverser{ + addr: common.Address{'r', 'e', 'v', 'e', 'r', 's', 'e'}, gas: 1e6, } p := New(handler, 8) @@ -141,7 +154,7 @@ func TestProcessor(t *testing.T) { } data := binary.BigEndian.AppendUint64(nil, uint64(i)) - gas, err := core.IntrinsicGas(data, nil, false, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + gas, err := intrinsicGas(data, types.AccessList{}, &handler.addr, &rules) require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, true, true, true)", data) txs[i] = types.NewTx(&types.LegacyTx{ @@ -151,20 +164,21 @@ func TestProcessor(t *testing.T) { }) } - block := types.NewBlock(&types.Header{}, txs, nil, nil, trie.NewStackTrie(nil)) + extra := []byte("extra") + block := types.NewBlock(&types.Header{Extra: extra}, txs, nil, nil, trie.NewStackTrie(nil)) require.NoError(t, p.StartBlock(block, rules), "StartBlock()") defer p.FinishBlock(block) for i, tx := range txs { wantOK := wantProcessed[i] - var want [sha256.Size]byte + var want []byte if wantOK { - want = handler.Process(i, tx) + want = reverserOutput(tx.Data(), extra) } got, gotOK := p.Result(i) - if got != want || gotOK != wantOK { + if !bytes.Equal(got, want) || gotOK != wantOK { t.Errorf("Result(%d) got (%#x, %t); want (%#x, %t)", i, got, gotOK, want, wantOK) } } @@ -193,8 +207,8 @@ type vmHooks struct { func TestIntegration(t *testing.T) { const handlerGas = 500 - handler := &shaHandler{ - addr: common.Address{'s', 'h', 'a', 2, 5, 6}, + handler := &reverser{ + addr: common.Address{'r', 'e', 'v', 'e', 'r', 's', 'e'}, gas: handlerGas, } sut := New(handler, 8) @@ -254,7 +268,8 @@ func TestIntegration(t *testing.T) { // Having all arguments `false` is equivalent to what // [core.ApplyTransaction] will do. - gas, err := core.IntrinsicGas(data, types.AccessList{}, false, false, false, false) + rules := evm.ChainConfig().Rules(big.NewInt(0), false, 0) + gas, err := intrinsicGas(data, types.AccessList{}, &addr, &rules) require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, false, false, false)", data) if addr == handler.addr { gas += handlerGas From 9788f8b9f7c4a1b1f7f158d2951997fcb10ca312 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 23 Sep 2025 12:23:36 +0100 Subject: [PATCH 09/18] test(vm): preprocessing gas charges --- core/vm/hooks.libevm.go | 16 ++ core/vm/preprocess.libevm_test.go | 180 +++++++++++++++++++ libevm/ethtest/dummy.go | 44 +++++ libevm/precompiles/parallel/parallel_test.go | 31 +--- 4 files changed, 247 insertions(+), 24 deletions(-) create mode 100644 core/vm/preprocess.libevm_test.go create mode 100644 libevm/ethtest/dummy.go diff --git a/core/vm/hooks.libevm.go b/core/vm/hooks.libevm.go index cef960dfe28..a0ef69ba811 100644 --- a/core/vm/hooks.libevm.go +++ b/core/vm/hooks.libevm.go @@ -89,3 +89,19 @@ func (evm *EVM) overrideEVMResetArgs(txCtx TxContext, statedb StateDB) (TxContex args := libevmHooks.Get().OverrideEVMResetArgs(evm.chainRules, &EVMResetArgs{txCtx, statedb}) return args.TxContext, args.StateDB } + +// NOOPHooks implements [Hooks] such that every method is a noop. +type NOOPHooks struct{} + +var _ Hooks = NOOPHooks{} + +// OverrideNewEVMArgs returns the args unchanged. +func (NOOPHooks) OverrideNewEVMArgs(a *NewEVMArgs) *NewEVMArgs { return a } + +// OverrideEVMResetArgs returns the args unchanged. +func (NOOPHooks) OverrideEVMResetArgs(_ params.Rules, a *EVMResetArgs) *EVMResetArgs { + return a +} + +// PreprocessingGasCharge returns (0, nil). +func (NOOPHooks) PreprocessingGasCharge(common.Hash) (uint64, error) { return 0, nil } diff --git a/core/vm/preprocess.libevm_test.go b/core/vm/preprocess.libevm_test.go new file mode 100644 index 00000000000..ee9463f641a --- /dev/null +++ b/core/vm/preprocess.libevm_test.go @@ -0,0 +1,180 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package vm_test + +import ( + "fmt" + "math" + "math/big" + "testing" + + "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/libevm/ethtest" + "github.com/ava-labs/libevm/params" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type preprocessingCharger struct { + vm.NOOPHooks + charge map[common.Hash]uint64 +} + +func (p preprocessingCharger) PreprocessingGasCharge(tx common.Hash) (uint64, error) { + c, ok := p.charge[tx] + if !ok { + return 0, fmt.Errorf("unknown tx %v", tx) + } + return c, nil +} + +func TestChargePreprocessingGas(t *testing.T) { + tests := []struct { + name string + to *common.Address + charge uint64 + txGas uint64 + wantVMErr error + wantGasUsed uint64 + }{ + { + name: "standard create", + to: nil, + txGas: params.TxGas + params.CreateGas, + wantGasUsed: params.TxGas + params.CreateGas, + }, + { + name: "create with extra charge", + to: nil, + charge: 1234, + txGas: params.TxGas + params.CreateGas + 2000, + wantGasUsed: params.TxGas + params.CreateGas + 1234, + }, + { + name: "standard call", + to: &common.Address{}, + txGas: params.TxGas, + wantGasUsed: params.TxGas, + }, + { + name: "out of gas", + to: &common.Address{}, + charge: 1000, + txGas: params.TxGas + 999, + wantGasUsed: params.TxGas + 999, + wantVMErr: vm.ErrOutOfGas, + }, + { + name: "call with extra charge", + to: &common.Address{}, + charge: 13579, + txGas: params.TxGas + 20000, + wantGasUsed: params.TxGas + 13579, + }, + } + + config := params.AllDevChainProtocolChanges + key, err := crypto.GenerateKey() + require.NoError(t, err, "crypto.GenerateKey()") + eoa := crypto.PubkeyToAddress(key.PublicKey) + + header := &types.Header{ + Number: big.NewInt(0), + Difficulty: big.NewInt(0), + BaseFee: big.NewInt(0), + } + signer := types.MakeSigner(config, header.Number, header.Time) + + var txs types.Transactions + charge := make(map[common.Hash]uint64) + for _, tt := range tests { + tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ + To: tt.to, + GasPrice: big.NewInt(1), + Gas: tt.txGas, + }) + txs = append(txs, tx) + charge[tx.Hash()] = tt.charge + } + + vm.RegisterHooks(&preprocessingCharger{ + charge: charge, + }) + t.Cleanup(vm.TestOnlyClearRegisteredHooks) + + for i, tt := range tests { + tx := txs[i] + + t.Run(tt.name, func(t *testing.T) { + t.Logf("Extra gas charge: %d", tt.charge) + + t.Run("ApplyTransaction", func(t *testing.T) { + sdb, err := state.New( + types.EmptyRootHash, + state.NewDatabase(rawdb.NewMemoryDatabase()), + nil, + ) + require.NoError(t, err, "state.New(types.EmptyRootHash, [memory db], nil)") + sdb.SetTxContext(tx.Hash(), i) + sdb.SetBalance(eoa, new(uint256.Int).SetAllOne()) + + var gotGasUsed uint64 + gp := core.GasPool(math.MaxUint64) + + receipt, err := core.ApplyTransaction( + config, ethtest.DummyChainContext(), &common.Address{}, + &gp, sdb, header, tx, &gotGasUsed, vm.Config{}, + ) + require.NoError(t, err, "core.ApplyTransaction(...)") + + wantStatus := types.ReceiptStatusSuccessful + if tt.wantVMErr != nil { + wantStatus = types.ReceiptStatusFailed + } + assert.Equalf(t, wantStatus, receipt.Status, "%T.Status", receipt) + + if got, want := gotGasUsed, tt.wantGasUsed; got != want { + t.Errorf("core.ApplyTransaction(..., &gotGasUsed, ...) got %d; want %d", got, want) + } + if got, want := receipt.GasUsed, tt.wantGasUsed; got != want { + t.Errorf("core.ApplyTransaction(...) -> %T.GasUsed = %d; want %d", receipt, got, want) + } + }) + + t.Run("VM_error", func(t *testing.T) { + sdb, evm := ethtest.NewZeroEVM(t, ethtest.WithChainConfig(config)) + sdb.SetBalance(eoa, new(uint256.Int).SetAllOne()) + sdb.SetTxContext(tx.Hash(), i) + + msg, err := core.TransactionToMessage(tx, signer, header.BaseFee) + require.NoError(t, err, "core.TransactionToMessage(...)") + + gp := core.GasPool(math.MaxUint64) + got, err := core.ApplyMessage(evm, msg, &gp) + require.NoError(t, err, "core.ApplyMessage(...)") + require.ErrorIsf(t, got.Err, tt.wantVMErr, "%T.Err", got) + }) + }) + } +} diff --git a/libevm/ethtest/dummy.go b/libevm/ethtest/dummy.go new file mode 100644 index 00000000000..e800a513d27 --- /dev/null +++ b/libevm/ethtest/dummy.go @@ -0,0 +1,44 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package ethtest + +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" +) + +// DummyChainContext returns a dummy that returns [DummyEngine] when its +// Engine() method is called, and panics when its GetHeader() method is called. +func DummyChainContext() core.ChainContext { + return chainContext{} +} + +// DummyEngine returns a dummy that panics when its Author() method is called. +func DummyEngine() 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/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index f12c9bfcfd3..a244093bcf1 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -32,7 +32,6 @@ import ( "go.uber.org/goleak" "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/libevm/core/vm" @@ -190,19 +189,13 @@ func TestProcessor(t *testing.T) { } } -type noopHooks struct{} - -func (noopHooks) OverrideNewEVMArgs(a *vm.NewEVMArgs) *vm.NewEVMArgs { - return a -} - -func (noopHooks) OverrideEVMResetArgs(_ params.Rules, a *vm.EVMResetArgs) *vm.EVMResetArgs { - return a -} - type vmHooks struct { vm.Preprocessor // the [Processor] - noopHooks + vm.NOOPHooks +} + +func (h *vmHooks) PreprocessingGasCharge(tx common.Hash) (uint64, error) { + return h.Preprocessor.PreprocessingGasCharge(tx) } func TestIntegration(t *testing.T) { @@ -214,7 +207,7 @@ func TestIntegration(t *testing.T) { sut := New(handler, 8) t.Cleanup(sut.Close) - vm.RegisterHooks(vmHooks{Preprocessor: sut}) + vm.RegisterHooks(&vmHooks{Preprocessor: sut}) t.Cleanup(vm.TestOnlyClearRegisteredHooks) stub := &hookstest.Stub{ @@ -313,7 +306,7 @@ func TestIntegration(t *testing.T) { var usedGas uint64 receipt, err := core.ApplyTransaction( evm.ChainConfig(), - chainContext{}, + ethtest.DummyChainContext(), &block.Header().Coinbase, &pool, state, @@ -330,13 +323,3 @@ func TestIntegration(t *testing.T) { t.Errorf("%T diff (-want +got):\n%s", got, diff) } } - -// Dummy implementations of interfaces required by [core.ApplyTransaction]. -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) { return common.Address{}, nil } From 9b26ef3157f659601b49787cb3eb76c67fc31104 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 23 Sep 2025 12:32:41 +0100 Subject: [PATCH 10/18] test(vm): error propagation from preprocessing gas charge --- core/vm/preprocess.libevm_test.go | 39 +++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/core/vm/preprocess.libevm_test.go b/core/vm/preprocess.libevm_test.go index ee9463f641a..a45a2e84ce9 100644 --- a/core/vm/preprocess.libevm_test.go +++ b/core/vm/preprocess.libevm_test.go @@ -17,6 +17,7 @@ package vm_test import ( + "errors" "fmt" "math" "math/big" @@ -41,22 +42,25 @@ type preprocessingCharger struct { charge map[common.Hash]uint64 } +var errUnknownTx = errors.New("unknown tx") + func (p preprocessingCharger) PreprocessingGasCharge(tx common.Hash) (uint64, error) { c, ok := p.charge[tx] if !ok { - return 0, fmt.Errorf("unknown tx %v", tx) + return 0, fmt.Errorf("%w: %v", errUnknownTx, tx) } return c, nil } func TestChargePreprocessingGas(t *testing.T) { tests := []struct { - name string - to *common.Address - charge uint64 - txGas uint64 - wantVMErr error - wantGasUsed uint64 + name string + to *common.Address + charge uint64 + skipChargeRegistration bool + txGas uint64 + wantVMErr error + wantGasUsed uint64 }{ { name: "standard create", @@ -92,6 +96,14 @@ func TestChargePreprocessingGas(t *testing.T) { txGas: params.TxGas + 20000, wantGasUsed: params.TxGas + 13579, }, + { + name: "error propagation", + to: &common.Address{}, + skipChargeRegistration: true, + txGas: params.TxGas, + wantGasUsed: params.TxGas, + wantVMErr: errUnknownTx, + }, } config := params.AllDevChainProtocolChanges @@ -108,14 +120,19 @@ func TestChargePreprocessingGas(t *testing.T) { var txs types.Transactions charge := make(map[common.Hash]uint64) - for _, tt := range tests { + for i, tt := range tests { tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ + // Although nonces aren't strictly necessary, they guarantee a + // different tx hash for each one. + Nonce: uint64(i), //nolint:gosec // Known to not overflow To: tt.to, GasPrice: big.NewInt(1), Gas: tt.txGas, }) txs = append(txs, tx) - charge[tx.Hash()] = tt.charge + if !tt.skipChargeRegistration { + charge[tx.Hash()] = tt.charge + } } vm.RegisterHooks(&preprocessingCharger{ @@ -138,6 +155,7 @@ func TestChargePreprocessingGas(t *testing.T) { require.NoError(t, err, "state.New(types.EmptyRootHash, [memory db], nil)") sdb.SetTxContext(tx.Hash(), i) sdb.SetBalance(eoa, new(uint256.Int).SetAllOne()) + sdb.SetNonce(eoa, tx.Nonce()) var gotGasUsed uint64 gp := core.GasPool(math.MaxUint64) @@ -164,8 +182,9 @@ func TestChargePreprocessingGas(t *testing.T) { t.Run("VM_error", func(t *testing.T) { sdb, evm := ethtest.NewZeroEVM(t, ethtest.WithChainConfig(config)) - sdb.SetBalance(eoa, new(uint256.Int).SetAllOne()) sdb.SetTxContext(tx.Hash(), i) + sdb.SetBalance(eoa, new(uint256.Int).SetAllOne()) + sdb.SetNonce(eoa, tx.Nonce()) msg, err := core.TransactionToMessage(tx, signer, header.BaseFee) require.NoError(t, err, "core.TransactionToMessage(...)") From f7ff38dccc22bb311884738eac5986478f71aec1 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 23 Sep 2025 12:34:00 +0100 Subject: [PATCH 11/18] chore: placate the linter --- core/vm/preprocess.libevm_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/vm/preprocess.libevm_test.go b/core/vm/preprocess.libevm_test.go index a45a2e84ce9..890e007e9e5 100644 --- a/core/vm/preprocess.libevm_test.go +++ b/core/vm/preprocess.libevm_test.go @@ -23,6 +23,10 @@ import ( "math/big" "testing" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core" "github.com/ava-labs/libevm/core/rawdb" @@ -32,9 +36,6 @@ import ( "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm/ethtest" "github.com/ava-labs/libevm/params" - "github.com/holiman/uint256" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type preprocessingCharger struct { From 031d4ff026bca2a750bb11391897cfbdfa4d91e3 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 23 Sep 2025 15:26:28 +0100 Subject: [PATCH 12/18] fix: clone tx data in test handler --- core/vm/preprocess.libevm_test.go | 2 +- libevm/precompiles/parallel/parallel_test.go | 25 +++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/core/vm/preprocess.libevm_test.go b/core/vm/preprocess.libevm_test.go index 890e007e9e5..e361205b2c6 100644 --- a/core/vm/preprocess.libevm_test.go +++ b/core/vm/preprocess.libevm_test.go @@ -125,7 +125,7 @@ func TestChargePreprocessingGas(t *testing.T) { tx := types.MustSignNewTx(key, signer, &types.LegacyTx{ // Although nonces aren't strictly necessary, they guarantee a // different tx hash for each one. - Nonce: uint64(i), //nolint:gosec // Known to not overflow + Nonce: uint64(i), To: tt.to, GasPrice: big.NewInt(1), Gas: tt.txGas, diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index a244093bcf1..10b220c2e7a 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -65,7 +65,7 @@ func (r *reverser) Gas(tx *types.Transaction) (uint64, bool) { } func reverserOutput(data, extra []byte) []byte { - out := append(data, extra...) + out := append(slices.Clone(data), extra...) slices.Reverse(out) return out } @@ -222,7 +222,7 @@ func TestIntegration(t *testing.T) { if !ok { t.Errorf("no result for tx[%d] %v", txi, txh) } - env.StateDB().AddLog(&types.Log{ + sdb.AddLog(&types.Log{ Data: got[:], }) return nil, nil @@ -231,13 +231,13 @@ func TestIntegration(t *testing.T) { } stub.Register(t) - state, evm := ethtest.NewZeroEVM(t) - key, err := crypto.GenerateKey() require.NoErrorf(t, err, "crypto.GenerateKey()") eoa := crypto.PubkeyToAddress(key.PublicKey) + + state, evm := ethtest.NewZeroEVM(t) state.CreateAccount(eoa) - state.AddBalance(eoa, uint256.NewInt(10*params.Ether)) + state.SetBalance(eoa, new(uint256.Int).SetAllOne()) var ( txs types.Transactions @@ -251,7 +251,14 @@ func TestIntegration(t *testing.T) { cmpopts.IgnoreFields(types.Log{}, "BlockHash"), } - signer := types.LatestSigner(evm.ChainConfig()) + header := &types.Header{ + Number: big.NewInt(0), + BaseFee: big.NewInt(0), + } + config := evm.ChainConfig() + rules := config.Rules(header.Number, true, header.Time) + signer := types.MakeSigner(config, header.Number, header.Time) + for i, addr := range []common.Address{ {'o', 't', 'h', 'e', 'r'}, handler.addr, @@ -259,9 +266,6 @@ func TestIntegration(t *testing.T) { ui := uint(i) data := []byte("hello, world") - // Having all arguments `false` is equivalent to what - // [core.ApplyTransaction] will do. - rules := evm.ChainConfig().Rules(big.NewInt(0), false, 0) gas, err := intrinsicGas(data, types.AccessList{}, &addr, &rules) require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, false, false, false)", data) if addr == handler.addr { @@ -293,8 +297,7 @@ func TestIntegration(t *testing.T) { want = append(want, wantR) } - block := types.NewBlock(&types.Header{}, txs, nil, nil, trie.NewStackTrie(nil)) - rules := evm.ChainConfig().Rules(block.Number(), true, block.Time()) + block := types.NewBlock(header, txs, nil, nil, trie.NewStackTrie(nil)) require.NoError(t, sut.StartBlock(block, rules), "StartBlock()") defer sut.FinishBlock(block) From 5ca8376cb70bc41013c1cae6cf63063eacb070ea Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 24 Sep 2025 12:36:32 +0100 Subject: [PATCH 13/18] feat: read-only state access --- libevm/ethtest/evm.go | 19 ++++- libevm/precompiles/parallel/parallel.go | 88 ++++++++++++++++---- libevm/precompiles/parallel/parallel_test.go | 11 +-- 3 files changed, 93 insertions(+), 25 deletions(-) diff --git a/libevm/ethtest/evm.go b/libevm/ethtest/evm.go index 4e16c4e90bb..7a7b463295e 100644 --- a/libevm/ethtest/evm.go +++ b/libevm/ethtest/evm.go @@ -23,14 +23,28 @@ import ( "github.com/stretchr/testify/require" - "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/ethdb" "github.com/ava-labs/libevm/params" ) +// NewEmptyStateDB returns a fresh database from [rawdb.NewMemoryDatabase], a +// [state.Database] wrapping it, and a [state.StateDB] wrapping that, opened to +// [types.EmptyRootHash]. +func NewEmptyStateDB(tb testing.TB) (ethdb.Database, state.Database, *state.StateDB) { + tb.Helper() + + db := rawdb.NewMemoryDatabase() + cache := state.NewDatabase(db) + sdb, err := state.New(types.EmptyRootHash, cache, nil) + require.NoError(tb, err, "state.New()") + return db, cache, sdb +} + // NewZeroEVM returns a new EVM backed by a [rawdb.NewMemoryDatabase]; all other // arguments to [vm.NewEVM] are the zero values of their respective types, // except for the use of [core.CanTransfer] and [core.Transfer] instead of nil @@ -38,8 +52,7 @@ import ( func NewZeroEVM(tb testing.TB, opts ...EVMOption) (*state.StateDB, *vm.EVM) { tb.Helper() - sdb, err := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) - require.NoError(tb, err, "state.New()") + _, _, sdb := NewEmptyStateDB(tb) args := &evmConstructorArgs{ vm.BlockContext{ diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index 3f0e3f29a9d..6227c3d0dd8 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -25,8 +25,10 @@ import ( "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/core/vm" + "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/params" ) @@ -44,16 +46,17 @@ import ( type Handler[Result any] interface { BeforeBlock(*types.Header) Gas(*types.Transaction) (gas uint64, process bool) - Process(index int, tx *types.Transaction) Result + Process(index int, tx *types.Transaction, sdb libevm.StateReader) Result } // A Processor orchestrates dispatch and collection of results from a [Handler]. type Processor[R any] struct { - handler Handler[R] - workers sync.WaitGroup - work chan *job - results [](chan result[R]) - txGas map[common.Hash]uint64 + handler Handler[R] + workers sync.WaitGroup + work chan *job + results [](chan result[R]) + txGas map[common.Hash]uint64 + stateShare stateDBSharer } type job struct { @@ -66,36 +69,75 @@ type result[T any] struct { val *T } +// A stateDBSharer allows concurrent workers to make copies of a primary +// database. When the `nextAvailable` channel is closed, all workers call +// [state.StateDB.Copy] then signal completion on the [sync.WaitGroup]. The +// channel is replaced for each round of distribution. +type stateDBSharer struct { + nextAvailable chan struct{} + primary *state.StateDB + mu sync.Mutex + workers int + wg sync.WaitGroup +} + // New constructs a new [Processor] with the specified number of concurrent // workers. [Processor.Close] must be called after the final call to // [Processor.FinishBlock] to avoid leaking goroutines. func New[R any](h Handler[R], workers int) *Processor[R] { + workers = max(workers, 1) + p := &Processor[R]{ handler: h, work: make(chan *job), txGas: make(map[common.Hash]uint64), + stateShare: stateDBSharer{ + workers: workers, + nextAvailable: make(chan struct{}), + }, } - workers = max(workers, 1) - p.workers.Add(workers) + p.workers.Add(workers) // for shutdown via [Processor.Close] + p.stateShare.wg.Add(workers) // for readiness of [Processor.worker] loops for range workers { go p.worker() } + p.stateShare.wg.Wait() + return p } func (p *Processor[R]) worker() { defer p.workers.Done() + + var sdb *state.StateDB + share := &p.stateShare + stateAvailable := share.nextAvailable + // Without this signal of readiness, a premature call to + // [Processor.StartBlock] could replace `share.nextAvailable` before we've + // copied it. + share.wg.Done() + for { - w, ok := <-p.work - if !ok { - return - } + select { + case <-stateAvailable: // guaranteed at the beginning of each block + share.mu.Lock() + sdb = share.primary.Copy() + share.mu.Unlock() - r := p.handler.Process(w.index, w.tx) - p.results[w.index] <- result[R]{ - tx: w.tx.Hash(), - val: &r, + stateAvailable = share.nextAvailable + share.wg.Done() + + case w, ok := <-p.work: + if !ok { + return + } + + r := p.handler.Process(w.index, w.tx, sdb) + p.results[w.index] <- result[R]{ + tx: w.tx.Hash(), + val: &r, + } } } } @@ -109,7 +151,8 @@ func (p *Processor[R]) Close() { // StartBlock dispatches transactions to the [Handler] and returns immediately. // It MUST be paired with a call to [Processor.FinishBlock], without overlap of // blocks. -func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules) error { +func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules, sdb *state.StateDB) error { + p.stateShare.distribute(sdb) p.handler.BeforeBlock(types.CopyHeader(b.Header())) txs := b.Transactions() jobs := make([]*job, 0, len(txs)) @@ -149,6 +192,17 @@ func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules) error { return nil } +func (s *stateDBSharer) distribute(sdb *state.StateDB) { + s.primary = sdb // no need to Copy() as each worker does it + + ch := s.nextAvailable + s.nextAvailable = make(chan struct{}) // already copied by each worker + + s.wg.Add(s.workers) + close(ch) + s.wg.Wait() +} + // FinishBlock returns the [Processor] to a state ready for the next block. A // return from FinishBlock guarantees that all dispatched work from the // respective call to [Processor.StartBlock] has been completed. diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index 10b220c2e7a..b8931c525e3 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -70,7 +70,7 @@ func reverserOutput(data, extra []byte) []byte { return out } -func (r *reverser) Process(i int, tx *types.Transaction) []byte { +func (r *reverser) Process(i int, tx *types.Transaction, _ libevm.StateReader) []byte { return reverserOutput(tx.Data(), r.extra) } @@ -127,6 +127,8 @@ func TestProcessor(t *testing.T) { }) } + _, _, sdb := ethtest.NewEmptyStateDB(t) + for _, tt := range tests { t.Run("", func(t *testing.T) { t.Logf("%+v", tt) @@ -165,7 +167,7 @@ func TestProcessor(t *testing.T) { extra := []byte("extra") block := types.NewBlock(&types.Header{Extra: extra}, txs, nil, nil, trie.NewStackTrie(nil)) - require.NoError(t, p.StartBlock(block, rules), "StartBlock()") + require.NoError(t, p.StartBlock(block, rules, sdb), "StartBlock()") defer p.FinishBlock(block) for i, tx := range txs { @@ -287,18 +289,17 @@ func TestIntegration(t *testing.T) { TransactionIndex: ui, } if addr == handler.addr { - res := handler.Process(i, tx) wantR.Logs = []*types.Log{{ TxHash: tx.Hash(), TxIndex: ui, - Data: res[:], + Data: reverserOutput(data, nil), }} } want = append(want, wantR) } block := types.NewBlock(header, txs, nil, nil, trie.NewStackTrie(nil)) - require.NoError(t, sut.StartBlock(block, rules), "StartBlock()") + require.NoError(t, sut.StartBlock(block, rules, state), "StartBlock()") defer sut.FinishBlock(block) pool := core.GasPool(math.MaxUint64) From 63e8dcd25d0507d043f7e79443de9ae68bf8b6ed Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 24 Sep 2025 12:47:42 +0100 Subject: [PATCH 14/18] test: `StateDB` propagation --- libevm/precompiles/parallel/parallel.go | 4 +-- libevm/precompiles/parallel/parallel_test.go | 32 ++++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index 6227c3d0dd8..8ba1760f2a9 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -46,7 +46,7 @@ import ( type Handler[Result any] interface { BeforeBlock(*types.Header) Gas(*types.Transaction) (gas uint64, process bool) - Process(index int, tx *types.Transaction, sdb libevm.StateReader) Result + Process(sdb libevm.StateReader, index int, tx *types.Transaction) Result } // A Processor orchestrates dispatch and collection of results from a [Handler]. @@ -133,7 +133,7 @@ func (p *Processor[R]) worker() { return } - r := p.handler.Process(w.index, w.tx, sdb) + r := p.handler.Process(sdb, w.index, w.tx) p.results[w.index] <- result[R]{ tx: w.tx.Hash(), val: &r, diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index b8931c525e3..0ba4e11335c 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -48,13 +48,14 @@ func TestMain(m *testing.M) { } type reverser struct { - extra []byte - addr common.Address - gas uint64 + headerExtra []byte + addr common.Address + stateKey common.Hash + gas uint64 } func (r *reverser) BeforeBlock(h *types.Header) { - r.extra = h.Extra + r.headerExtra = slices.Clone(h.Extra) } func (r *reverser) Gas(tx *types.Transaction) (uint64, bool) { @@ -64,20 +65,25 @@ func (r *reverser) Gas(tx *types.Transaction) (uint64, bool) { return r.gas, true } -func reverserOutput(data, extra []byte) []byte { - out := append(slices.Clone(data), extra...) +func reverserOutput(txData []byte, state common.Hash, extra []byte) []byte { + out := slices.Concat(txData, state[:], extra) slices.Reverse(out) return out } -func (r *reverser) Process(i int, tx *types.Transaction, _ libevm.StateReader) []byte { - return reverserOutput(tx.Data(), r.extra) +func (r *reverser) Process(sdb libevm.StateReader, i int, tx *types.Transaction) []byte { + return reverserOutput( + tx.Data(), + sdb.GetTransientState(r.addr, r.stateKey), + r.headerExtra, + ) } func TestProcessor(t *testing.T) { handler := &reverser{ - addr: common.Address{'r', 'e', 'v', 'e', 'r', 's', 'e'}, - gas: 1e6, + addr: common.Address{'r', 'e', 'v', 'e', 'r', 's', 'e'}, + stateKey: common.Hash{'k', 'e', 'y'}, + gas: 1e6, } p := New(handler, 8) t.Cleanup(p.Close) @@ -128,6 +134,8 @@ func TestProcessor(t *testing.T) { } _, _, sdb := ethtest.NewEmptyStateDB(t) + stateVal := common.Hash{'s', 't', 'a', 't', 'e'} + sdb.SetTransientState(handler.addr, handler.stateKey, stateVal) for _, tt := range tests { t.Run("", func(t *testing.T) { @@ -175,7 +183,7 @@ func TestProcessor(t *testing.T) { var want []byte if wantOK { - want = reverserOutput(tx.Data(), extra) + want = reverserOutput(tx.Data(), stateVal, extra) } got, gotOK := p.Result(i) @@ -292,7 +300,7 @@ func TestIntegration(t *testing.T) { wantR.Logs = []*types.Log{{ TxHash: tx.Hash(), TxIndex: ui, - Data: reverserOutput(data, nil), + Data: reverserOutput(data, common.Hash{}, nil), }} } want = append(want, wantR) From 866bb86448e7e146707899825184007c49d6b8d0 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Mon, 29 Sep 2025 12:07:24 +0100 Subject: [PATCH 15/18] refactor: readability improvements --- core/vm/preprocess.libevm_test.go | 9 +-- libevm/precompiles/parallel/parallel.go | 63 ++++++++++---------- libevm/precompiles/parallel/parallel_test.go | 44 +++++++------- 3 files changed, 54 insertions(+), 62 deletions(-) diff --git a/core/vm/preprocess.libevm_test.go b/core/vm/preprocess.libevm_test.go index e361205b2c6..509682668c0 100644 --- a/core/vm/preprocess.libevm_test.go +++ b/core/vm/preprocess.libevm_test.go @@ -29,8 +29,6 @@ import ( "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" @@ -148,12 +146,7 @@ func TestChargePreprocessingGas(t *testing.T) { t.Logf("Extra gas charge: %d", tt.charge) t.Run("ApplyTransaction", func(t *testing.T) { - sdb, err := state.New( - types.EmptyRootHash, - state.NewDatabase(rawdb.NewMemoryDatabase()), - nil, - ) - require.NoError(t, err, "state.New(types.EmptyRootHash, [memory db], nil)") + _, _, sdb := ethtest.NewEmptyStateDB(t) sdb.SetTxContext(tx.Hash(), i) sdb.SetBalance(eoa, new(uint256.Int).SetAllOne()) sdb.SetNonce(eoa, tx.Nonce()) diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index 8ba1760f2a9..4caca882898 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -69,18 +69,6 @@ type result[T any] struct { val *T } -// A stateDBSharer allows concurrent workers to make copies of a primary -// database. When the `nextAvailable` channel is closed, all workers call -// [state.StateDB.Copy] then signal completion on the [sync.WaitGroup]. The -// channel is replaced for each round of distribution. -type stateDBSharer struct { - nextAvailable chan struct{} - primary *state.StateDB - mu sync.Mutex - workers int - wg sync.WaitGroup -} - // New constructs a new [Processor] with the specified number of concurrent // workers. [Processor.Close] must be called after the final call to // [Processor.FinishBlock] to avoid leaking goroutines. @@ -107,6 +95,29 @@ func New[R any](h Handler[R], workers int) *Processor[R] { return p } +// A stateDBSharer allows concurrent workers to make copies of a primary +// database. When the `nextAvailable` channel is closed, all workers call +// [state.StateDB.Copy] then signal completion on the [sync.WaitGroup]. The +// channel is replaced for each round of distribution. +type stateDBSharer struct { + nextAvailable chan struct{} + primary *state.StateDB + mu sync.Mutex + workers int + wg sync.WaitGroup +} + +func (s *stateDBSharer) distribute(sdb *state.StateDB) { + s.primary = sdb // no need to Copy() as each worker does it + + ch := s.nextAvailable // already copied by [Processor.worker], which is waiting for it to close + s.nextAvailable = make(chan struct{}) // will be copied, ready for the next distribution + + s.wg.Add(s.workers) + close(ch) + s.wg.Wait() +} + func (p *Processor[R]) worker() { defer p.workers.Done() @@ -192,17 +203,6 @@ func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules, sdb *state return nil } -func (s *stateDBSharer) distribute(sdb *state.StateDB) { - s.primary = sdb // no need to Copy() as each worker does it - - ch := s.nextAvailable - s.nextAvailable = make(chan struct{}) // already copied by each worker - - s.wg.Add(s.workers) - close(ch) - s.wg.Wait() -} - // FinishBlock returns the [Processor] to a state ready for the next block. A // return from FinishBlock guarantees that all dispatched work from the // respective call to [Processor.StartBlock] has been completed. @@ -210,7 +210,7 @@ func (p *Processor[R]) FinishBlock(b *types.Block) { for i := range len(b.Transactions()) { // Every result channel is guaranteed to have some value in its buffer // because [Processor.BeforeBlock] either sends a nil *R or it - // dispatches a job that will send a non-nil *R. + // dispatches a job, which will send a non-nil *R. tx := (<-p.results[i]).tx delete(p.txGas, tx) } @@ -243,7 +243,7 @@ func (p *Processor[R]) Result(i int) (R, bool) { return *r.val, true } -func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) (process bool, err error) { +func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) (process bool, retErr error) { // An explicit 0 is necessary to avoid [Processor.PreprocessingGasCharge] // returning [ErrTxUnknown]. p.txGas[tx.Hash()] = 0 @@ -253,7 +253,7 @@ func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) return false, nil } defer func() { - if process && err == nil { + if process && retErr == nil { p.txGas[tx.Hash()] = cost } }() @@ -262,11 +262,12 @@ func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) if err != nil { return false, fmt.Errorf("calculating intrinsic gas of %v: %v", tx.Hash(), err) } - - // This could only overflow if the gas limit was insufficient to cover - // the intrinsic cost, which would have invalidated it for inclusion. - left := tx.Gas() - spent - return left >= cost, nil + if spent > tx.Gas() { + // If this happens then consensus has a bug because the tx shouldn't + // have been included. We include the check, however, for completeness. + return false, core.ErrIntrinsicGas + } + return tx.Gas()-spent >= cost, nil } func txIntrinsicGas(tx *types.Transaction, rules *params.Rules) (uint64, error) { diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index 0ba4e11335c..dc083da99bd 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -47,41 +47,39 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m, goleak.IgnoreCurrent()) } -type reverser struct { +type concat struct { headerExtra []byte addr common.Address stateKey common.Hash gas uint64 } -func (r *reverser) BeforeBlock(h *types.Header) { - r.headerExtra = slices.Clone(h.Extra) +func (c *concat) BeforeBlock(h *types.Header) { + c.headerExtra = slices.Clone(h.Extra) } -func (r *reverser) Gas(tx *types.Transaction) (uint64, bool) { - if to := tx.To(); to == nil || *to != r.addr { - return 0, false +func (c *concat) Gas(tx *types.Transaction) (uint64, bool) { + if to := tx.To(); to != nil && *to == c.addr { + return c.gas, true } - return r.gas, true + return 0, false } -func reverserOutput(txData []byte, state common.Hash, extra []byte) []byte { - out := slices.Concat(txData, state[:], extra) - slices.Reverse(out) - return out +func concatOutput(txData []byte, state common.Hash, extra []byte) []byte { + return slices.Concat(txData, state[:], extra) } -func (r *reverser) Process(sdb libevm.StateReader, i int, tx *types.Transaction) []byte { - return reverserOutput( +func (c *concat) Process(sdb libevm.StateReader, i int, tx *types.Transaction) []byte { + return concatOutput( tx.Data(), - sdb.GetTransientState(r.addr, r.stateKey), - r.headerExtra, + sdb.GetTransientState(c.addr, c.stateKey), + c.headerExtra, ) } func TestProcessor(t *testing.T) { - handler := &reverser{ - addr: common.Address{'r', 'e', 'v', 'e', 'r', 's', 'e'}, + handler := &concat{ + addr: common.Address{'c', 'o', 'n', 'c', 'a', 't'}, stateKey: common.Hash{'k', 'e', 'y'}, gas: 1e6, } @@ -164,7 +162,7 @@ func TestProcessor(t *testing.T) { data := binary.BigEndian.AppendUint64(nil, uint64(i)) gas, err := intrinsicGas(data, types.AccessList{}, &handler.addr, &rules) - require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, true, true, true)", data) + require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, ...)", data) txs[i] = types.NewTx(&types.LegacyTx{ To: &to, @@ -183,7 +181,7 @@ func TestProcessor(t *testing.T) { var want []byte if wantOK { - want = reverserOutput(tx.Data(), stateVal, extra) + want = concatOutput(tx.Data(), stateVal, extra) } got, gotOK := p.Result(i) @@ -210,8 +208,8 @@ func (h *vmHooks) PreprocessingGasCharge(tx common.Hash) (uint64, error) { func TestIntegration(t *testing.T) { const handlerGas = 500 - handler := &reverser{ - addr: common.Address{'r', 'e', 'v', 'e', 'r', 's', 'e'}, + handler := &concat{ + addr: common.Address{'c', 'o', 'n', 'c', 'a', 't'}, gas: handlerGas, } sut := New(handler, 8) @@ -277,7 +275,7 @@ func TestIntegration(t *testing.T) { data := []byte("hello, world") gas, err := intrinsicGas(data, types.AccessList{}, &addr, &rules) - require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, false, false, false)", data) + require.NoError(t, err, "core.IntrinsicGas(%#x, nil, false, ...)", data) if addr == handler.addr { gas += handlerGas } @@ -300,7 +298,7 @@ func TestIntegration(t *testing.T) { wantR.Logs = []*types.Log{{ TxHash: tx.Hash(), TxIndex: ui, - Data: reverserOutput(data, common.Hash{}, nil), + Data: concatOutput(data, common.Hash{}, nil), }} } want = append(want, wantR) From 32a101a9e1e03b0faec1126fcecce6290d8480ad Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 29 Oct 2025 20:52:27 +0000 Subject: [PATCH 16/18] feat: before- and after-block hooks with additional arguments --- libevm/precompiles/parallel/parallel.go | 32 +++++++++++++++++--- libevm/precompiles/parallel/parallel_test.go | 14 +++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index 4caca882898..cab21885169 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -29,6 +29,7 @@ import ( "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/libevm" + "github.com/ava-labs/libevm/libevm/stateconf" "github.com/ava-labs/libevm/params" ) @@ -43,10 +44,22 @@ import ( // // Scenario (2) allows precompile access to be determined through inspection of // the [types.Transaction] alone, without the need for execution. +// +// All [libevm.StateReader] instances are opened to the state at the beginning +// of the block. The [StateDB] is the same one used to execute the block, +// before being committed, and MAY be written to. type Handler[Result any] interface { - BeforeBlock(*types.Header) + BeforeBlock(libevm.StateReader, *types.Block) Gas(*types.Transaction) (gas uint64, process bool) Process(sdb libevm.StateReader, index int, tx *types.Transaction) Result + AfterBlock(StateDB, *types.Block, types.Receipts) +} + +// StateDB is the subset of [state.StateDB] methods that MAY be called by +// [Handler.AfterBlock]. +type StateDB interface { + libevm.StateReader + SetState(_ common.Address, key, val common.Hash, _ ...stateconf.StateDBStateOption) } // A Processor orchestrates dispatch and collection of results from a [Handler]. @@ -162,9 +175,19 @@ func (p *Processor[R]) Close() { // StartBlock dispatches transactions to the [Handler] and returns immediately. // It MUST be paired with a call to [Processor.FinishBlock], without overlap of // blocks. -func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules, sdb *state.StateDB) error { +func (p *Processor[R]) StartBlock(sdb *state.StateDB, rules params.Rules, b *types.Block) error { + // The distribution mechanism copies the StateDB so we don't need to do it + // here, but the [Handler] is called directly so we do copy. p.stateShare.distribute(sdb) - p.handler.BeforeBlock(types.CopyHeader(b.Header())) + p.handler.BeforeBlock( + sdb.Copy(), + types.NewBlockWithHeader( + b.Header(), + ).WithBody( + *b.Body(), + ), + ) + txs := b.Transactions() jobs := make([]*job, 0, len(txs)) @@ -206,7 +229,7 @@ func (p *Processor[R]) StartBlock(b *types.Block, rules params.Rules, sdb *state // FinishBlock returns the [Processor] to a state ready for the next block. A // return from FinishBlock guarantees that all dispatched work from the // respective call to [Processor.StartBlock] has been completed. -func (p *Processor[R]) FinishBlock(b *types.Block) { +func (p *Processor[R]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.Receipts) { for i := range len(b.Transactions()) { // Every result channel is guaranteed to have some value in its buffer // because [Processor.BeforeBlock] either sends a nil *R or it @@ -214,6 +237,7 @@ func (p *Processor[R]) FinishBlock(b *types.Block) { tx := (<-p.results[i]).tx delete(p.txGas, tx) } + p.handler.AfterBlock(sdb, b, rs) } // Result blocks until the i'th transaction passed to [Processor.StartBlock] has diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index dc083da99bd..7ed2589c4f5 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -54,8 +54,8 @@ type concat struct { gas uint64 } -func (c *concat) BeforeBlock(h *types.Header) { - c.headerExtra = slices.Clone(h.Extra) +func (c *concat) BeforeBlock(_ libevm.StateReader, b *types.Block) { + c.headerExtra = slices.Clone(b.Header().Extra) } func (c *concat) Gas(tx *types.Transaction) (uint64, bool) { @@ -77,6 +77,8 @@ func (c *concat) Process(sdb libevm.StateReader, i int, tx *types.Transaction) [ ) } +func (*concat) AfterBlock(StateDB, *types.Block, types.Receipts) {} + func TestProcessor(t *testing.T) { handler := &concat{ addr: common.Address{'c', 'o', 'n', 'c', 'a', 't'}, @@ -173,8 +175,8 @@ func TestProcessor(t *testing.T) { extra := []byte("extra") block := types.NewBlock(&types.Header{Extra: extra}, txs, nil, nil, trie.NewStackTrie(nil)) - require.NoError(t, p.StartBlock(block, rules, sdb), "StartBlock()") - defer p.FinishBlock(block) + require.NoError(t, p.StartBlock(sdb, rules, block), "StartBlock()") + defer p.FinishBlock(sdb, block, nil) for i, tx := range txs { wantOK := wantProcessed[i] @@ -305,8 +307,7 @@ func TestIntegration(t *testing.T) { } block := types.NewBlock(header, txs, nil, nil, trie.NewStackTrie(nil)) - require.NoError(t, sut.StartBlock(block, rules, state), "StartBlock()") - defer sut.FinishBlock(block) + require.NoError(t, sut.StartBlock(state, rules, block), "StartBlock()") pool := core.GasPool(math.MaxUint64) var got []*types.Receipt @@ -332,4 +333,5 @@ func TestIntegration(t *testing.T) { if diff := cmp.Diff(want, got, ignore); diff != "" { t.Errorf("%T diff (-want +got):\n%s", got, diff) } + sut.FinishBlock(state, block, got) } From ed05fb6210f97a7a5627ace089b4d827708e8fd5 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 30 Oct 2025 21:03:22 +0000 Subject: [PATCH 17/18] feat: `Handler.Prefetch()` pipelines into `Handler.Process()` --- libevm/precompiles/parallel/parallel.go | 94 ++++++++----- libevm/precompiles/parallel/parallel_test.go | 138 ++++++++++++------- 2 files changed, 149 insertions(+), 83 deletions(-) diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index cab21885169..f35d0b7208a 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -48,10 +48,11 @@ import ( // All [libevm.StateReader] instances are opened to the state at the beginning // of the block. The [StateDB] is the same one used to execute the block, // before being committed, and MAY be written to. -type Handler[Result any] interface { +type Handler[Data, Result any] interface { BeforeBlock(libevm.StateReader, *types.Block) Gas(*types.Transaction) (gas uint64, process bool) - Process(sdb libevm.StateReader, index int, tx *types.Transaction) Result + Prefetch(sdb libevm.StateReader, index int, tx *types.Transaction) Data + Process(sdb libevm.StateReader, index int, tx *types.Transaction, data Data) Result AfterBlock(StateDB, *types.Block, types.Receipts) } @@ -63,13 +64,14 @@ type StateDB interface { } // A Processor orchestrates dispatch and collection of results from a [Handler]. -type Processor[R any] struct { - handler Handler[R] - workers sync.WaitGroup - work chan *job - results [](chan result[R]) - txGas map[common.Hash]uint64 - stateShare stateDBSharer +type Processor[D, R any] struct { + handler Handler[D, R] + workers sync.WaitGroup + prefetch, process chan *job + data [](chan D) + results [](chan result[R]) + txGas map[common.Hash]uint64 + stateShare stateDBSharer } type job struct { @@ -85,13 +87,16 @@ type result[T any] struct { // New constructs a new [Processor] with the specified number of concurrent // workers. [Processor.Close] must be called after the final call to // [Processor.FinishBlock] to avoid leaking goroutines. -func New[R any](h Handler[R], workers int) *Processor[R] { - workers = max(workers, 1) - - p := &Processor[R]{ - handler: h, - work: make(chan *job), - txGas: make(map[common.Hash]uint64), +func New[D, R any](h Handler[D, R], prefetchers, processors int) *Processor[D, R] { + prefetchers = max(prefetchers, 1) + processors = max(processors, 1) + workers := prefetchers + processors + + p := &Processor[D, R]{ + handler: h, + prefetch: make(chan *job), + process: make(chan *job), + txGas: make(map[common.Hash]uint64), stateShare: stateDBSharer{ workers: workers, nextAvailable: make(chan struct{}), @@ -100,8 +105,11 @@ func New[R any](h Handler[R], workers int) *Processor[R] { p.workers.Add(workers) // for shutdown via [Processor.Close] p.stateShare.wg.Add(workers) // for readiness of [Processor.worker] loops - for range workers { - go p.worker() + for range prefetchers { + go p.worker(p.prefetch, nil) + } + for range processors { + go p.worker(nil, p.process) } p.stateShare.wg.Wait() @@ -131,7 +139,7 @@ func (s *stateDBSharer) distribute(sdb *state.StateDB) { s.wg.Wait() } -func (p *Processor[R]) worker() { +func (p *Processor[D, R]) worker(prefetch, process chan *job) { defer p.workers.Done() var sdb *state.StateDB @@ -152,14 +160,20 @@ func (p *Processor[R]) worker() { stateAvailable = share.nextAvailable share.wg.Done() - case w, ok := <-p.work: + case job, ok := <-prefetch: + if !ok { + return + } + p.data[job.index] <- p.handler.Prefetch(sdb, job.index, job.tx) + + case job, ok := <-process: if !ok { return } - r := p.handler.Process(sdb, w.index, w.tx) - p.results[w.index] <- result[R]{ - tx: w.tx.Hash(), + r := p.handler.Process(sdb, job.index, job.tx, <-p.data[job.index]) + p.results[job.index] <- result[R]{ + tx: job.tx.Hash(), val: &r, } } @@ -167,15 +181,16 @@ func (p *Processor[R]) worker() { } // Close shuts down the [Processor], after which it can no longer be used. -func (p *Processor[R]) Close() { - close(p.work) +func (p *Processor[D, R]) Close() { + close(p.prefetch) + close(p.process) p.workers.Wait() } // StartBlock dispatches transactions to the [Handler] and returns immediately. // It MUST be paired with a call to [Processor.FinishBlock], without overlap of // blocks. -func (p *Processor[R]) StartBlock(sdb *state.StateDB, rules params.Rules, b *types.Block) error { +func (p *Processor[D, R]) StartBlock(sdb *state.StateDB, rules params.Rules, b *types.Block) error { // The distribution mechanism copies the StateDB so we don't need to do it // here, but the [Handler] is called directly so we do copy. p.stateShare.distribute(sdb) @@ -191,9 +206,10 @@ func (p *Processor[R]) StartBlock(sdb *state.StateDB, rules params.Rules, b *typ txs := b.Transactions() jobs := make([]*job, 0, len(txs)) - // We can reuse the channels already in the results slice because they're - // emptied by [Processor.FinishBlock]. + // We can reuse the channels already in the data and results slices because + // they're emptied by [Processor.FinishBlock]. for i, n := len(p.results), len(txs); i < n; i++ { + p.data = append(p.data, make(chan D, 1)) p.results = append(p.results, make(chan result[R], 1)) } @@ -216,11 +232,17 @@ func (p *Processor[R]) StartBlock(sdb *state.StateDB, rules params.Rules, b *typ } } + // The first goroutine pipelines into the second, which has its results + // emptied by [Processor.FinishBlock]. The return of said function therefore + // guarantees that we haven't leaked either of these. + go func() { + for _, j := range jobs { + p.prefetch <- j + } + }() go func() { - // This goroutine is guaranteed to have returned by the time - // [Processor.FinishBlock] does. for _, j := range jobs { - p.work <- j + p.process <- j } }() return nil @@ -229,7 +251,7 @@ func (p *Processor[R]) StartBlock(sdb *state.StateDB, rules params.Rules, b *typ // FinishBlock returns the [Processor] to a state ready for the next block. A // return from FinishBlock guarantees that all dispatched work from the // respective call to [Processor.StartBlock] has been completed. -func (p *Processor[R]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.Receipts) { +func (p *Processor[D, R]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.Receipts) { for i := range len(b.Transactions()) { // Every result channel is guaranteed to have some value in its buffer // because [Processor.BeforeBlock] either sends a nil *R or it @@ -251,7 +273,7 @@ func (p *Processor[R]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.Rece // [Processor.PreprocessingGasCharge] if registered as a [vm.Preprocessor]. // The same value will be returned by each call with the same argument, such // that if R is a pointer then modifications will persist between calls. -func (p *Processor[R]) Result(i int) (R, bool) { +func (p *Processor[D, R]) Result(i int) (R, bool) { ch := p.results[i] r := <-ch defer func() { @@ -267,7 +289,7 @@ func (p *Processor[R]) Result(i int) (R, bool) { return *r.val, true } -func (p *Processor[R]) shouldProcess(tx *types.Transaction, rules params.Rules) (process bool, retErr error) { +func (p *Processor[R, D]) shouldProcess(tx *types.Transaction, rules params.Rules) (process bool, retErr error) { // An explicit 0 is necessary to avoid [Processor.PreprocessingGasCharge] // returning [ErrTxUnknown]. p.txGas[tx.Hash()] = 0 @@ -317,7 +339,7 @@ var ErrTxUnknown = errors.New("transaction unknown by parallel preprocessor") // PreprocessingGasCharge implements the [vm.Preprocessor] interface and MUST be // registered via [vm.RegisterHooks] to ensure proper gas accounting. -func (p *Processor[R]) PreprocessingGasCharge(tx common.Hash) (uint64, error) { +func (p *Processor[R, D]) PreprocessingGasCharge(tx common.Hash) (uint64, error) { g, ok := p.txGas[tx] if !ok { return 0, fmt.Errorf("%w: %v", ErrTxUnknown, tx) @@ -325,4 +347,4 @@ func (p *Processor[R]) PreprocessingGasCharge(tx common.Hash) (uint64, error) { return g, nil } -var _ vm.Preprocessor = (*Processor[struct{}])(nil) +var _ vm.Preprocessor = (*Processor[struct{}, struct{}])(nil) diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index 7ed2589c4f5..41a6d779f47 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -17,7 +17,6 @@ package parallel import ( - "bytes" "encoding/binary" "math" "math/big" @@ -47,45 +46,74 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m, goleak.IgnoreCurrent()) } -type concat struct { - headerExtra []byte - addr common.Address - stateKey common.Hash - gas uint64 +type recorder struct { + gas uint64 + addr common.Address + blockKey, prefetchKey, processKey common.Hash + + gotHeaderExtra []byte + gotBlockVal common.Hash + gotReceipts types.Receipts } -func (c *concat) BeforeBlock(_ libevm.StateReader, b *types.Block) { - c.headerExtra = slices.Clone(b.Header().Extra) +func (r *recorder) BeforeBlock(sdb libevm.StateReader, b *types.Block) { + r.gotHeaderExtra = slices.Clone(b.Header().Extra) + r.gotBlockVal = sdb.GetState(r.addr, r.blockKey) } -func (c *concat) Gas(tx *types.Transaction) (uint64, bool) { - if to := tx.To(); to != nil && *to == c.addr { - return c.gas, true +func (r *recorder) Gas(tx *types.Transaction) (uint64, bool) { + if to := tx.To(); to != nil && *to == r.addr { + return r.gas, true } return 0, false } -func concatOutput(txData []byte, state common.Hash, extra []byte) []byte { - return slices.Concat(txData, state[:], extra) +func (r *recorder) Prefetch(sdb libevm.StateReader, i int, tx *types.Transaction) common.Hash { + return sdb.GetState(r.addr, r.prefetchKey) } -func (c *concat) Process(sdb libevm.StateReader, i int, tx *types.Transaction) []byte { - return concatOutput( - tx.Data(), - sdb.GetTransientState(c.addr, c.stateKey), - c.headerExtra, - ) +type recorded struct { + HeaderExtra, TxData []byte + Block, Prefetch, Process common.Hash +} + +func (r *recorder) Process(sdb libevm.StateReader, i int, tx *types.Transaction, prefetched common.Hash) recorded { + return recorded{ + HeaderExtra: slices.Clone(r.gotHeaderExtra), + TxData: slices.Clone(tx.Data()), + Block: r.gotBlockVal, + Prefetch: prefetched, + Process: sdb.GetState(r.addr, r.processKey), + } +} + +func (r *recorded) asLog() *types.Log { + return &types.Log{ + Topics: []common.Hash{ + r.Block, r.Prefetch, r.Process, + }, + Data: slices.Concat(r.HeaderExtra, []byte("|"), r.TxData), + } +} + +func (r *recorder) AfterBlock(_ StateDB, _ *types.Block, rs types.Receipts) { + r.gotReceipts = slices.Clone(rs) } -func (*concat) AfterBlock(StateDB, *types.Block, types.Receipts) {} +func asHash(s string) (h common.Hash) { + copy(h[:], []byte(s)) + return +} func TestProcessor(t *testing.T) { - handler := &concat{ - addr: common.Address{'c', 'o', 'n', 'c', 'a', 't'}, - stateKey: common.Hash{'k', 'e', 'y'}, - gas: 1e6, + handler := &recorder{ + addr: common.Address{'c', 'o', 'n', 'c', 'a', 't'}, + gas: 1e6, + blockKey: asHash("block"), + prefetchKey: asHash("prefetch"), + processKey: asHash("process"), } - p := New(handler, 8) + p := New(handler, 8, 8) t.Cleanup(p.Close) type blockParams struct { @@ -134,8 +162,13 @@ func TestProcessor(t *testing.T) { } _, _, sdb := ethtest.NewEmptyStateDB(t) - stateVal := common.Hash{'s', 't', 'a', 't', 'e'} - sdb.SetTransientState(handler.addr, handler.stateKey, stateVal) + h := handler + blockVal := asHash("block_val") + sdb.SetState(h.addr, h.blockKey, blockVal) + prefetchVal := asHash("prefetch_val") + sdb.SetState(h.addr, h.prefetchKey, prefetchVal) + processVal := asHash("process_val") + sdb.SetState(h.addr, h.processKey, processVal) for _, tt := range tests { t.Run("", func(t *testing.T) { @@ -181,14 +214,24 @@ func TestProcessor(t *testing.T) { for i, tx := range txs { wantOK := wantProcessed[i] - var want []byte + var want recorded if wantOK { - want = concatOutput(tx.Data(), stateVal, extra) + want = recorded{ + HeaderExtra: extra, + Block: blockVal, + Prefetch: prefetchVal, + Process: processVal, + TxData: tx.Data(), + } } got, gotOK := p.Result(i) - if !bytes.Equal(got, want) || gotOK != wantOK { - t.Errorf("Result(%d) got (%#x, %t); want (%#x, %t)", i, got, gotOK, want, wantOK) + if gotOK != wantOK { + t.Errorf("Result(%d) got ok %t; want %t", i, gotOK, wantOK) + continue + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Result(%d) diff (-want +got):\n%s", i, diff) } } }) @@ -210,11 +253,11 @@ func (h *vmHooks) PreprocessingGasCharge(tx common.Hash) (uint64, error) { func TestIntegration(t *testing.T) { const handlerGas = 500 - handler := &concat{ + handler := &recorder{ addr: common.Address{'c', 'o', 'n', 'c', 'a', 't'}, gas: handlerGas, } - sut := New(handler, 8) + sut := New(handler, 8, 8) t.Cleanup(sut.Close) vm.RegisterHooks(&vmHooks{Preprocessor: sut}) @@ -232,9 +275,7 @@ func TestIntegration(t *testing.T) { if !ok { t.Errorf("no result for tx[%d] %v", txi, txh) } - sdb.AddLog(&types.Log{ - Data: got[:], - }) + sdb.AddLog(got.asLog()) return nil, nil }), }, @@ -251,7 +292,7 @@ func TestIntegration(t *testing.T) { var ( txs types.Transactions - want []*types.Receipt + want types.Receipts ) ignore := cmp.Options{ cmpopts.IgnoreFields( @@ -297,11 +338,14 @@ func TestIntegration(t *testing.T) { TransactionIndex: ui, } if addr == handler.addr { - wantR.Logs = []*types.Log{{ - TxHash: tx.Hash(), - TxIndex: ui, - Data: concatOutput(data, common.Hash{}, nil), - }} + want := (&recorded{ + TxData: tx.Data(), + }).asLog() + + want.TxHash = tx.Hash() + want.TxIndex = ui + + wantR.Logs = []*types.Log{want} } want = append(want, wantR) } @@ -310,7 +354,7 @@ func TestIntegration(t *testing.T) { require.NoError(t, sut.StartBlock(state, rules, block), "StartBlock()") pool := core.GasPool(math.MaxUint64) - var got []*types.Receipt + var receipts types.Receipts for i, tx := range txs { state.SetTxContext(tx.Hash(), i) @@ -327,11 +371,11 @@ func TestIntegration(t *testing.T) { vm.Config{}, ) require.NoError(t, err, "ApplyTransaction([%d])", i) - got = append(got, receipt) + receipts = append(receipts, receipt) } + sut.FinishBlock(state, block, receipts) - if diff := cmp.Diff(want, got, ignore); diff != "" { - t.Errorf("%T diff (-want +got):\n%s", got, diff) + if diff := cmp.Diff(want, handler.gotReceipts, ignore); diff != "" { + t.Errorf("%T diff (-want +got):\n%s", receipts, diff) } - sut.FinishBlock(state, block, got) } From e737446a3a0b5b0723cf9e4897ab1ad30fe91da8 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 31 Oct 2025 21:43:51 +0000 Subject: [PATCH 18/18] feat: `Handler.PostProcess()` method for result aggregation before end of block --- go.mod | 20 ++--- go.sum | 36 ++++----- libevm/libevm.go | 3 + libevm/precompiles/parallel/parallel.go | 77 +++++++++++++------- libevm/precompiles/parallel/parallel_test.go | 18 ++++- 5 files changed, 98 insertions(+), 56 deletions(-) diff --git a/go.mod b/go.mod index e7dc2f5b1c0..0ae1145599b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ava-labs/libevm -go 1.23 +go 1.24.8 require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 @@ -31,7 +31,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang/protobuf v1.5.3 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.2 @@ -59,20 +59,20 @@ require ( github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible github.com/status-im/keycard-go v0.2.0 github.com/stretchr/testify v1.8.4 - github.com/supranational/blst v0.3.11 + github.com/supranational/blst v0.3.14 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.25.7 go.uber.org/automaxprocs v1.5.2 go.uber.org/goleak v1.3.0 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.43.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/mod v0.14.0 - golang.org/x/sync v0.5.0 - golang.org/x/sys v0.16.0 - golang.org/x/text v0.14.0 + golang.org/x/mod v0.29.0 + golang.org/x/sync v0.17.0 + golang.org/x/sys v0.37.0 + golang.org/x/text v0.30.0 golang.org/x/time v0.3.0 - golang.org/x/tools v0.15.0 + golang.org/x/tools v0.38.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -139,7 +139,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/net v0.18.0 // indirect + golang.org/x/net v0.46.0 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect diff --git a/go.sum b/go.sum index 87821192c5e..3f876903d96 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/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.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -580,8 +580,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= -github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= +github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -635,8 +635,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -670,8 +670,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -711,8 +711,8 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -731,8 +731,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -796,8 +796,8 @@ 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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -810,8 +810,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -866,8 +866,8 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/libevm/libevm.go b/libevm/libevm.go index a429d604879..4f9e3a363b7 100644 --- a/libevm/libevm.go +++ b/libevm/libevm.go @@ -56,6 +56,9 @@ type StateReader interface { AddressInAccessList(addr common.Address) bool SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) + + TxHash() common.Hash + TxIndex() int } // AddressContext carries addresses available to contexts such as calls and diff --git a/libevm/precompiles/parallel/parallel.go b/libevm/precompiles/parallel/parallel.go index f35d0b7208a..007093146b1 100644 --- a/libevm/precompiles/parallel/parallel.go +++ b/libevm/precompiles/parallel/parallel.go @@ -21,6 +21,7 @@ package parallel import ( "errors" "fmt" + "iter" "sync" "github.com/ava-labs/libevm/common" @@ -48,12 +49,13 @@ import ( // All [libevm.StateReader] instances are opened to the state at the beginning // of the block. The [StateDB] is the same one used to execute the block, // before being committed, and MAY be written to. -type Handler[Data, Result any] interface { +type Handler[Data, Result, Aggregated any] interface { BeforeBlock(libevm.StateReader, *types.Block) Gas(*types.Transaction) (gas uint64, process bool) Prefetch(sdb libevm.StateReader, index int, tx *types.Transaction) Data Process(sdb libevm.StateReader, index int, tx *types.Transaction, data Data) Result - AfterBlock(StateDB, *types.Block, types.Receipts) + PostProcess(iter.Seq2[int, Result]) Aggregated + AfterBlock(StateDB, Aggregated, *types.Block, types.Receipts) } // StateDB is the subset of [state.StateDB] methods that MAY be called by @@ -64,14 +66,17 @@ type StateDB interface { } // A Processor orchestrates dispatch and collection of results from a [Handler]. -type Processor[D, R any] struct { - handler Handler[D, R] - workers sync.WaitGroup +type Processor[D, R, A any] struct { + handler Handler[D, R, A] + workers sync.WaitGroup + + stateShare stateDBSharer + txGas map[common.Hash]uint64 + prefetch, process chan *job data [](chan D) results [](chan result[R]) - txGas map[common.Hash]uint64 - stateShare stateDBSharer + aggregated chan A } type job struct { @@ -87,20 +92,21 @@ type result[T any] struct { // New constructs a new [Processor] with the specified number of concurrent // workers. [Processor.Close] must be called after the final call to // [Processor.FinishBlock] to avoid leaking goroutines. -func New[D, R any](h Handler[D, R], prefetchers, processors int) *Processor[D, R] { +func New[D, R, A any](h Handler[D, R, A], prefetchers, processors int) *Processor[D, R, A] { prefetchers = max(prefetchers, 1) processors = max(processors, 1) workers := prefetchers + processors - p := &Processor[D, R]{ - handler: h, - prefetch: make(chan *job), - process: make(chan *job), - txGas: make(map[common.Hash]uint64), + p := &Processor[D, R, A]{ + handler: h, stateShare: stateDBSharer{ workers: workers, nextAvailable: make(chan struct{}), }, + txGas: make(map[common.Hash]uint64), + prefetch: make(chan *job), + process: make(chan *job), + aggregated: make(chan A), } p.workers.Add(workers) // for shutdown via [Processor.Close] @@ -139,7 +145,7 @@ func (s *stateDBSharer) distribute(sdb *state.StateDB) { s.wg.Wait() } -func (p *Processor[D, R]) worker(prefetch, process chan *job) { +func (p *Processor[D, R, A]) worker(prefetch, process chan *job) { defer p.workers.Done() var sdb *state.StateDB @@ -181,7 +187,7 @@ func (p *Processor[D, R]) worker(prefetch, process chan *job) { } // Close shuts down the [Processor], after which it can no longer be used. -func (p *Processor[D, R]) Close() { +func (p *Processor[D, R, A]) Close() { close(p.prefetch) close(p.process) p.workers.Wait() @@ -190,7 +196,7 @@ func (p *Processor[D, R]) Close() { // StartBlock dispatches transactions to the [Handler] and returns immediately. // It MUST be paired with a call to [Processor.FinishBlock], without overlap of // blocks. -func (p *Processor[D, R]) StartBlock(sdb *state.StateDB, rules params.Rules, b *types.Block) error { +func (p *Processor[D, R, A]) StartBlock(sdb *state.StateDB, rules params.Rules, b *types.Block) error { // The distribution mechanism copies the StateDB so we don't need to do it // here, but the [Handler] is called directly so we do copy. p.stateShare.distribute(sdb) @@ -232,9 +238,6 @@ func (p *Processor[D, R]) StartBlock(sdb *state.StateDB, rules params.Rules, b * } } - // The first goroutine pipelines into the second, which has its results - // emptied by [Processor.FinishBlock]. The return of said function therefore - // guarantees that we haven't leaked either of these. go func() { for _, j := range jobs { p.prefetch <- j @@ -245,13 +248,33 @@ func (p *Processor[D, R]) StartBlock(sdb *state.StateDB, rules params.Rules, b * p.process <- j } }() + go func() { + n := len(b.Transactions()) + p.aggregated <- p.handler.PostProcess(p.resultIter(n)) + }() return nil } +func (p *Processor[D, R, A]) resultIter(n int) iter.Seq2[int, R] { + return func(yield func(int, R) bool) { + for i := range n { + r, ok := p.Result(i) + if !ok { + continue + } + if !yield(i, r) { + return + } + } + } +} + // FinishBlock returns the [Processor] to a state ready for the next block. A // return from FinishBlock guarantees that all dispatched work from the // respective call to [Processor.StartBlock] has been completed. -func (p *Processor[D, R]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.Receipts) { +func (p *Processor[D, R, A]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.Receipts) { + p.handler.AfterBlock(sdb, <-p.aggregated, b, rs) + for i := range len(b.Transactions()) { // Every result channel is guaranteed to have some value in its buffer // because [Processor.BeforeBlock] either sends a nil *R or it @@ -259,7 +282,6 @@ func (p *Processor[D, R]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.R tx := (<-p.results[i]).tx delete(p.txGas, tx) } - p.handler.AfterBlock(sdb, b, rs) } // Result blocks until the i'th transaction passed to [Processor.StartBlock] has @@ -271,9 +293,12 @@ func (p *Processor[D, R]) FinishBlock(sdb vm.StateDB, b *types.Block, rs types.R // Multiple calls to Result with the same argument are allowed. Callers MUST NOT // charge the gas price for preprocessing as this is handled by // [Processor.PreprocessingGasCharge] if registered as a [vm.Preprocessor]. +// // The same value will be returned by each call with the same argument, such -// that if R is a pointer then modifications will persist between calls. -func (p *Processor[D, R]) Result(i int) (R, bool) { +// that if R is a pointer then modifications will persist between calls. The +// caller does NOT have mutually exclusive access to R, which MUST carry a mutex +// if thread safety is required. +func (p *Processor[D, R, A]) Result(i int) (R, bool) { ch := p.results[i] r := <-ch defer func() { @@ -289,7 +314,7 @@ func (p *Processor[D, R]) Result(i int) (R, bool) { return *r.val, true } -func (p *Processor[R, D]) shouldProcess(tx *types.Transaction, rules params.Rules) (process bool, retErr error) { +func (p *Processor[R, D, S]) shouldProcess(tx *types.Transaction, rules params.Rules) (process bool, retErr error) { // An explicit 0 is necessary to avoid [Processor.PreprocessingGasCharge] // returning [ErrTxUnknown]. p.txGas[tx.Hash()] = 0 @@ -339,7 +364,7 @@ var ErrTxUnknown = errors.New("transaction unknown by parallel preprocessor") // PreprocessingGasCharge implements the [vm.Preprocessor] interface and MUST be // registered via [vm.RegisterHooks] to ensure proper gas accounting. -func (p *Processor[R, D]) PreprocessingGasCharge(tx common.Hash) (uint64, error) { +func (p *Processor[R, D, S]) PreprocessingGasCharge(tx common.Hash) (uint64, error) { g, ok := p.txGas[tx] if !ok { return 0, fmt.Errorf("%w: %v", ErrTxUnknown, tx) @@ -347,4 +372,4 @@ func (p *Processor[R, D]) PreprocessingGasCharge(tx common.Hash) (uint64, error) return g, nil } -var _ vm.Preprocessor = (*Processor[struct{}, struct{}])(nil) +var _ vm.Preprocessor = (*Processor[any, any, any])(nil) diff --git a/libevm/precompiles/parallel/parallel_test.go b/libevm/precompiles/parallel/parallel_test.go index 41a6d779f47..d949cbd68f8 100644 --- a/libevm/precompiles/parallel/parallel_test.go +++ b/libevm/precompiles/parallel/parallel_test.go @@ -18,6 +18,8 @@ package parallel import ( "encoding/binary" + "iter" + "maps" "math" "math/big" "math/rand/v2" @@ -54,6 +56,7 @@ type recorder struct { gotHeaderExtra []byte gotBlockVal common.Hash gotReceipts types.Receipts + gotPerTx map[int]recorded } func (r *recorder) BeforeBlock(sdb libevm.StateReader, b *types.Block) { @@ -96,8 +99,13 @@ func (r *recorded) asLog() *types.Log { } } -func (r *recorder) AfterBlock(_ StateDB, _ *types.Block, rs types.Receipts) { +func (r *recorder) PostProcess(results iter.Seq2[int, recorded]) map[int]recorded { + return maps.Collect(results) +} + +func (r *recorder) AfterBlock(_ StateDB, perTx map[int]recorded, _ *types.Block, rs types.Receipts) { r.gotReceipts = slices.Clone(rs) + r.gotPerTx = perTx } func asHash(s string) (h common.Hash) { @@ -209,8 +217,8 @@ func TestProcessor(t *testing.T) { extra := []byte("extra") block := types.NewBlock(&types.Header{Extra: extra}, txs, nil, nil, trie.NewStackTrie(nil)) require.NoError(t, p.StartBlock(sdb, rules, block), "StartBlock()") - defer p.FinishBlock(sdb, block, nil) + wantPerTx := make(map[int]recorded) for i, tx := range txs { wantOK := wantProcessed[i] @@ -223,6 +231,7 @@ func TestProcessor(t *testing.T) { Process: processVal, TxData: tx.Data(), } + wantPerTx[i] = want } got, gotOK := p.Result(i) @@ -234,6 +243,11 @@ func TestProcessor(t *testing.T) { t.Errorf("Result(%d) diff (-want +got):\n%s", i, diff) } } + + p.FinishBlock(sdb, block, nil) + if diff := cmp.Diff(wantPerTx, h.gotPerTx); diff != "" { + t.Errorf("handler.PostProcess() argument diff (-want +got):\n%s", diff) + } }) if t.Failed() {