Skip to content

Commit e35febe

Browse files
authored
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
1 parent 464de82 commit e35febe

File tree

7 files changed

+150
-4
lines changed

7 files changed

+150
-4
lines changed

core/state_transition.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,7 @@ func (st *StateTransition) preCheck() error {
364364
//
365365
// However if any consensus issue encountered, return the error directly with
366366
// nil evm execution result.
367-
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
368-
if err := st.canExecuteTransaction(); err != nil {
369-
return nil, err
370-
}
367+
func (st *StateTransition) transitionDb() (*ExecutionResult, error) {
371368
// First check this message satisfies all consensus rules before
372369
// applying the message. The rules include these clauses
373370
//

core/state_transition.libevm.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package core
1818

1919
import (
20+
"fmt"
21+
22+
"github.com/ava-labs/libevm/core/vm"
2023
"github.com/ava-labs/libevm/log"
2124
"github.com/ava-labs/libevm/params"
2225
)
@@ -27,6 +30,50 @@ func (st *StateTransition) rulesHooks() params.RulesHooks {
2730
return rules.Hooks()
2831
}
2932

33+
// NOTE: other than the final paragraph, the comment on
34+
// [StateTransition.TransitionDb] is copied, verbatim, from the upstream
35+
// version, which has been changed to [StateTransition.transitionDb] to allow
36+
// its behaviour to be augmented.
37+
38+
// Keeps the vm package imported by this specific file so VS Code can support
39+
// comments like [vm.EVM].
40+
var _ = (*vm.EVM)(nil)
41+
42+
// TransitionDb will transition the state by applying the current message and
43+
// returning the evm execution result with following fields.
44+
//
45+
// - used gas: total gas used (including gas being refunded)
46+
// - returndata: the returned data from evm
47+
// - concrete execution error: various EVM errors which abort the execution, e.g.
48+
// ErrOutOfGas, ErrExecutionReverted
49+
//
50+
// However if any consensus issue encountered, return the error directly with
51+
// nil evm execution result.
52+
//
53+
// libevm-specific behaviour: if, during execution, [vm.EVM.InvalidateExecution]
54+
// is called with a non-nil error then said error will be returned, wrapped. All
55+
// state transitions (e.g. nonce incrementing) will be reverted to a snapshot
56+
// taken before execution.
57+
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
58+
if err := st.canExecuteTransaction(); err != nil {
59+
return nil, err
60+
}
61+
62+
snap := st.state.Snapshot() // computationally cheap operation
63+
res, err := st.transitionDb() // original geth implementation
64+
65+
// [NOTE]: At the time of implementation of this libevm override, non-nil
66+
// values of `err` and `invalid` (below) are mutually exclusive. However, as
67+
// a defensive measure, we don't return early on non-nil `err` in case an
68+
// upstream update breaks this invariant.
69+
70+
if invalid := st.evm.ExecutionInvalidated(); invalid != nil {
71+
st.state.RevertToSnapshot(snap)
72+
err = fmt.Errorf("execution invalidated: %w", invalid)
73+
}
74+
return res, err
75+
}
76+
3077
// canExecuteTransaction is a convenience wrapper for calling the
3178
// [params.RulesHooks.CanExecuteTransaction] hook.
3279
func (st *StateTransition) canExecuteTransaction() error {

core/vm/contracts.libevm.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ type PrecompileEnvironment interface {
187187
BlockNumber() *big.Int
188188
BlockTime() uint64
189189

190+
// Invalidate invalidates the transaction calling this precompile.
191+
InvalidateExecution(error)
192+
190193
// Call is equivalent to [EVM.Call] except that the `caller` argument is
191194
// removed and automatically determined according to the type of call that
192195
// invoked the precompile.

core/vm/contracts.libevm_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package vm_test
1818
import (
1919
"bytes"
2020
"encoding/json"
21+
"errors"
2122
"fmt"
23+
"math"
2224
"math/big"
2325
"reflect"
2426
"strings"
@@ -302,6 +304,79 @@ func TestNewStatefulPrecompile(t *testing.T) {
302304
}
303305
}
304306

307+
func TestPrecompileInvalidatesExecution(t *testing.T) {
308+
errIfInvalidated := errors.New("execution invalidated")
309+
inputToInvalidate := []byte("invalidate")
310+
run := func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) {
311+
if bytes.Equal(input, inputToInvalidate) {
312+
env.InvalidateExecution(errIfInvalidated)
313+
}
314+
return []byte{}, nil
315+
}
316+
317+
precompile := common.HexToAddress("60C0DE") // GO CODE
318+
hooks := &hookstest.Stub{
319+
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
320+
precompile: vm.NewStatefulPrecompile(run),
321+
},
322+
}
323+
hooks.Register(t)
324+
325+
// The EVM instance MUST be reused across all tests to ensure that
326+
// [vm.EVM.Reset] undoes any invalidation.
327+
stateDB, evm := ethtest.NewZeroEVM(t)
328+
329+
tests := []struct {
330+
name string
331+
nonce uint64
332+
input []byte
333+
wantErr error
334+
}{
335+
{
336+
name: "not_invalidating",
337+
input: []byte{},
338+
nonce: 0,
339+
wantErr: nil,
340+
},
341+
{
342+
name: "invalidating",
343+
nonce: 1,
344+
input: inputToInvalidate,
345+
wantErr: errIfInvalidated,
346+
},
347+
{
348+
// Tests that:
349+
// (a) [vm.EVM.Reset] undoes the previous invalidation; and
350+
// (b) Invalidation reverted state changes, as seen by the nonce.
351+
name: "evm_reset_not_invalidating_after_invalid",
352+
input: []byte{},
353+
nonce: 1, // unchanged because the last was invalidated
354+
wantErr: nil,
355+
},
356+
}
357+
358+
for _, tt := range tests {
359+
t.Run(tt.name, func(t *testing.T) {
360+
msg := &core.Message{
361+
Nonce: tt.nonce,
362+
Data: tt.input,
363+
364+
// Common across all txs
365+
To: &precompile,
366+
GasLimit: 1e6, // arbitrary but sufficiently high
367+
GasPrice: big.NewInt(0),
368+
Value: big.NewInt(0),
369+
}
370+
371+
evm.Reset(core.NewEVMTxContext(msg), stateDB)
372+
373+
gas := core.GasPool(math.MaxUint64)
374+
_, err := core.ApplyMessage(evm, msg, &gas)
375+
require.ErrorIs(t, err, tt.wantErr, "core.ApplyMessage()")
376+
})
377+
}
378+
}
379+
305380
func TestInheritReadOnly(t *testing.T) {
306381
// The regular test of stateful precompiles only checks the read-only state
307382
// when called directly via vm.EVM.*Call*() methods. That approach will not

core/vm/environment.libevm.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ func (e *environment) IncomingCallType() CallType { return e.callType }
4949
func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) }
5050
func (e *environment) BlockTime() uint64 { return e.evm.Context.Time }
5151

