diff --git a/fvm/evm/emulator/config.go b/fvm/evm/emulator/config.go index b15ede521e9..e1bfc0b1375 100644 --- a/fvm/evm/emulator/config.go +++ b/fvm/evm/emulator/config.go @@ -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 ( @@ -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 @@ -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 = ¶ms.ChainConfig{ +var DefaultChainConfig = &gethParams.ChainConfig{ ChainID: FlowEVMTestnetChainID, // default is testnet // Fork scheduling based on block heights @@ -66,20 +77,20 @@ var DefaultChainConfig = ¶ms.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()))) }, }, } @@ -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 @@ -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 @@ -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 @@ -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 + } +} diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index 69ad180d043..93561565a59 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -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), ) } @@ -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, @@ -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 + } +} diff --git a/fvm/evm/emulator/emulator_test.go b/fvm/evm/emulator/emulator_test.go index 3cca27b0906..a7c5768cca0 100644 --- a/fvm/evm/emulator/emulator_test.go +++ b/fvm/evm/emulator/emulator_test.go @@ -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)) @@ -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) +} diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 16e38d91a08..927fdc76d2f 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -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) diff --git a/fvm/evm/handler/addressAllocator.go b/fvm/evm/handler/addressAllocator.go index 28e476d9427..d1dc8299130 100644 --- a/fvm/evm/handler/addressAllocator.go +++ b/fvm/evm/handler/addressAllocator.go @@ -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 @@ -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 @@ -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) @@ -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 +} diff --git a/fvm/evm/handler/addressAllocator_test.go b/fvm/evm/handler/addressAllocator_test.go index ab8eb0de2b4..03794baea9a 100644 --- a/fvm/evm/handler/addressAllocator_test.go +++ b/fvm/evm/handler/addressAllocator_test.go @@ -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) }) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 8bb9d4e09aa..5ff314ddc18 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -10,6 +10,7 @@ import ( "github.com/onflow/flow-go/fvm/environment" "github.com/onflow/flow-go/fvm/errors" + "github.com/onflow/flow-go/fvm/evm/precompiles" "github.com/onflow/flow-go/fvm/evm/types" ) @@ -21,6 +22,7 @@ type ContractHandler struct { addressAllocator types.AddressAllocator backend types.Backend emulator types.Emulator + precompiles []types.Precompile } func (h *ContractHandler) FlowTokenAddress() common.Address { @@ -42,12 +44,25 @@ func NewContractHandler( addressAllocator: addressAllocator, backend: backend, emulator: emulator, + precompiles: getPrecompiles(addressAllocator, backend), } } +func getPrecompiles( + addressAllocator types.AddressAllocator, + backend types.Backend, +) []types.Precompile { + archAddress := addressAllocator.AllocatePrecompileAddress(1) + archContract := precompiles.ArchContract( + archAddress, + backend.GetCurrentBlockHeight, + ) + return []types.Precompile{archContract} +} + // AllocateAddress allocates an address to be used by the bridged accounts func (h *ContractHandler) AllocateAddress() types.Address { - target, err := h.addressAllocator.AllocateAddress() + target, err := h.addressAllocator.AllocateCOAAddress() handleError(err) return target } @@ -141,6 +156,7 @@ func (h *ContractHandler) getBlockContext() types.BlockContext { return types.BlockContext{ BlockNumber: bp.Height, DirectCallBaseGasUsage: types.DefaultDirectCallBaseGasUsage, + ExtraPrecompiles: h.precompiles, } } diff --git a/fvm/evm/handler/handler_test.go b/fvm/evm/handler/handler_test.go index 0acf3d3ceff..2d374ef231f 100644 --- a/fvm/evm/handler/handler_test.go +++ b/fvm/evm/handler/handler_test.go @@ -24,6 +24,7 @@ import ( "github.com/onflow/flow-go/fvm/errors" "github.com/onflow/flow-go/fvm/evm/emulator" "github.com/onflow/flow-go/fvm/evm/handler" + "github.com/onflow/flow-go/fvm/evm/precompiles" "github.com/onflow/flow-go/fvm/evm/testutils" "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/systemcontracts" @@ -279,12 +280,12 @@ func TestHandler_OpsWithoutEmulator(t *testing.T) { aa, err := handler.NewAddressAllocator(backend, rootAddr) require.NoError(t, err) - handler := handler.NewContractHandler(flowTokenAddress, blockchain, aa, backend, nil) + h := handler.NewContractHandler(flowTokenAddress, blockchain, aa, backend, nil) - foa := handler.AllocateAddress() + foa := h.AllocateAddress() require.NotNil(t, foa) - expectedAddress := types.NewAddress(gethCommon.HexToAddress("0x00000000000000000001")) + expectedAddress := handler.MakeCOAAddress(1) require.Equal(t, expectedAddress, foa) }) }) @@ -355,8 +356,6 @@ func TestHandler_BridgedAccount(t *testing.T) { }) t.Run("test withdraw (unhappy case)", func(t *testing.T) { - t.Parallel() - testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) { testutils.RunWithTestFlowEVMRootAddress(t, backend, func(rootAddr flow.Address) { testutils.RunWithEOATestAccount(t, backend, rootAddr, func(eoa *testutils.EOATestAccount) { @@ -478,9 +477,7 @@ func TestHandler_BridgedAccount(t *testing.T) { require.NotNil(t, foa) // deposit 10000 flow - orgBalance, err := types.NewBalanceFromAttoFlow(new(big.Int).Mul(big.NewInt(1e18), big.NewInt(10000))) - require.NoError(t, err) - vault := types.NewFlowTokenVault(orgBalance) + vault := types.NewFlowTokenVault(testutils.MakeABalanceInFlow(10000)) foa.Deposit(vault) testContract := testutils.GetStorageTestContract(t) @@ -506,6 +503,31 @@ func TestHandler_BridgedAccount(t *testing.T) { }) }) + t.Run("test call to cadence arch", func(t *testing.T) { + t.Parallel() + + testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) { + blockHeight := uint64(123) + backend.GetCurrentBlockHeightFunc = func() (uint64, error) { + return blockHeight, nil + } + testutils.RunWithTestFlowEVMRootAddress(t, backend, func(rootAddr flow.Address) { + h := SetupHandler(t, backend, rootAddr) + + foa := h.AccountByAddress(h.AllocateAddress(), true) + require.NotNil(t, foa) + + vault := types.NewFlowTokenVault(testutils.MakeABalanceInFlow(10000)) + foa.Deposit(vault) + + arch := handler.MakePrecompileAddress(1) + + ret := foa.Call(arch, precompiles.FlowBlockHeightFuncSig[:], math.MaxUint64, types.Balance(0)) + require.Equal(t, big.NewInt(int64(blockHeight)), new(big.Int).SetBytes(ret)) + }) + }) + }) + // TODO add test with test emulator for unhappy cases (emulator) } diff --git a/fvm/evm/precompiles/arch.go b/fvm/evm/precompiles/arch.go new file mode 100644 index 00000000000..ca6477e311a --- /dev/null +++ b/fvm/evm/precompiles/arch.go @@ -0,0 +1,56 @@ +package precompiles + +import ( + "encoding/binary" + "fmt" + + gethCommon "github.com/ethereum/go-ethereum/common" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +var ( + FlowBlockHeightFuncSig = ComputeFunctionSelector("flowBlockHeight", nil) + // TODO update me with a higher value if needed + FlowBlockHeightFixedGas = uint64(1) +) + +// ArchContract return a procompile for the Cadence Arch contract +// which facilitates access of Flow EVM environment into the Cadence environment. +// for more details see this Flip 223. +func ArchContract( + address types.Address, + heightProvider func() (uint64, error), +) types.Precompile { + return MultiFunctionPrecompileContract( + address, + []Function{&flowBlockHeightFunction{heightProvider}}, + ) +} + +type flowBlockHeightFunction struct { + flowBlockHeightLookUp func() (uint64, error) +} + +func (c *flowBlockHeightFunction) FunctionSelector() FunctionSelector { + return FlowBlockHeightFuncSig +} + +func (c *flowBlockHeightFunction) ComputeGas(input []byte) uint64 { + return FlowBlockHeightFixedGas +} + +func (c *flowBlockHeightFunction) Run(input []byte) ([]byte, error) { + if len(input) > 0 { + return nil, fmt.Errorf("unexpected input is provided") + } + bh, err := c.flowBlockHeightLookUp() + if err != nil { + return nil, err + } + encoded := make([]byte, 8) + binary.BigEndian.PutUint64(encoded, bh) + // the EVM works natively in 256-bit words, + // we left pad to that size to prevent extra gas consumtion for masking. + return gethCommon.LeftPadBytes(encoded, 32), nil +} diff --git a/fvm/evm/precompiles/arch_test.go b/fvm/evm/precompiles/arch_test.go new file mode 100644 index 00000000000..9f0cf186da7 --- /dev/null +++ b/fvm/evm/precompiles/arch_test.go @@ -0,0 +1,35 @@ +package precompiles_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/precompiles" + "github.com/onflow/flow-go/fvm/evm/testutils" +) + +func TestArchContract(t *testing.T) { + address := testutils.RandomAddress(t) + + height := uint64(12) + pc := precompiles.ArchContract( + address, + func() (uint64, error) { + return height, nil + }, + ) + + input := precompiles.FlowBlockHeightFuncSig.Bytes() + require.Equal(t, address, pc.Address()) + require.Equal(t, precompiles.FlowBlockHeightFixedGas, pc.RequiredGas(input)) + ret, err := pc.Run(input) + require.NoError(t, err) + + expected := make([]byte, 32) + expected[31] = 12 + require.Equal(t, expected, ret) + + _, err = pc.Run([]byte{1, 2, 3}) + require.Error(t, err) +} diff --git a/fvm/evm/precompiles/precompile.go b/fvm/evm/precompiles/precompile.go new file mode 100644 index 00000000000..85f96cd59c1 --- /dev/null +++ b/fvm/evm/precompiles/precompile.go @@ -0,0 +1,75 @@ +package precompiles + +import ( + "errors" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +// InvalidMethodCallGasUsage captures how much gas we charge for invalid method call +const InvalidMethodCallGasUsage = uint64(1) + +// ErrInvalidMethodCall is returned when the method is not available on the contract +var ErrInvalidMethodCall = errors.New("invalid method call") + +// Function is an interface for a function in a multi-function precompile contract +type Function interface { + // FunctionSelector returns the function selector bytes for this function + FunctionSelector() FunctionSelector + + // ComputeGas computes the gas needed for the given input + ComputeGas(input []byte) uint64 + + // Run runs the function on the given data + Run(input []byte) ([]byte, error) +} + +// MultiFunctionPrecompileContract constructs a multi-function precompile smart contract +func MultiFunctionPrecompileContract( + address types.Address, + functions []Function, +) types.Precompile { + pc := &precompile{ + functions: make(map[FunctionSelector]Function), + address: address, + } + for _, f := range functions { + pc.functions[f.FunctionSelector()] = f + } + return pc +} + +type precompile struct { + address types.Address + functions map[FunctionSelector]Function +} + +func (p *precompile) Address() types.Address { + return p.address +} + +// RequiredGas calculates the contract gas use +func (p *precompile) RequiredGas(input []byte) uint64 { + if len(input) < FunctionSelectorLength { + return InvalidMethodCallGasUsage + } + sig, data := SplitFunctionSelector(input) + callable, found := p.functions[sig] + if !found { + return InvalidMethodCallGasUsage + } + return callable.ComputeGas(data) +} + +// Run runs the precompiled contract +func (p *precompile) Run(input []byte) ([]byte, error) { + if len(input) < FunctionSelectorLength { + return nil, ErrInvalidMethodCall + } + sig, data := SplitFunctionSelector(input) + callable, found := p.functions[sig] + if !found { + return nil, ErrInvalidMethodCall + } + return callable.Run(data) +} diff --git a/fvm/evm/precompiles/precompile_test.go b/fvm/evm/precompiles/precompile_test.go new file mode 100644 index 00000000000..654cc71c14f --- /dev/null +++ b/fvm/evm/precompiles/precompile_test.go @@ -0,0 +1,73 @@ +package precompiles_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/precompiles" + "github.com/onflow/flow-go/fvm/evm/testutils" +) + +func TestMutiFunctionContract(t *testing.T) { + t.Parallel() + + address := testutils.RandomAddress(t) + sig := precompiles.FunctionSelector{1, 2, 3, 4} + data := "data" + input := append(sig[:], data...) + gas := uint64(20) + output := []byte("output") + + pc := precompiles.MultiFunctionPrecompileContract(address, []precompiles.Function{ + &mockedFunction{ + FunctionSelectorFunc: func() precompiles.FunctionSelector { + return sig + }, + ComputeGasFunc: func(inp []byte) uint64 { + require.Equal(t, []byte(data), inp) + return gas + }, + RunFunc: func(inp []byte) ([]byte, error) { + require.Equal(t, []byte(data), inp) + return output, nil + }, + }}) + + require.Equal(t, address, pc.Address()) + require.Equal(t, gas, pc.RequiredGas(input)) + ret, err := pc.Run(input) + require.NoError(t, err) + require.Equal(t, output, ret) + + input2 := []byte("non existing signature and data") + _, err = pc.Run(input2) + require.Equal(t, precompiles.ErrInvalidMethodCall, err) +} + +type mockedFunction struct { + FunctionSelectorFunc func() precompiles.FunctionSelector + ComputeGasFunc func(input []byte) uint64 + RunFunc func(input []byte) ([]byte, error) +} + +func (mf *mockedFunction) FunctionSelector() precompiles.FunctionSelector { + if mf.FunctionSelectorFunc == nil { + panic("method not set for mocked function") + } + return mf.FunctionSelectorFunc() +} + +func (mf *mockedFunction) ComputeGas(input []byte) uint64 { + if mf.ComputeGasFunc == nil { + panic("method not set for mocked function") + } + return mf.ComputeGasFunc(input) +} + +func (mf *mockedFunction) Run(input []byte) ([]byte, error) { + if mf.RunFunc == nil { + panic("method not set for mocked function") + } + return mf.RunFunc(input) +} diff --git a/fvm/evm/precompiles/signature.go b/fvm/evm/precompiles/signature.go new file mode 100644 index 00000000000..a62c8f5b9ac --- /dev/null +++ b/fvm/evm/precompiles/signature.go @@ -0,0 +1,35 @@ +package precompiles + +import ( + "fmt" + "strings" + + gethCrypto "github.com/ethereum/go-ethereum/crypto" +) + +const FunctionSelectorLength = 4 + +// This is derived as the first 4 bytes of the Keccak hash of the ASCII form of the signature of the method +type FunctionSelector [FunctionSelectorLength]byte + +func (fs FunctionSelector) Bytes() []byte { + return fs[:] +} + +// ComputeFunctionSelector computes the function selector +// given the canonical name of function and args. +// for example the canonical format for int is int256 +func ComputeFunctionSelector(name string, args []string) FunctionSelector { + var sig FunctionSelector + input := fmt.Sprintf("%v(%v)", name, strings.Join(args, ",")) + copy(sig[0:FunctionSelectorLength], gethCrypto.Keccak256([]byte(input))[:FunctionSelectorLength]) + return sig +} + +// SplitFunctionSelector splits the function signature from input data and +// returns the rest of the data +func SplitFunctionSelector(input []byte) (FunctionSelector, []byte) { + var funcSig FunctionSelector + copy(funcSig[:], input[0:FunctionSelectorLength]) + return funcSig, input[FunctionSelectorLength:] +} diff --git a/fvm/evm/precompiles/signature_test.go b/fvm/evm/precompiles/signature_test.go new file mode 100644 index 00000000000..d6f36b9fffe --- /dev/null +++ b/fvm/evm/precompiles/signature_test.go @@ -0,0 +1,27 @@ +package precompiles_test + +import ( + "testing" + + gethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/precompiles" +) + +func TestFunctionSelector(t *testing.T) { + t.Parallel() + + expected := gethCrypto.Keccak256([]byte("test()"))[:4] + require.Equal(t, expected, precompiles.ComputeFunctionSelector("test", nil).Bytes()) + + expected = gethCrypto.Keccak256([]byte("test(uint32,uint16)"))[:precompiles.FunctionSelectorLength] + require.Equal(t, expected, + precompiles.ComputeFunctionSelector("test", []string{"uint32", "uint16"}).Bytes()) + + selector := []byte{1, 2, 3, 4} + data := []byte{5, 6, 7, 8} + retSelector, retData := precompiles.SplitFunctionSelector(append(selector, data...)) + require.Equal(t, selector, retSelector[:]) + require.Equal(t, data, retData) +} diff --git a/fvm/evm/testutils/backend.go b/fvm/evm/testutils/backend.go index fc08d84aad8..8c73fbf02f3 100644 --- a/fvm/evm/testutils/backend.go +++ b/fvm/evm/testutils/backend.go @@ -9,11 +9,13 @@ import ( "github.com/onflow/atree" "github.com/onflow/cadence" jsoncdc "github.com/onflow/cadence/encoding/json" + "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" "github.com/stretchr/testify/require" "golang.org/x/exp/maps" "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/meter" "github.com/onflow/flow-go/model/flow" ) @@ -33,6 +35,7 @@ func RunWithTestBackend(t testing.TB, f func(*TestBackend)) { TestValueStore: GetSimpleValueStore(), testEventEmitter: getSimpleEventEmitter(), testMeter: getSimpleMeter(), + TestBlockInfo: &TestBlockInfo{}, } f(tb) } @@ -153,8 +156,11 @@ type TestBackend struct { *TestValueStore *testMeter *testEventEmitter + *TestBlockInfo } +var _ types.Backend = &TestBackend{} + func (tb *TestBackend) TotalStorageSize() int { if tb.TotalStorageSizeFunc == nil { panic("method not set") @@ -376,3 +382,26 @@ func (vs *testEventEmitter) Reset() { } vs.reset() } + +type TestBlockInfo struct { + GetCurrentBlockHeightFunc func() (uint64, error) + GetBlockAtHeightFunc func(height uint64) (runtime.Block, bool, error) +} + +var _ environment.BlockInfo = &TestBlockInfo{} + +// GetCurrentBlockHeight returns the current block height. +func (tb *TestBlockInfo) GetCurrentBlockHeight() (uint64, error) { + if tb.GetCurrentBlockHeightFunc == nil { + panic("GetCurrentBlockHeight method is not set") + } + return tb.GetCurrentBlockHeightFunc() +} + +// GetBlockAtHeight returns the block at the given height. +func (tb *TestBlockInfo) GetBlockAtHeight(height uint64) (runtime.Block, bool, error) { + if tb.GetBlockAtHeightFunc == nil { + panic("GetBlockAtHeight method is not set") + } + return tb.GetBlockAtHeightFunc(height) +} diff --git a/fvm/evm/testutils/emulator.go b/fvm/evm/testutils/emulator.go index 5f7f2ce3068..0cdc0d4d93c 100644 --- a/fvm/evm/testutils/emulator.go +++ b/fvm/evm/testutils/emulator.go @@ -1,14 +1,9 @@ package testutils import ( - cryptoRand "crypto/rand" "math/big" - "math/rand" - "testing" - gethCommon "github.com/ethereum/go-ethereum/common" gethTypes "github.com/ethereum/go-ethereum/core/types" - "github.com/stretchr/testify/require" "github.com/onflow/flow-go/fvm/evm/types" ) @@ -72,49 +67,3 @@ func (em *TestEmulator) RunTransaction(tx *gethTypes.Transaction) (*types.Result } return em.RunTransactionFunc(tx) } - -func RandomCommonHash(t testing.TB) gethCommon.Hash { - ret := gethCommon.Hash{} - _, err := cryptoRand.Read(ret[:gethCommon.HashLength]) - require.NoError(t, err) - return ret -} - -func RandomBigInt(limit int64) *big.Int { - return big.NewInt(rand.Int63n(limit) + 1) -} - -func RandomAddress(t testing.TB) types.Address { - return types.NewAddress(RandomCommonAddress(t)) -} - -func RandomCommonAddress(t testing.TB) gethCommon.Address { - ret := gethCommon.Address{} - _, err := cryptoRand.Read(ret[:gethCommon.AddressLength]) - require.NoError(t, err) - return ret -} - -func RandomGas(limit int64) uint64 { - return uint64(rand.Int63n(limit) + 1) -} - -func RandomData(t testing.TB) []byte { - // byte size [1, 100] - size := rand.Intn(100) + 1 - ret := make([]byte, size) - _, err := cryptoRand.Read(ret[:]) - require.NoError(t, err) - return ret -} - -func GetRandomLogFixture(t testing.TB) *gethTypes.Log { - return &gethTypes.Log{ - Address: RandomCommonAddress(t), - Topics: []gethCommon.Hash{ - RandomCommonHash(t), - RandomCommonHash(t), - }, - Data: RandomData(t), - } -} diff --git a/fvm/evm/testutils/misc.go b/fvm/evm/testutils/misc.go new file mode 100644 index 00000000000..b335ad3adfb --- /dev/null +++ b/fvm/evm/testutils/misc.go @@ -0,0 +1,65 @@ +package testutils + +import ( + cryptoRand "crypto/rand" + "math/big" + "math/rand" + "testing" + + gethCommon "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +func RandomCommonHash(t testing.TB) gethCommon.Hash { + ret := gethCommon.Hash{} + _, err := cryptoRand.Read(ret[:gethCommon.HashLength]) + require.NoError(t, err) + return ret +} + +func RandomBigInt(limit int64) *big.Int { + return big.NewInt(rand.Int63n(limit) + 1) +} + +func RandomAddress(t testing.TB) types.Address { + return types.NewAddress(RandomCommonAddress(t)) +} + +func RandomCommonAddress(t testing.TB) gethCommon.Address { + ret := gethCommon.Address{} + _, err := cryptoRand.Read(ret[:gethCommon.AddressLength]) + require.NoError(t, err) + return ret +} + +func RandomGas(limit int64) uint64 { + return uint64(rand.Int63n(limit) + 1) +} + +func RandomData(t testing.TB) []byte { + // byte size [1, 100] + size := rand.Intn(100) + 1 + ret := make([]byte, size) + _, err := cryptoRand.Read(ret[:]) + require.NoError(t, err) + return ret +} + +func GetRandomLogFixture(t testing.TB) *gethTypes.Log { + return &gethTypes.Log{ + Address: RandomCommonAddress(t), + Topics: []gethCommon.Hash{ + RandomCommonHash(t), + RandomCommonHash(t), + }, + Data: RandomData(t), + } +} + +// MakeABalanceInFlow makes a balance object that has `amount` Flow Token in it +func MakeABalanceInFlow(amount uint64) types.Balance { + return types.Balance(uint64(100_000_000) * amount) +} diff --git a/fvm/evm/types/emulator.go b/fvm/evm/types/emulator.go index 73eef076a5b..01577a4132a 100644 --- a/fvm/evm/types/emulator.go +++ b/fvm/evm/types/emulator.go @@ -4,6 +4,7 @@ import ( "math/big" gethTypes "github.com/ethereum/go-ethereum/core/types" + gethVM "github.com/ethereum/go-ethereum/core/vm" ) var ( @@ -14,12 +15,20 @@ var ( BlockNumberForEVMRules = big.NewInt(1) ) +type Precompile interface { + gethVM.PrecompiledContract + Address() Address +} + // BlockContext holds the context needed for the emulator operations type BlockContext struct { BlockNumber uint64 DirectCallBaseGasUsage uint64 DirectCallGasPrice uint64 GasFeeCollector Address + + // a set of extra precompiles to be injected + ExtraPrecompiles []Precompile } // NewDefaultBlockContext returns a new default block context diff --git a/fvm/evm/types/handler.go b/fvm/evm/types/handler.go index 3badb5c6175..bfe187234e8 100644 --- a/fvm/evm/types/handler.go +++ b/fvm/evm/types/handler.go @@ -47,12 +47,16 @@ type Backend interface { environment.ValueStore environment.Meter environment.EventEmitter + environment.BlockInfo } // AddressAllocator allocates addresses, used by the handler type AddressAllocator interface { - // AllocateAddress allocates an address to be used by a bridged account resource - AllocateAddress() (Address, error) + // AllocateAddress allocates an address to be used by a COA resource + AllocateCOAAddress() (Address, error) + + // AllocateAddress allocates an address by index to be used by a precompile contract + AllocatePrecompileAddress(index uint64) Address } // BlockStore stores the chain of blocks