Skip to content

Commit

Permalink
core: implement auth and authcall
Browse files Browse the repository at this point in the history
  • Loading branch information
lightclient committed May 14, 2024
1 parent 9c7ec85 commit bad02c1
Show file tree
Hide file tree
Showing 13 changed files with 405 additions and 4 deletions.
128 changes: 128 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"
"encoding/binary"
"errors"
"fmt"
Expand Down Expand Up @@ -4398,5 +4399,132 @@ func TestEIP7002(t *testing.T) {
t.Fatalf("wrong amount: want %d, got %d", want.Amount, got[i].Amount)
}
}
}

func TestEIP3074(t *testing.T) {
var (
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
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},
// The address 0xAAAA sloads 0x00 and 0x01
aa: {
Code: nil, // added below
Nonce: 0,
Balance: big.NewInt(0),
},
// The address 0xBBBB calls 0xAAAA
bb: {
Code: []byte{
byte(vm.CALLER),
byte(vm.PUSH0),
byte(vm.SSTORE),
byte(vm.STOP),
},
Nonce: 0,
Balance: big.NewInt(0),
},
},
}
)

invoker := []byte{
// copy sig to memory
byte(vm.CALLDATASIZE),
byte(vm.PUSH0),
byte(vm.PUSH0),
byte(vm.CALLDATACOPY),

// set up auth
byte(vm.CALLDATASIZE),
byte(vm.PUSH0),
}
// push authority to stack
invoker = append(invoker, append([]byte{byte(vm.PUSH20)}, addr.Bytes()...)...)
invoker = append(invoker, []byte{

byte(vm.AUTH),
byte(vm.POP),

// execute authcall
byte(vm.PUSH0), // out size
byte(vm.DUP1), // out offset
byte(vm.DUP1), // out insize
byte(vm.DUP1), // in offset
byte(vm.DUP1), // valueExt
byte(vm.DUP1), // value
byte(vm.PUSH2), // address
byte(0xbb),
byte(0xbb),
byte(vm.GAS), // gas
byte(vm.AUTHCALL),
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.ShanghaiTime = u64(0)
gspec.Config.CancunTime = u64(0)
gspec.Config.PragueTime = u64(0)
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(common.Big1.Bytes(), 32)...)
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]...)
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).Hooks()}, 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))
}
}
108 changes: 108 additions & 0 deletions core/vm/eips.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
Expand Down Expand Up @@ -533,3 +534,110 @@ func enable4762(jt *JumpTable) {
}
}
}

func enable3074(jt *JumpTable) {
jt[AUTH] = &operation{
execute: opAuth,
constantGas: 3100 + params.WarmStorageReadCostEIP2929,
dynamicGas: gasAuthEIP2929,
minStack: minStack(3, 1),
maxStack: maxStack(3, 1),
memorySize: memoryAuth,
}
jt[AUTHCALL] = &operation{
execute: opAuthCall,
constantGas: params.WarmStorageReadCostEIP2929,
dynamicGas: gasAuthCallEIP2929,
minStack: minStack(8, 1),
maxStack: maxStack(8, 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)
if err != nil {
scope.Authorized = nil
scope.Stack.push(uint256.NewInt(0))
return nil, err
}

// Check recovered matches expected authority.
var recovered common.Address
copy(recovered[:], crypto.Keccak256(pub[1:])[12:])
if 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(), 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
}
ret, returnGas, err := interpreter.evm.AuthCall(scope.Contract, *scope.Authorized, toAddr, args, gas, &value)
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 @@ -38,6 +38,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
67 changes: 67 additions & 0 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,73 @@ 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 addres$ in Scope.
func (evm *EVM) AuthCall(invoker ContractRef, caller, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) {
// Capture the tracer start/end events in debug mode
if evm.Config.Tracer != nil {
evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig())
defer func(startGas uint64) {
evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err)
}(gas)
}
// 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.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)

if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() {
// Calling a non-existing account, don't do anything.
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
evm.Context.Transfer(evm.StateDB, caller, addr, value)

if isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer)
} 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.Run(contract, input, false)
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 {
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution)
}

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
15 changes: 15 additions & 0 deletions core/vm/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,18 @@ func callGas(isEip150 bool, availableGas, base uint64, callCost *uint256.Int) (u

return callCost.Uint64(), nil
}

func authCallGas(availableGas, base uint64, callCost *uint256.Int) (uint64, error) {
availableGas = availableGas - base
gas := availableGas - availableGas/64
// If the bit length exceeds 64 bit we know that the newly calculated "gas" for EIP150
// is smaller than the requested amount. Therefore we return the new gas instead
// of returning an error.
if !callCost.IsUint64() || gas < callCost.Uint64() {
return gas, nil
}
if !callCost.IsUint64() {
return 0, ErrGasUintOverflow
}
return callCost.Uint64(), nil
}
Loading

0 comments on commit bad02c1

Please sign in to comment.