52+
func (e *environment) InvalidateExecution(err error) { e.evm.InvalidateExecution(err) }
53+
5254
func (e *environment) refundGas(add uint64) error {
5355
gas, overflow := math.SafeAdd(e.self.Gas, add)
5456
if overflow {

core/vm/evm.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ type EVM struct {
128128
// available gas is calculated in gasCall* according to the 63/64 rule and later
129129
// applied in opCall*.
130130
callGasTemp uint64
131+
132+
// libevm
133+
executionInvalidated error // see [EVM.InvalidateExecution]
131134
}
132135

133136
// 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
160163
// Reset resets the EVM with a new transaction context.Reset
161164
// This is not threadsafe and should only be done very cautiously.
162165
func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) {
166+
evm.executionInvalidated = nil // see [EVM.InvalidateExecution]
163167
evm.TxContext, evm.StateDB = evm.overrideEVMResetArgs(txCtx, statedb)
164168
}
165169

core/vm/evm.libevm.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,21 @@ func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Ad
4343

4444
return gas, err
4545
}
46+
47+
// InvalidateExecution sets the error that will be returned by
48+
// [EVM.ExecutionInvalidated] for the length of the current transaction; i.e.
49+
// until [EVM.Reset] is called. This is honoured by state-transition logic to
50+
// render the execution itself void (as against reverted).
51+
//
52+
// This method MUST NOT be exposed in a manner that allows contracts to set
53+
// the error; it MAY be exposed to precompiles.
54+
func (evm *EVM) InvalidateExecution(err error) {
55+
evm.executionInvalidated = err
56+
}
57+
58+
// ExecutionInvalidated returns the last value passed to
59+
// [EVM.InvalidateExecution] or nil if no such call has occurred or if
60+
// [EVM.Reset] has been called.
61+
func (evm *EVM) ExecutionInvalidated() error {
62+
return evm.executionInvalidated
63+
}

0 commit comments

Comments
 (0)