Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: implement AUTH and AUTHCALL opcodes (EIP: 3074) #1226

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 130 additions & 0 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package core

import (
"bytes"
"errors"
"fmt"
"math/big"
Expand Down Expand Up @@ -5100,3 +5101,132 @@ func TestEIP3651(t *testing.T) {
t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual)
}
}

func TestEIP3074(t *testing.T) {
var (
// Invoker contract (which uses `auth` and `authcall` and calls destination contract)
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
// Destination contract which will be ultimately called
bb = common.HexToAddress("0x000000000000000000000000000000000000bbbb")
engine = beacon.NewFaker()

// A sender who makes transactions, has some funds
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
addr = crypto.PubkeyToAddress(key.PublicKey)
funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether))
config = *params.AllEthashProtocolChanges
gspec = &Genesis{
Config: &config,
Alloc: GenesisAlloc{
addr: {Balance: funds},
aa: {
Code: nil, // added below
Nonce: 0,
Balance: big.NewInt(0),
},
bb: {
Code: []byte{
byte(vm.CALLER), // pushes the caller to stack [caller]
byte(vm.PUSH0), // pushes 0 to stack [caller, 0]
byte(vm.SSTORE), // stores caller in slot 0 (to verify caller later)
byte(vm.STOP),
},
Nonce: 0,
Balance: big.NewInt(0),
},
},
}
)

invoker := []byte{
// for `auth`, signature needs to be in memory (which will be passed via calldata)
// copy the signature from calldata to memory
byte(vm.CALLDATASIZE), // pushes calldata size to stack [len(calldata)] (size)
byte(vm.PUSH0), // pushes 0 to stack [len(calldata), 0] (offset)
byte(vm.PUSH0), // pushes 0 to stack [len(calldata), 0, 0] (destOffset)
byte(vm.CALLDATACOPY), // copy calldata to memory (based on destOffset, offset, size above)

// set up auth
byte(vm.CALLDATASIZE), // pushes calldata size to stack [len(calldata)] (length)
byte(vm.PUSH0), // pushes 0 to stack [len(calldata), 0] (offset)
}
// push authority to stack [len(calldata), 0, authority] (authority)
invoker = append(invoker, append([]byte{byte(vm.PUSH20)}, addr.Bytes()...)...)
invoker = append(invoker, []byte{
byte(vm.AUTH), // call auth (based on authority, offset, length above)
byte(vm.POP), // pop result of auth

// execute authcall
byte(vm.PUSH0), // [0] (retLength)
byte(vm.DUP1), // [0, 0] (retOffset)
byte(vm.DUP1), // [0, 0, 0] (argsLength)
byte(vm.DUP1), // [0, 0, 0, 0] (argsOffset)
byte(vm.DUP1), // [0, 0, 0, 0, 0] (value)
byte(vm.PUSH2), // push the destination contract address
byte(0xbb), // [0, 0, 0, 0, 0, 0xbb]
byte(0xbb), // [0, 0, 0, 0, 0, 0xbbbb] (addr)
byte(vm.GAS), // [0, 0, 0, 0, 0, 0xbbbb, gas] (gas)
byte(vm.AUTHCALL), // call authcall (based on 7 values above)
byte(vm.STOP),
}...,
)

// Set the invoker's code.
if entry := gspec.Alloc[aa]; true {
entry.Code = invoker
gspec.Alloc[aa] = entry
}

gspec.Config.BerlinBlock = common.Big0
gspec.Config.LondonBlock = common.Big0
gspec.Config.TerminalTotalDifficulty = common.Big0
gspec.Config.TerminalTotalDifficultyPassed = true
gspec.Config.ShanghaiBlock = common.Big0
gspec.Config.CancunBlock = common.Big0
gspec.Config.PragueBlock = common.Big0
signer := types.LatestSigner(gspec.Config)

_, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) {
commit := common.Hash{0x42}
msg := []byte{params.AuthMagic}
msg = append(msg, common.LeftPadBytes(gspec.Config.ChainID.Bytes(), 32)...)
msg = append(msg, common.LeftPadBytes(big.NewInt(1).Bytes(), 32)...) // nonce: 1
msg = append(msg, common.LeftPadBytes(aa.Bytes(), 32)...)
msg = append(msg, commit.Bytes()...)
msg = crypto.Keccak256(msg)

sig, _ := crypto.Sign(msg, key)
sig = append([]byte{sig[len(sig)-1]}, sig[0:len(sig)-1]...)

// Send a tx to the invoker contract (which will perform auth and authcall)
txdata := &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 0,
To: &aa,
Gas: 500000,
GasFeeCap: newGwei(5),
GasTipCap: big.NewInt(2),
AccessList: nil,
Data: append(sig, commit.Bytes()...),
}
tx := types.NewTx(txdata)
tx, _ = types.SignTx(tx, signer, key)

b.AddTx(tx)
})
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{Tracer: logger.NewMarkdownLogger(&logger.Config{}, os.Stderr)}, nil, nil, nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
defer chain.Stop()
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}

// Verify authcall worked correctly.
state, _ := chain.State()
got := state.GetState(bb, common.Hash{})
if want := common.LeftPadBytes(addr.Bytes(), 32); !bytes.Equal(got.Bytes(), want) {
t.Fatalf("incorrect sender in authcall: got %s, want %s", got.Hex(), common.Bytes2Hex(want))
}
}
113 changes: 113 additions & 0 deletions core/vm/eips.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"sort"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
Expand Down Expand Up @@ -333,3 +334,115 @@ func enable6780(jt *JumpTable) {
maxStack: maxStack(1, 0),
}
}

func enable3074(jt *JumpTable) {
jt[AUTH] = &operation{
execute: opAuth,
constantGas: 3100,
dynamicGas: gasReturn,
minStack: minStack(3, 1),
maxStack: maxStack(3, 1),
memorySize: memoryAuth,
}
jt[AUTHCALL] = &operation{
execute: opAuthCall,
constantGas: params.CallGasEIP150,
dynamicGas: gasCallEIP2929,
minStack: minStack(7, 1),
maxStack: maxStack(7, 1),
memorySize: memoryCall,
}
}

// opAuth implements the EIP-3074 AUTH instruction.
func opAuth(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
var (
tmp = scope.Stack.pop()
authority = common.Address(tmp.Bytes20())
offset = scope.Stack.pop()
length = scope.Stack.pop()
data = scope.Memory.GetPtr(int64(offset.Uint64()), int64(length.Uint64()))
sig = make([]byte, 65)
commit common.Hash
)
copy(sig, data)
if len(data) > 65 {
copy(commit[:], data[65:])
}

// If the desired authority has code, the operation must be considered
// unsuccessful.
statedb := interpreter.evm.StateDB
if statedb.GetCodeSize(authority) != 0 {
scope.Authorized = nil
scope.Stack.push(uint256.NewInt(0))
return nil, nil
}

// Build original auth message.
msg := []byte{params.AuthMagic}
msg = append(msg, common.LeftPadBytes(interpreter.evm.chainConfig.ChainID.Bytes(), 32)...)
msg = append(msg, common.LeftPadBytes(uint256.NewInt(statedb.GetNonce(authority)).Bytes(), 32)...)
msg = append(msg, common.LeftPadBytes(scope.Contract.Address().Bytes(), 32)...)
msg = append(msg, commit.Bytes()...)
msg = crypto.Keccak256(msg)

// Verify signature against provided address.
sig = append(sig[1:], sig[0]) // send y parity to back
pub, err := crypto.Ecrecover(msg, sig)

var recovered common.Address
if err == nil {
copy(recovered[:], crypto.Keccak256(pub[1:])[12:])
}

if err != nil || recovered != authority {
scope.Authorized = nil
scope.Stack.push(uint256.NewInt(0))
return nil, err
}

scope.Stack.push(uint256.NewInt(1))
scope.Authorized = &authority
return nil, nil
}

