feat!: disambiguate EVM-semantic and raw caller/self addresses for precompiles (#211)

## Why this should be merged

Provides precompiles with unambiguous access to contextual addresses,
without the consumer needing to understand how they change under
different call types.

## How this works

The `libevm.AddressContext` type, which used to carry 3 addresses, now
provides different versions of `Caller` and `Self`. The EVM-semantic
versions are as defined by the rules of the EVM (and available before
this change). The raw versions are the unmodified caller and self.

## How this was tested

Extension of existing UTs to include raw addresses in addition to
existing, EVM-semantic ones.
This commit is contained in:
Arran Schlosberg 2025-08-07 23:48:32 +01:00 committed by GitHub
parent e35febe777
commit e7035f19ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 144 additions and 67 deletions

View file

@ -223,9 +223,11 @@ func (args *evmCallArgs) env() *environment {
} }
return &environment{ return &environment{
evm: args.evm, evm: args.evm,
self: contract, self: contract,
callType: args.callType, callType: args.callType,
rawCaller: args.caller.Address(),
rawSelf: args.addr,
} }
} }

View file

@ -120,6 +120,7 @@ type statefulPrecompileOutput struct {
func (o statefulPrecompileOutput) String() string { func (o statefulPrecompileOutput) String() string {
var lines []string var lines []string
out := reflect.ValueOf(o) out := reflect.ValueOf(o)
FieldLoop:
for i, n := 0, out.NumField(); i < n; i++ { for i, n := 0, out.NumField(); i < n; i++ {
name := out.Type().Field(i).Name name := out.Type().Field(i).Name
fld := out.Field(i).Interface() fld := out.Field(i).Interface()
@ -129,7 +130,12 @@ func (o statefulPrecompileOutput) String() string {
case []byte: case []byte:
verb = "%#x" verb = "%#x"
case *libevm.AddressContext: case *libevm.AddressContext:
verb = "%+v" lines = append(
lines,
fmt.Sprintf("EVMSemantic addresses: %+v", o.Addresses.EVMSemantic),
fmt.Sprintf("Raw addresses: %+v", o.Addresses.Raw),
)
continue FieldLoop
case vm.CallType: case vm.CallType:
verb = "%d (%[2]q)" verb = "%d (%[2]q)"
} }
@ -211,6 +217,13 @@ func TestNewStatefulPrecompile(t *testing.T) {
state.SetBalance(caller, new(uint256.Int).Not(uint256.NewInt(0))) state.SetBalance(caller, new(uint256.Int).Not(uint256.NewInt(0)))
evm.Origin = eoa evm.Origin = eoa
// By definition, the raw caller and self are the same for every test case,
// regardless of the incoming call type.
rawAddresses := libevm.CallerAndSelf{
Caller: caller,
Self: precompile,
}
tests := []struct { tests := []struct {
name string name string
call func() ([]byte, uint64, error) call func() ([]byte, uint64, error)
@ -227,9 +240,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
return evm.Call(callerContract, precompile, input, gasLimit, transferValue) return evm.Call(callerContract, precompile, input, gasLimit, transferValue)
}, },
wantAddresses: &libevm.AddressContext{ wantAddresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, EVMSemantic: rawAddresses,
Self: precompile, Raw: &rawAddresses,
}, },
wantReadOnly: false, wantReadOnly: false,
wantTransferValue: transferValue, wantTransferValue: transferValue,
@ -242,8 +255,11 @@ func TestNewStatefulPrecompile(t *testing.T) {
}, },
wantAddresses: &libevm.AddressContext{ wantAddresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, EVMSemantic: libevm.CallerAndSelf{
Self: caller, Caller: caller,
Self: caller,
},
Raw: &rawAddresses,
}, },
wantReadOnly: false, wantReadOnly: false,
wantTransferValue: transferValue, wantTransferValue: transferValue,
@ -256,8 +272,11 @@ func TestNewStatefulPrecompile(t *testing.T) {
}, },
wantAddresses: &libevm.AddressContext{ wantAddresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: eoa, // inherited from caller EVMSemantic: libevm.CallerAndSelf{
Self: caller, Caller: eoa, // inherited from caller
Self: caller,
},
Raw: &rawAddresses,
}, },
wantReadOnly: false, wantReadOnly: false,
wantTransferValue: uint256.NewInt(0), wantTransferValue: uint256.NewInt(0),
@ -269,9 +288,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
return evm.StaticCall(callerContract, precompile, input, gasLimit) return evm.StaticCall(callerContract, precompile, input, gasLimit)
}, },
wantAddresses: &libevm.AddressContext{ wantAddresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, EVMSemantic: rawAddresses,
Self: precompile, Raw: &rawAddresses,
}, },
wantReadOnly: true, wantReadOnly: true,
wantTransferValue: uint256.NewInt(0), wantTransferValue: uint256.NewInt(0),
@ -527,7 +546,7 @@ func TestCanCreateContract(t *testing.T) {
gasUsage := rng.Uint64n(gasLimit) gasUsage := rng.Uint64n(gasLimit)
makeErr := func(cc *libevm.AddressContext, stateVal common.Hash) error { makeErr := func(cc *libevm.AddressContext, stateVal common.Hash) error {
return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.Caller, cc.Self, stateVal) return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.EVMSemantic.Caller, cc.EVMSemantic.Self, stateVal)
} }
hooks := &hookstest.Stub{ hooks := &hookstest.Stub{
CanCreateContractFn: func(cc *libevm.AddressContext, gas uint64, s libevm.StateReader) (uint64, error) { CanCreateContractFn: func(cc *libevm.AddressContext, gas uint64, s libevm.StateReader) (uint64, error) {
@ -555,14 +574,34 @@ func TestCanCreateContract(t *testing.T) {
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) { create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
return evm.Create(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0)) return evm.Create(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0))
}, },
wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create}, value), wantErr: makeErr(
&libevm.AddressContext{
Origin: origin,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller,
Self: create,
},
// `Raw` is documented as always being nil.
},
value,
),
}, },
{ {
name: "Create2", name: "Create2",
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) { create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
return evm.Create2(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:])) return evm.Create2(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:]))
}, },
wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create2}, value), wantErr: makeErr(
&libevm.AddressContext{
Origin: origin,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller,
Self: create2,
},
// As above re `Raw` always being nil.
},
value,
),
}, },
} }
@ -630,7 +669,10 @@ func TestPrecompileMakeCall(t *testing.T) {
if bytes.Equal(input, unsafeCallerProxyOptSentinel) { if bytes.Equal(input, unsafeCallerProxyOptSentinel) {
opts = append(opts, vm.WithUNSAFECallerAddressProxying()) opts = append(opts, vm.WithUNSAFECallerAddressProxying())
} }
// We are ultimately testing env.Call(), hence why this is the SUT. // We are ultimately testing env.Call(), hence why this is the
// SUT. If this is ever extended to include DELEGATECALL or
// CALLCODE then the expected [libevm.AddressContext.Raw] values
// of the tests cases also need to change.
return env.Call(dest, precompileCallData, env.Gas(), uint256.NewInt(0), opts...) return env.Call(dest, precompileCallData, env.Gas(), uint256.NewInt(0), opts...)
}), }),
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
@ -663,8 +705,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: sut, EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: sut,
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
}, },
@ -675,8 +719,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, // overridden by CallOption EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: caller, // overridden by CallOption
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
}, },
@ -686,8 +732,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, // SUT runs as its own caller because of CALLCODE EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: caller, // SUT runs as its own caller because of CALLCODE
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
}, },
@ -698,8 +746,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, // CallOption is a NOOP EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: caller, // CallOption is a NOOP
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
}, },
@ -709,8 +759,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, // as with CALLCODE EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: caller, // as with CALLCODE
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
}, },
@ -721,8 +773,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, // CallOption is a NOOP EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: caller, // CallOption is a NOOP
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
}, },
@ -732,8 +786,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: sut, EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: sut,
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
// This demonstrates that even though the precompile makes a // This demonstrates that even though the precompile makes a
@ -749,8 +805,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{ want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{ Addresses: &libevm.AddressContext{
Origin: eoa, Origin: eoa,
Caller: caller, // overridden by CallOption EVMSemantic: libevm.CallerAndSelf{
Self: dest, Caller: caller, // overridden by CallOption
Self: dest,
},
}, },
Input: precompileCallData, Input: precompileCallData,
ReadOnly: true, ReadOnly: true,
@ -760,6 +818,9 @@ func TestPrecompileMakeCall(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.incomingCallType.String(), func(t *testing.T) { t.Run(tt.incomingCallType.String(), func(t *testing.T) {
// From the perspective of `dest` after a CALL from `sut`.
tt.want.Addresses.Raw = &tt.want.Addresses.EVMSemantic
t.Logf("calldata = %q", tt.eoaTxCallData) t.Logf("calldata = %q", tt.eoaTxCallData)
state, evm := ethtest.NewZeroEVM(t) state, evm := ethtest.NewZeroEVM(t)
evm.Origin = eoa evm.Origin = eoa
@ -816,26 +877,3 @@ func TestPrecompileCallWithTracer(t *testing.T) {
require.NoErrorf(t, json.Unmarshal(gotJSON, &got), "json.Unmarshal(%T.GetResult(), %T)", tracer, &got) require.NoErrorf(t, json.Unmarshal(gotJSON, &got), "json.Unmarshal(%T.GetResult(), %T)", tracer, &got)
require.Equal(t, value, got[contract].Storage[zeroHash], "value loaded with SLOAD") require.Equal(t, value, got[contract].Storage[zeroHash], "value loaded with SLOAD")
} }
//nolint:testableexamples // Including output would only make the example more complicated and hide the true intent
func ExamplePrecompileEnvironment() {
// To determine the actual caller of a precompile, as against the effective
// caller (under EVM rules, as exposed by `Addresses().Caller`):
actualCaller := func(env vm.PrecompileEnvironment) common.Address {
if env.IncomingCallType() == vm.DelegateCall {
// DelegateCall acts as if it were its own caller.
return env.Addresses().Self
}
// CallCode could return either `Self` or `Caller` as it acts as its
// caller but doesn't inherit the caller's caller as DelegateCall does.
// Having it handled here is arbitrary from a behavioural perspective
// and is done only to simplify the code.
//
// Call and StaticCall don't affect self/caller semantics in any way.
return env.Addresses().Caller
}
// actualCaller would typically be a top-level function. It's only a
// variable to include it in this example function.
_ = actualCaller
}

View file

@ -36,6 +36,8 @@ type environment struct {
evm *EVM evm *EVM
self *Contract self *Contract
callType CallType callType CallType
rawSelf, rawCaller common.Address
} }
func (e *environment) Gas() uint64 { return e.self.Gas } func (e *environment) Gas() uint64 { return e.self.Gas }
@ -79,8 +81,14 @@ func (e *environment) ReadOnly() bool {
func (e *environment) Addresses() *libevm.AddressContext { func (e *environment) Addresses() *libevm.AddressContext {
return &libevm.AddressContext{ return &libevm.AddressContext{
Origin: e.evm.Origin, Origin: e.evm.Origin,
Caller: e.self.CallerAddress, EVMSemantic: libevm.CallerAndSelf{
Self: e.self.Address(), Caller: e.self.CallerAddress,
Self: e.self.Address(),
},
Raw: &libevm.CallerAndSelf{
Caller: e.rawCaller,
Self: e.rawSelf,
},
} }
} }

View file

@ -25,7 +25,15 @@ import (
// canCreateContract is a convenience wrapper for calling the // canCreateContract is a convenience wrapper for calling the
// [params.RulesHooks.CanCreateContract] hook. // [params.RulesHooks.CanCreateContract] hook.
func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Address, gas uint64) (remainingGas uint64, _ error) { func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Address, gas uint64) (remainingGas uint64, _ error) {
addrs := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: contractToCreate} addrs := &libevm.AddressContext{
Origin: evm.Origin,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller.Address(),
Self: contractToCreate,
},
// The "raw" caller isn't guaranteed to be known if the caller is a
// delegate so the `Raw` field is documented as always being nil.
}
gas, err := evm.chainRules.Hooks().CanCreateContract(addrs, gas, evm.StateDB) gas, err := evm.chainRules.Hooks().CanCreateContract(addrs, gas, evm.StateDB)
// NOTE that this block only performs logging and that all paths propagate // NOTE that this block only performs logging and that all paths propagate
@ -34,8 +42,8 @@ func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Ad
log.Debug( log.Debug(
"Contract creation blocked by libevm hook", "Contract creation blocked by libevm hook",
"origin", addrs.Origin, "origin", addrs.Origin,
"caller", addrs.Caller, "caller", addrs.EVMSemantic.Caller,
"contract", addrs.Self, "contract", addrs.EVMSemantic.Self,
"hooks", log.TypeOf(evm.chainRules.Hooks()), "hooks", log.TypeOf(evm.chainRules.Hooks()),
"reason", err, "reason", err,
) )

View file

@ -61,10 +61,30 @@ type StateReader interface {
// AddressContext carries addresses available to contexts such as calls and // AddressContext carries addresses available to contexts such as calls and
// contract creation. // contract creation.
// //
// With respect to contract creation, the Self address MAY be the predicted // With respect to contract creation, the EVMSemantic.Self address MAY be the
// address of the contract about to be deployed, which may not exist yet. // predicted address of the contract about to be deployed, which might not exist
// yet.
type AddressContext struct { type AddressContext struct {
Origin common.Address // equivalent to vm.ORIGIN op code Origin common.Address // equivalent to vm.ORIGIN op code
Caller common.Address // equivalent to vm.CALLER op code // EVMSemantic addresses are those defined by the rules of the EVM, based on
Self common.Address // equivalent to vm.ADDRESS op code // the type of call made to a contract; i.e. the addresses pushed to the
// stack by the vm.CALLER and vm.SELF op codes, respectively.
EVMSemantic CallerAndSelf
// Raw addresses are those that would be available to a contract under a
// standard CALL; i.e. not interpreted according EVM rules. They are the
// "intuitive" addresses such that the `Caller` is the account that called
// `Self` even if it did so via DELEGATECALL or CALLCODE (in which cases
// `Raw` and `EVMSemantic` would differ).
//
// Raw MUST NOT be nil when returned to a precompile implementation but MAY
// be nil in other situations (e.g. hooks), which MUST document behaviour on
// a case-by-case basis.
Raw *CallerAndSelf
}
// CallerAndSelf carries said addresses for use in an [AddressContext], where
// the definitions of `Caller` and `Self` are defined based on context.
type CallerAndSelf struct {
Caller common.Address
Self common.Address
} }

View file

@ -67,7 +67,8 @@ type RulesHooks interface {
// by returning a nil (allowed) or non-nil (blocked) error. // by returning a nil (allowed) or non-nil (blocked) error.
type RulesAllowlistHooks interface { type RulesAllowlistHooks interface {
// CanCreateContract is called after the deployer's nonce is incremented but // CanCreateContract is called after the deployer's nonce is incremented but
// before all other state-modifying actions. // before all other state-modifying actions. The [libevm.AddressContext.Raw]
// field will always be nil.
CanCreateContract(_ *libevm.AddressContext, gas uint64, _ libevm.StateReader) (gasRemaining uint64, _ error) CanCreateContract(_ *libevm.AddressContext, gas uint64, _ libevm.StateReader) (gasRemaining uint64, _ error)
CanExecuteTransaction(from common.Address, to *common.Address, _ libevm.StateReader) error CanExecuteTransaction(from common.Address, to *common.Address, _ libevm.StateReader) error
} }