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{
evm: args.evm,
self: contract,
callType: args.callType,
evm: args.evm,
self: contract,
callType: args.callType,
rawCaller: args.caller.Address(),
rawSelf: args.addr,
}
}

View file

@ -120,6 +120,7 @@ type statefulPrecompileOutput struct {
func (o statefulPrecompileOutput) String() string {
var lines []string
out := reflect.ValueOf(o)
FieldLoop:
for i, n := 0, out.NumField(); i < n; i++ {
name := out.Type().Field(i).Name
fld := out.Field(i).Interface()
@ -129,7 +130,12 @@ func (o statefulPrecompileOutput) String() string {
case []byte:
verb = "%#x"
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:
verb = "%d (%[2]q)"
}
@ -211,6 +217,13 @@ func TestNewStatefulPrecompile(t *testing.T) {
state.SetBalance(caller, new(uint256.Int).Not(uint256.NewInt(0)))
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 {
name string
call func() ([]byte, uint64, error)
@ -227,9 +240,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
return evm.Call(callerContract, precompile, input, gasLimit, transferValue)
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller,
Self: precompile,
Origin: eoa,
EVMSemantic: rawAddresses,
Raw: &rawAddresses,
},
wantReadOnly: false,
wantTransferValue: transferValue,
@ -242,8 +255,11 @@ func TestNewStatefulPrecompile(t *testing.T) {
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller,
Self: caller,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller,
Self: caller,
},
Raw: &rawAddresses,
},
wantReadOnly: false,
wantTransferValue: transferValue,
@ -256,8 +272,11 @@ func TestNewStatefulPrecompile(t *testing.T) {
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: eoa, // inherited from caller
Self: caller,
EVMSemantic: libevm.CallerAndSelf{
Caller: eoa, // inherited from caller
Self: caller,
},
Raw: &rawAddresses,
},
wantReadOnly: false,
wantTransferValue: uint256.NewInt(0),
@ -269,9 +288,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
return evm.StaticCall(callerContract, precompile, input, gasLimit)
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller,
Self: precompile,
Origin: eoa,
EVMSemantic: rawAddresses,
Raw: &rawAddresses,
},
wantReadOnly: true,
wantTransferValue: uint256.NewInt(0),
@ -527,7 +546,7 @@ func TestCanCreateContract(t *testing.T) {
gasUsage := rng.Uint64n(gasLimit)
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{
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) {
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",
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[:]))
},
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) {
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...)
}),
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
@ -663,8 +705,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: sut,
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: sut,
Self: dest,
},
},
Input: precompileCallData,
},
@ -675,8 +719,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // overridden by CallOption
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // overridden by CallOption
Self: dest,
},
},
Input: precompileCallData,
},
@ -686,8 +732,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // SUT runs as its own caller because of CALLCODE
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // SUT runs as its own caller because of CALLCODE
Self: dest,
},
},
Input: precompileCallData,
},
@ -698,8 +746,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // CallOption is a NOOP
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // CallOption is a NOOP
Self: dest,
},
},
Input: precompileCallData,
},
@ -709,8 +759,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // as with CALLCODE
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // as with CALLCODE
Self: dest,
},
},
Input: precompileCallData,
},
@ -721,8 +773,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // CallOption is a NOOP
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // CallOption is a NOOP
Self: dest,
},
},
Input: precompileCallData,
},
@ -732,8 +786,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: sut,
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: sut,
Self: dest,
},
},
Input: precompileCallData,
// This demonstrates that even though the precompile makes a
@ -749,8 +805,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // overridden by CallOption
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // overridden by CallOption
Self: dest,
},
},
Input: precompileCallData,
ReadOnly: true,
@ -760,6 +818,9 @@ func TestPrecompileMakeCall(t *testing.T) {
for _, tt := range tests {
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)
state, evm := ethtest.NewZeroEVM(t)
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.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
self *Contract
callType CallType
rawSelf, rawCaller common.Address
}
func (e *environment) Gas() uint64 { return e.self.Gas }
@ -79,8 +81,14 @@ func (e *environment) ReadOnly() bool {
func (e *environment) Addresses() *libevm.AddressContext {
return &libevm.AddressContext{
Origin: e.evm.Origin,
Caller: e.self.CallerAddress,
Self: e.self.Address(),
EVMSemantic: libevm.CallerAndSelf{
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
// [params.RulesHooks.CanCreateContract] hook.
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)
// 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(
"Contract creation blocked by libevm hook",
"origin", addrs.Origin,
"caller", addrs.Caller,
"contract", addrs.Self,
"caller", addrs.EVMSemantic.Caller,
"contract", addrs.EVMSemantic.Self,
"hooks", log.TypeOf(evm.chainRules.Hooks()),
"reason", err,
)

View file

@ -61,10 +61,30 @@ type StateReader interface {
// AddressContext carries addresses available to contexts such as calls and
// contract creation.
//
// With respect to contract creation, the Self address MAY be the predicted
// address of the contract about to be deployed, which may not exist yet.
// With respect to contract creation, the EVMSemantic.Self address MAY be the
// predicted address of the contract about to be deployed, which might not exist
// yet.
type AddressContext struct {
Origin common.Address // equivalent to vm.ORIGIN op code
Caller common.Address // equivalent to vm.CALLER op code
Self common.Address // equivalent to vm.ADDRESS op code
// EVMSemantic addresses are those defined by the rules of the EVM, based on
// 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.
type RulesAllowlistHooks interface {
// 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)
CanExecuteTransaction(from common.Address, to *common.Address, _ libevm.StateReader) error
}