// opAuthCall implements the EIP-3074 AUTHCALL instruction.
func opAuthCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
if scope.Authorized == nil {
return nil, ErrAuthorizedNotSet
}
var (
stack = scope.Stack
temp = stack.pop()
gas = interpreter.evm.callGasTemp
addr, value, inOffset, inSize, retOffset, retSize = stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop()
toAddr = common.Address(addr.Bytes20())
args = scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64()))
)

if interpreter.readOnly && !value.IsZero() {
return nil, ErrWriteProtection
}

var bigVal = big0
if !value.IsZero() {
bigVal = value.ToBig()
}

ret, returnGas, err := interpreter.evm.AuthCall(scope.Contract, *scope.Authorized, toAddr, args, gas, bigVal, nil)

if err != nil {
temp.Clear()
} else {
temp.SetOne()
}
stack.push(&temp)
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.Gas += returnGas

interpreter.returnData = ret
return ret, nil
}
1 change: 1 addition & 0 deletions core/vm/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
ErrGasUintOverflow = errors.New("gas uint64 overflow")
ErrInvalidCode = errors.New("invalid code: must not begin with 0xef")
ErrNonceUintOverflow = errors.New("nonce uint64 overflow")
ErrAuthorizedNotSet = errors.New("authcall without setting authorized")

// errStopToken is an internal token indicating interpreter loop termination,
// never returned to outside callers.
Expand Down
84 changes: 84 additions & 0 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,90 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
return ret, gas, err
}

// AuthCall mimic Call except it sets the caller to the Authorized address in Scope
// and invoker and addr represents the invoker contract and destination contract
// being called respectively.
func (evm *EVM) AuthCall(invoker ContractRef, caller, addr common.Address, input []byte, gas uint64, value *big.Int, interruptCtx context.Context) (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
}
// Fail if we're trying to transfer more than the available balance
if value.Sign() != 0 && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)
debug := evm.Config.Tracer != nil

if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP158 && value.Sign() == 0 {
// Calling a non existing account, don't do anything, but ping the tracer
if debug {
if evm.depth == 0 {
evm.Config.Tracer.CaptureStart(evm, invoker.Address(), addr, false, input, gas, value)
evm.Config.Tracer.CaptureEnd(ret, 0, nil)
} else {
evm.Config.Tracer.CaptureEnter(AUTHCALL, invoker.Address(), addr, input, gas, value)
evm.Config.Tracer.CaptureExit(ret, 0, nil)
}
}
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
evm.Context.Transfer(evm.StateDB, caller, addr, value)

// Capture the tracer start/end events in debug mode
if debug {
if evm.depth == 0 {
evm.Config.Tracer.CaptureStart(evm, invoker.Address(), addr, false, input, gas, value)
defer func(startGas uint64) { // Lazy evaluation of the parameters
evm.Config.Tracer.CaptureEnd(ret, startGas-gas, err)
}(gas)
} else {
// Handle tracer events for entering and exiting a call frame
evm.Config.Tracer.CaptureEnter(AUTHCALL, invoker.Address(), addr, input, gas, value)
defer func(startGas uint64) {
evm.Config.Tracer.CaptureExit(ret, startGas-gas, err)
}(gas)
}
}

if isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
code := evm.StateDB.GetCode(addr)
if len(code) == 0 {
ret, err = nil, nil // gas is unchanged
} else {
addrCopy := addr
callerCopy := caller
// If the account has no code, we can abort here
// The depth-check is already done, and precompiles handled above
contract := NewContract(AccountRef(callerCopy), AccountRef(addrCopy), value, gas)
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code)
ret, err = evm.interpreter.PreRun(contract, input, false, interruptCtx)
gas = contract.Gas
}
}
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
gas = 0
}
// TODO: consider clearing up unused snapshots:
//} else {
// evm.StateDB.DiscardSnapshot(snapshot)
}
return ret, gas, err
}

type codeAndHash struct {
code []byte
hash common.Hash
Expand Down