Skip to content

Commit

Permalink
Merge pull request #5233 from onflow/ramtin/5199-add-precompile
Browse files Browse the repository at this point in the history
[Flow EVM] extend EVM precompiles with cadence arch
  • Loading branch information
ramtinms authored Jan 23, 2024
2 parents 690a250 + a930a00 commit 3107bc9
Show file tree
Hide file tree
Showing 19 changed files with 647 additions and 97 deletions.
64 changes: 44 additions & 20 deletions fvm/evm/emulator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"math"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
gethCommon "github.com/ethereum/go-ethereum/common"
gethCore "github.com/ethereum/go-ethereum/core"
gethVM "github.com/ethereum/go-ethereum/core/vm"
gethCrypto "github.com/ethereum/go-ethereum/crypto"
gethParams "github.com/ethereum/go-ethereum/params"

"github.com/onflow/flow-go/fvm/evm/types"
)

var (
Expand All @@ -21,15 +23,24 @@ var (
// Config sets the required parameters
type Config struct {
// Chain Config
ChainConfig *params.ChainConfig
ChainConfig *gethParams.ChainConfig
// EVM config
EVMConfig vm.Config
EVMConfig gethVM.Config
// block context
BlockContext *vm.BlockContext
BlockContext *gethVM.BlockContext
// transaction context
TxContext *vm.TxContext
TxContext *gethVM.TxContext
// base unit of gas for direct calls
DirectCallBaseGasUsage uint64
// a set of extra precompiles to be injected
ExtraPrecompiles map[gethCommon.Address]gethVM.PrecompiledContract
}

func (c *Config) ChainRules() gethParams.Rules {
return c.ChainConfig.Rules(
c.BlockContext.BlockNumber,
c.BlockContext.Random != nil,
c.BlockContext.Time)
}

// DefaultChainConfig is the default chain config which
Expand All @@ -39,7 +50,7 @@ type Config struct {
// For the future changes of EVM, we need to update the EVM go mod version
// and set a proper height for the specific release based on the Flow EVM heights
// so it could gets activated at a desired time.
var DefaultChainConfig = &params.ChainConfig{
var DefaultChainConfig = &gethParams.ChainConfig{
ChainID: FlowEVMTestnetChainID, // default is testnet

// Fork scheduling based on block heights
Expand All @@ -66,20 +77,20 @@ var DefaultChainConfig = &params.ChainConfig{
func defaultConfig() *Config {
return &Config{
ChainConfig: DefaultChainConfig,
EVMConfig: vm.Config{
EVMConfig: gethVM.Config{
NoBaseFee: true,
},
TxContext: &vm.TxContext{
TxContext: &gethVM.TxContext{
GasPrice: new(big.Int),
BlobFeeCap: new(big.Int),
},
BlockContext: &vm.BlockContext{
CanTransfer: core.CanTransfer,
Transfer: core.Transfer,
BlockContext: &gethVM.BlockContext{
CanTransfer: gethCore.CanTransfer,
Transfer: gethCore.Transfer,
GasLimit: BlockLevelGasLimit, // block gas limit
BaseFee: big.NewInt(0),
GetHash: func(n uint64) common.Hash { // default returns some random hash values
return common.BytesToHash(crypto.Keccak256([]byte(new(big.Int).SetUint64(n).String())))
GetHash: func(n uint64) gethCommon.Hash { // default returns some random hash values
return gethCommon.BytesToHash(gethCrypto.Keccak256([]byte(new(big.Int).SetUint64(n).String())))
},
},
}
Expand Down Expand Up @@ -114,7 +125,7 @@ func WithMainnetChainID() Option {
}

// WithOrigin sets the origin of the transaction (signer)
func WithOrigin(origin common.Address) Option {
func WithOrigin(origin gethCommon.Address) Option {
return func(c *Config) *Config {
c.TxContext.Origin = origin
return c
Expand All @@ -138,7 +149,7 @@ func WithGasLimit(gasLimit uint64) Option {
}

// WithCoinbase sets the coinbase of the block where the fees are collected in
func WithCoinbase(coinbase common.Address) Option {
func WithCoinbase(coinbase gethCommon.Address) Option {
return func(c *Config) *Config {
c.BlockContext.Coinbase = coinbase
return c
Expand All @@ -162,7 +173,7 @@ func WithBlockTime(time uint64) Option {
}

// WithGetBlockHashFunction sets the functionality to look up block hash by height
func WithGetBlockHashFunction(getHash vm.GetHashFunc) Option {
func WithGetBlockHashFunction(getHash gethVM.GetHashFunc) Option {
return func(c *Config) *Config {
c.BlockContext.GetHash = getHash
return c
Expand All @@ -176,3 +187,16 @@ func WithDirectCallBaseGasUsage(gas uint64) Option {
return c
}
}

// WithExtraPrecompiles appends precompile list with extra precompiles
func WithExtraPrecompiles(precompiles []types.Precompile) Option {
return func(c *Config) *Config {
for _, pc := range precompiles {
if c.ExtraPrecompiles == nil {
c.ExtraPrecompiles = make(map[gethCommon.Address]gethVM.PrecompiledContract)
}
c.ExtraPrecompiles[pc.Address().ToCommon()] = pc
}
return c
}
}
24 changes: 24 additions & 0 deletions fvm/evm/emulator/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func newConfig(ctx types.BlockContext) *Config {
WithBlockNumber(new(big.Int).SetUint64(ctx.BlockNumber)),
WithCoinbase(ctx.GasFeeCollector.ToCommon()),
WithDirectCallBaseGasUsage(ctx.DirectCallBaseGasUsage),
WithExtraPrecompiles(ctx.ExtraPrecompiles),
)
}

Expand All @@ -53,6 +54,7 @@ func (em *Emulator) NewReadOnlyBlockView(ctx types.BlockContext) (types.ReadOnly
// NewBlockView constructs a new block view (mutable)
func (em *Emulator) NewBlockView(ctx types.BlockContext) (types.BlockView, error) {
cfg := newConfig(ctx)
SetupPrecompile(cfg)
return &BlockView{
config: cfg,
rootAddr: em.rootAddr,
Expand Down Expand Up @@ -279,3 +281,25 @@ func (proc *procedure) run(msg *gethCore.Message, txType uint8) (*types.Result,
}
return &res, err
}

func SetupPrecompile(cfg *Config) {
rules := cfg.ChainRules()
// captures the pointer to the map that has to be augmented
var precompiles map[gethCommon.Address]gethVM.PrecompiledContract
switch {
case rules.IsCancun:
precompiles = gethVM.PrecompiledContractsCancun
case rules.IsBerlin:
precompiles = gethVM.PrecompiledContractsBerlin
case rules.IsIstanbul:
precompiles = gethVM.PrecompiledContractsIstanbul
case rules.IsByzantium:
precompiles = gethVM.PrecompiledContractsByzantium
default:
precompiles = gethVM.PrecompiledContractsHomestead
}
for addr, contract := range cfg.ExtraPrecompiles {
// we override if exist since we call this method on every block
precompiles[addr] = contract
}
}
78 changes: 77 additions & 1 deletion fvm/evm/emulator/emulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ func TestStorageNoSideEffect(t *testing.T) {
var err error
em := emulator.NewEmulator(backend, flowEVMRoot)
testAccount := types.NewAddressFromString("test")

amount := big.NewInt(10)
RunWithNewBlockView(t, em, func(blk types.BlockView) {
_, err = blk.DirectCall(types.NewDepositCall(testAccount, amount))
Expand All @@ -351,3 +350,80 @@ func TestStorageNoSideEffect(t *testing.T) {
})
})
}

func TestCallingExtraPrecompiles(t *testing.T) {
testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) {
testutils.RunWithTestFlowEVMRootAddress(t, backend, func(flowEVMRoot flow.Address) {
RunWithNewEmulator(t, backend, flowEVMRoot, func(em *emulator.Emulator) {

testAccount := types.NewAddressFromString("test")
amount := big.NewInt(10_000_000)
RunWithNewBlockView(t, em, func(blk types.BlockView) {
_, err := blk.DirectCall(types.NewDepositCall(testAccount, amount))
require.NoError(t, err)
})

input := []byte{1, 2}
output := []byte{3, 4}
addr := testutils.RandomAddress(t)
pc := &MockedPrecompile{
AddressFunc: func() types.Address {
return addr
},
RequiredGasFunc: func(input []byte) uint64 {
return uint64(10)
},
RunFunc: func(inp []byte) ([]byte, error) {
require.Equal(t, input, inp)
return output, nil
},
}

ctx := types.NewDefaultBlockContext(blockNumber.Uint64())
ctx.ExtraPrecompiles = []types.Precompile{pc}

blk, err := em.NewBlockView(ctx)
require.NoError(t, err)

res, err := blk.DirectCall(
types.NewContractCall(
testAccount,
types.NewAddress(addr.ToCommon()),
input,
1_000_000,
big.NewInt(0), // this should be zero because the contract doesn't have receiver
),
)
require.NoError(t, err)
require.Equal(t, output, res.ReturnedValue)
})
})
})
}

type MockedPrecompile struct {
AddressFunc func() types.Address
RequiredGasFunc func(input []byte) uint64
RunFunc func(input []byte) ([]byte, error)
}

func (mp *MockedPrecompile) Address() types.Address {
if mp.AddressFunc == nil {
panic("Address not set for the mocked precompile")
}
return mp.AddressFunc()
}

func (mp *MockedPrecompile) RequiredGas(input []byte) uint64 {
if mp.RequiredGasFunc == nil {
panic("RequiredGas not set for the mocked precompile")
}
return mp.RequiredGasFunc(input)
}

func (mp *MockedPrecompile) Run(input []byte) ([]byte, error) {
if mp.RunFunc == nil {
panic("Run not set for the mocked precompile")
}
return mp.RunFunc(input)
}
3 changes: 0 additions & 3 deletions fvm/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,8 @@ func TestBridgedAccountWithdraw(t *testing.T) {
})
}

// TODO: provide proper contract code
func TestBridgedAccountDeploy(t *testing.T) {

t.Parallel()

RunWithTestBackend(t, func(backend *testutils.TestBackend) {
RunWithTestFlowEVMRootAddress(t, backend, func(rootAddr flow.Address) {
tc := GetStorageTestContract(t)
Expand Down
44 changes: 37 additions & 7 deletions fvm/evm/handler/addressAllocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ import (
"github.com/onflow/flow-go/model/flow"
)

const ledgerAddressAllocatorKey = "AddressAllocator"
const (
ledgerAddressAllocatorKey = "AddressAllocator"
uint64ByteSize = 8
addressPrefixLen = 12
)

var (
// prefixes:
// the first 12 bytes of addresses allocation
// leading zeros helps with storage and all zero is reserved for the EVM precompiles
FlowEVMPrecompileAddressPrefix = [addressPrefixLen]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
FlowEVMCOAAddressPrefix = [addressPrefixLen]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
)

type AddressAllocator struct {
led atree.Ledger
Expand All @@ -26,8 +38,8 @@ func NewAddressAllocator(led atree.Ledger, flexAddress flow.Address) (*AddressAl
}, nil
}

// AllocateAddress allocates an address
func (aa *AddressAllocator) AllocateAddress() (types.Address, error) {
// AllocateCOAAddress allocates an address for COA
func (aa *AddressAllocator) AllocateCOAAddress() (types.Address, error) {
data, err := aa.led.GetValue(aa.flexAddress[:], []byte(ledgerAddressAllocatorKey))
if err != nil {
return types.Address{}, err
Expand All @@ -38,10 +50,7 @@ func (aa *AddressAllocator) AllocateAddress() (types.Address, error) {
uuid = binary.BigEndian.Uint64(data)
}

target := types.Address{}
// first 12 bytes would be zero
// the next 8 bytes would be an increment of the UUID index
binary.BigEndian.PutUint64(target[12:], uuid)
target := MakeCOAAddress(uuid)

// store new uuid
newData := make([]byte, 8)
Expand All @@ -53,3 +62,24 @@ func (aa *AddressAllocator) AllocateAddress() (types.Address, error) {

return target, nil
}

func MakeCOAAddress(index uint64) types.Address {
return makePrefixedAddress(index, FlowEVMCOAAddressPrefix)
}

func (aa *AddressAllocator) AllocatePrecompileAddress(index uint64) types.Address {
target := MakePrecompileAddress(index)
return target
}

func MakePrecompileAddress(index uint64) types.Address {
return makePrefixedAddress(index, FlowEVMPrecompileAddressPrefix)
}

func makePrefixedAddress(index uint64, prefix [addressPrefixLen]byte) types.Address {
var addr types.Address
prefixIndex := types.AddressLength - uint64ByteSize
copy(addr[:prefixIndex], prefix[:])
binary.BigEndian.PutUint64(addr[prefixIndex:], index)
return addr
}
12 changes: 8 additions & 4 deletions fvm/evm/handler/addressAllocator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,20 @@ func TestAddressAllocator(t *testing.T) {
aa, err := handler.NewAddressAllocator(backend, root)
require.NoError(t, err)

adr := aa.AllocatePrecompileAddress(3)
expectedAddress := types.NewAddress(gethCommon.HexToAddress("0x0000000000000000000000010000000000000003"))
require.Equal(t, expectedAddress, adr)

// test default value fall back
adr, err := aa.AllocateAddress()
adr, err = aa.AllocateCOAAddress()
require.NoError(t, err)
expectedAddress := types.NewAddress(gethCommon.HexToAddress("0x00000000000000000001"))
expectedAddress = types.NewAddress(gethCommon.HexToAddress("0x0000000000000000000000020000000000000001"))
require.Equal(t, expectedAddress, adr)

// continous allocation logic
adr, err = aa.AllocateAddress()
adr, err = aa.AllocateCOAAddress()
require.NoError(t, err)
expectedAddress = types.NewAddress(gethCommon.HexToAddress("0x00000000000000000002"))
expectedAddress = types.NewAddress(gethCommon.HexToAddress("0x0000000000000000000000020000000000000002"))
require.Equal(t, expectedAddress, adr)
})

Expand Down
Loading

0 comments on commit 3107bc9

Please sign in to comment.