mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 10:50:44 +00:00
feat: enable invalidating txs (#208)
## Why this should be merged In case a tx's execution (specifically a registered precompile) should be invalidated, this allows the EVM to find this error. ## How this works Adds a setter and getter that can be called from a precompile. ## How this was tested UT
This commit is contained in:
parent
464de82910
commit
e35febe777
7 changed files with 150 additions and 4 deletions
|
|
@ -364,10 +364,7 @@ func (st *StateTransition) preCheck() error {
|
||||||
//
|
//
|
||||||
// However if any consensus issue encountered, return the error directly with
|
// However if any consensus issue encountered, return the error directly with
|
||||||
// nil evm execution result.
|
// nil evm execution result.
|
||||||
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
|
func (st *StateTransition) transitionDb() (*ExecutionResult, error) {
|
||||||
if err := st.canExecuteTransaction(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// First check this message satisfies all consensus rules before
|
// First check this message satisfies all consensus rules before
|
||||||
// applying the message. The rules include these clauses
|
// applying the message. The rules include these clauses
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ava-labs/libevm/core/vm"
|
||||||
"github.com/ava-labs/libevm/log"
|
"github.com/ava-labs/libevm/log"
|
||||||
"github.com/ava-labs/libevm/params"
|
"github.com/ava-labs/libevm/params"
|
||||||
)
|
)
|
||||||
|
|
@ -27,6 +30,50 @@ func (st *StateTransition) rulesHooks() params.RulesHooks {
|
||||||
return rules.Hooks()
|
return rules.Hooks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: other than the final paragraph, the comment on
|
||||||
|
// [StateTransition.TransitionDb] is copied, verbatim, from the upstream
|
||||||
|
// version, which has been changed to [StateTransition.transitionDb] to allow
|
||||||
|
// its behaviour to be augmented.
|
||||||
|
|
||||||
|
// Keeps the vm package imported by this specific file so VS Code can support
|
||||||
|
// comments like [vm.EVM].
|
||||||
|
var _ = (*vm.EVM)(nil)
|
||||||
|
|
||||||
|
// TransitionDb will transition the state by applying the current message and
|
||||||
|
// returning the evm execution result with following fields.
|
||||||
|
//
|
||||||
|
// - used gas: total gas used (including gas being refunded)
|
||||||
|
// - returndata: the returned data from evm
|
||||||
|
// - concrete execution error: various EVM errors which abort the execution, e.g.
|
||||||
|
// ErrOutOfGas, ErrExecutionReverted
|
||||||
|
//
|
||||||
|
// However if any consensus issue encountered, return the error directly with
|
||||||
|
// nil evm execution result.
|
||||||
|
//
|
||||||
|
// libevm-specific behaviour: if, during execution, [vm.EVM.InvalidateExecution]
|
||||||
|
// is called with a non-nil error then said error will be returned, wrapped. All
|
||||||
|
// state transitions (e.g. nonce incrementing) will be reverted to a snapshot
|
||||||
|
// taken before execution.
|
||||||
|
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
|
||||||
|
if err := st.canExecuteTransaction(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
snap := st.state.Snapshot() // computationally cheap operation
|
||||||
|
res, err := st.transitionDb() // original geth implementation
|
||||||
|
|
||||||
|
// [NOTE]: At the time of implementation of this libevm override, non-nil
|
||||||
|
// values of `err` and `invalid` (below) are mutually exclusive. However, as
|
||||||
|
// a defensive measure, we don't return early on non-nil `err` in case an
|
||||||
|
// upstream update breaks this invariant.
|
||||||
|
|
||||||
|
if invalid := st.evm.ExecutionInvalidated(); invalid != nil {
|
||||||
|
st.state.RevertToSnapshot(snap)
|
||||||
|
err = fmt.Errorf("execution invalidated: %w", invalid)
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
// canExecuteTransaction is a convenience wrapper for calling the
|
// canExecuteTransaction is a convenience wrapper for calling the
|
||||||
// [params.RulesHooks.CanExecuteTransaction] hook.
|
// [params.RulesHooks.CanExecuteTransaction] hook.
|
||||||
func (st *StateTransition) canExecuteTransaction() error {
|
func (st *StateTransition) canExecuteTransaction() error {
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,9 @@ type PrecompileEnvironment interface {
|
||||||
BlockNumber() *big.Int
|
BlockNumber() *big.Int
|
||||||
BlockTime() uint64
|
BlockTime() uint64
|
||||||
|
|
||||||
|
// Invalidate invalidates the transaction calling this precompile.
|
||||||
|
InvalidateExecution(error)
|
||||||
|
|
||||||
// Call is equivalent to [EVM.Call] except that the `caller` argument is
|
// Call is equivalent to [EVM.Call] except that the `caller` argument is
|
||||||
// removed and automatically determined according to the type of call that
|
// removed and automatically determined according to the type of call that
|
||||||
// invoked the precompile.
|
// invoked the precompile.
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ package vm_test
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -302,6 +304,79 @@ func TestNewStatefulPrecompile(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrecompileInvalidatesExecution(t *testing.T) {
|
||||||
|
errIfInvalidated := errors.New("execution invalidated")
|
||||||
|
inputToInvalidate := []byte("invalidate")
|
||||||
|
run := func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) {
|
||||||
|
if bytes.Equal(input, inputToInvalidate) {
|
||||||
|
env.InvalidateExecution(errIfInvalidated)
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
precompile := common.HexToAddress("60C0DE") // GO CODE
|
||||||
|
hooks := &hookstest.Stub{
|
||||||
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
||||||
|
precompile: vm.NewStatefulPrecompile(run),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
hooks.Register(t)
|
||||||
|
|
||||||
|
// The EVM instance MUST be reused across all tests to ensure that
|
||||||
|
// [vm.EVM.Reset] undoes any invalidation.
|
||||||
|
stateDB, evm := ethtest.NewZeroEVM(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nonce uint64
|
||||||
|
input []byte
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "not_invalidating",
|
||||||
|
input: []byte{},
|
||||||
|
nonce: 0,
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalidating",
|
||||||
|
nonce: 1,
|
||||||
|
input: inputToInvalidate,
|
||||||
|
wantErr: errIfInvalidated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Tests that:
|
||||||
|
// (a) [vm.EVM.Reset] undoes the previous invalidation; and
|
||||||
|
// (b) Invalidation reverted state changes, as seen by the nonce.
|
||||||
|
name: "evm_reset_not_invalidating_after_invalid",
|
||||||
|
input: []byte{},
|
||||||
|
nonce: 1, // unchanged because the last was invalidated
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
msg := &core.Message{
|
||||||
|
Nonce: tt.nonce,
|
||||||
|
Data: tt.input,
|
||||||
|
|
||||||
|
// Common across all txs
|
||||||
|
To: &precompile,
|
||||||
|
GasLimit: 1e6, // arbitrary but sufficiently high
|
||||||
|
GasPrice: big.NewInt(0),
|
||||||
|
Value: big.NewInt(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
evm.Reset(core.NewEVMTxContext(msg), stateDB)
|
||||||
|
|
||||||
|
gas := core.GasPool(math.MaxUint64)
|
||||||
|
_, err := core.ApplyMessage(evm, msg, &gas)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr, "core.ApplyMessage()")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInheritReadOnly(t *testing.T) {
|
func TestInheritReadOnly(t *testing.T) {
|
||||||
// The regular test of stateful precompiles only checks the read-only state
|
// The regular test of stateful precompiles only checks the read-only state
|
||||||
// when called directly via vm.EVM.*Call*() methods. That approach will not
|
// when called directly via vm.EVM.*Call*() methods. That approach will not
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ func (e *environment) IncomingCallType() CallType { return e.callType }
|
||||||
func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) }
|
func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) }
|
||||||
func (e *environment) BlockTime() uint64 { return e.evm.Context.Time }
|
func (e *environment) BlockTime() uint64 { return e.evm.Context.Time }
|
||||||
|
|
||||||
|
func (e *environment) InvalidateExecution(err error) { e.evm.InvalidateExecution(err) }
|
||||||
|
|
||||||
func (e *environment) refundGas(add uint64) error {
|
func (e *environment) refundGas(add uint64) error {
|
||||||
gas, overflow := math.SafeAdd(e.self.Gas, add)
|
gas, overflow := math.SafeAdd(e.self.Gas, add)
|
||||||
if overflow {
|
if overflow {
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,9 @@ type EVM struct {
|
||||||
// available gas is calculated in gasCall* according to the 63/64 rule and later
|
// available gas is calculated in gasCall* according to the 63/64 rule and later
|
||||||
// applied in opCall*.
|
// applied in opCall*.
|
||||||
callGasTemp uint64
|
callGasTemp uint64
|
||||||
|
|
||||||
|
// libevm
|
||||||
|
executionInvalidated error // see [EVM.InvalidateExecution]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEVM returns a new EVM. The returned EVM is not thread safe and should
|
// NewEVM returns a new EVM. The returned EVM is not thread safe and should
|
||||||
|
|
@ -160,6 +163,7 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig
|
||||||
// Reset resets the EVM with a new transaction context.Reset
|
// Reset resets the EVM with a new transaction context.Reset
|
||||||
// This is not threadsafe and should only be done very cautiously.
|
// This is not threadsafe and should only be done very cautiously.
|
||||||
func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) {
|
func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) {
|
||||||
|
evm.executionInvalidated = nil // see [EVM.InvalidateExecution]
|
||||||
evm.TxContext, evm.StateDB = evm.overrideEVMResetArgs(txCtx, statedb)
|
evm.TxContext, evm.StateDB = evm.overrideEVMResetArgs(txCtx, statedb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,21 @@ func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Ad
|
||||||
|
|
||||||
return gas, err
|
return gas, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// render the execution itself void (as against reverted).
|
||||||
|
//
|
||||||
|
// This method MUST NOT be exposed in a manner that allows contracts to set
|
||||||
|
// the error; it MAY be exposed to precompiles.
|
||||||
|
func (evm *EVM) InvalidateExecution(err error) {
|
||||||
|
evm.executionInvalidated = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionInvalidated returns the last value passed to
|
||||||
|
// [EVM.InvalidateExecution] or nil if no such call has occurred or if
|
||||||
|
// [EVM.Reset] has been called.
|
||||||
|
func (evm *EVM) ExecutionInvalidated() error {
|
||||||
|
return evm.executionInvalidated
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue