From af39ec27b8e1ebc3861bb4467e4e0d662e382795 Mon Sep 17 00:00:00 2001 From: vyzo Date: Mon, 14 Nov 2022 21:06:55 +0200 Subject: [PATCH] NV18: FEVM: Basic smoke test (#9635) * unknown return types should not be treated as errors from WaitForMessage * simplecoin FEVM smoke test * add itest-fevm to circle matrix * use a named error for metadata lookup failures * hand-write the fevm basic test * make gen * address nits --- .circleci/config.yml | 5 + chain/stmgr/utils.go | 7 +- itests/contracts/SimpleCoin.bin | 1 + itests/contracts/SimpleCoin.sol | 31 ++++++ itests/fevm_test.go | 171 ++++++++++++++++++++++++++++++++ node/impl/full/state.go | 23 +++-- 6 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 itests/contracts/SimpleCoin.bin create mode 100644 itests/contracts/SimpleCoin.sol create mode 100644 itests/fevm_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 4af2bfc12..5180750c1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -911,6 +911,11 @@ workflows: suite: itest-dup_mpool_messages target: "./itests/dup_mpool_messages_test.go" + - test: + name: test-itest-fevm + suite: itest-fevm + target: "./itests/fevm_test.go" + - test: name: test-itest-gas_estimation suite: itest-gas_estimation diff --git a/chain/stmgr/utils.go b/chain/stmgr/utils.go index 062b93b11..0de1d8044 100644 --- a/chain/stmgr/utils.go +++ b/chain/stmgr/utils.go @@ -2,6 +2,7 @@ package stmgr import ( "context" + "errors" "fmt" "reflect" @@ -27,6 +28,8 @@ import ( "github.com/filecoin-project/lotus/node/modules/dtypes" ) +var ErrMetadataNotFound = errors.New("actor metadata not found") + func GetReturnType(ctx context.Context, sm *StateManager, to address.Address, method abi.MethodNum, ts *types.TipSet) (cbg.CBORUnmarshaler, error) { act, err := sm.LoadActor(ctx, to, ts) if err != nil { @@ -35,7 +38,7 @@ func GetReturnType(ctx context.Context, sm *StateManager, to address.Address, me m, found := sm.tsExec.NewActorRegistry().Methods[act.Code][method] if !found { - return nil, fmt.Errorf("unknown method %d for actor %s", method, act.Code) + return nil, fmt.Errorf("unknown method %d for actor %s: %w", method, act.Code, ErrMetadataNotFound) } return reflect.New(m.Ret.Elem()).Interface().(cbg.CBORUnmarshaler), nil @@ -44,7 +47,7 @@ func GetReturnType(ctx context.Context, sm *StateManager, to address.Address, me func GetParamType(ar *vm.ActorRegistry, actCode cid.Cid, method abi.MethodNum) (cbg.CBORUnmarshaler, error) { m, found := ar.Methods[actCode][method] if !found { - return nil, fmt.Errorf("unknown method %d for actor %s", method, actCode) + return nil, fmt.Errorf("unknown method %d for actor %s: %w", method, actCode, ErrMetadataNotFound) } return reflect.New(m.Params.Elem()).Interface().(cbg.CBORUnmarshaler), nil } diff --git a/itests/contracts/SimpleCoin.bin b/itests/contracts/SimpleCoin.bin new file mode 100644 index 000000000..55b83cb12 --- /dev/null +++ b/itests/contracts/SimpleCoin.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506127106000803273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555061051c806100656000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80637bd703e81461004657806390b98a1114610076578063f8b2cb4f146100a6575b600080fd5b610060600480360381019061005b919061030a565b6100d6565b60405161006d9190610350565b60405180910390f35b610090600480360381019061008b9190610397565b6100f4565b60405161009d91906103f2565b60405180910390f35b6100c060048036038101906100bb919061030a565b61025f565b6040516100cd9190610350565b60405180910390f35b600060026100e38361025f565b6100ed919061043c565b9050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410156101455760009050610259565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254610193919061047e565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546101e891906104b2565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161024c9190610350565b60405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102d7826102ac565b9050919050565b6102e7816102cc565b81146102f257600080fd5b50565b600081359050610304816102de565b92915050565b6000602082840312156103205761031f6102a7565b5b600061032e848285016102f5565b91505092915050565b6000819050919050565b61034a81610337565b82525050565b60006020820190506103656000830184610341565b92915050565b61037481610337565b811461037f57600080fd5b50565b6000813590506103918161036b565b92915050565b600080604083850312156103ae576103ad6102a7565b5b60006103bc858286016102f5565b92505060206103cd85828601610382565b9150509250929050565b60008115159050919050565b6103ec816103d7565b82525050565b600060208201905061040760008301846103e3565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061044782610337565b915061045283610337565b925082820261046081610337565b915082820484148315176104775761047661040d565b5b5092915050565b600061048982610337565b915061049483610337565b92508282039050818111156104ac576104ab61040d565b5b92915050565b60006104bd82610337565b91506104c883610337565b92508282019050808211156104e0576104df61040d565b5b9291505056fea26469706673582212205ede41ff9072784ccc19ac18de0781558d305a8139361fa85dc51a8614e47d8c64736f6c63430008110033 \ No newline at end of file diff --git a/itests/contracts/SimpleCoin.sol b/itests/contracts/SimpleCoin.sol new file mode 100644 index 000000000..5318b0cb8 --- /dev/null +++ b/itests/contracts/SimpleCoin.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.4.2; + +contract SimpleCoin { + mapping(address => uint256) balances; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + constructor() { + balances[tx.origin] = 10000; + } + + function sendCoin(address receiver, uint256 amount) + public + returns (bool sufficient) + { + if (balances[msg.sender] < amount) return false; + balances[msg.sender] -= amount; + balances[receiver] += amount; + emit Transfer(msg.sender, receiver, amount); + return true; + } + + function getBalanceInEth(address addr) public view returns (uint256) { + return getBalance(addr) * 2; + } + + function getBalance(address addr) public view returns (uint256) { + return balances[addr]; + } +} diff --git a/itests/fevm_test.go b/itests/fevm_test.go new file mode 100644 index 000000000..662b89763 --- /dev/null +++ b/itests/fevm_test.go @@ -0,0 +1,171 @@ +package itests + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + builtintypes "github.com/filecoin-project/go-state-types/builtin" + "github.com/filecoin-project/go-state-types/builtin/v10/eam" + + "github.com/filecoin-project/lotus/chain/actors" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/itests/kit" +) + +// TestFEVMBasic does a basic fevm contract installation and invocation +func TestFEVMBasic(t *testing.T) { + // TODO the contract installation and invocation can be lifted into utility methods + // He who writes the second test, shall do that. + kit.QuietMiningLogs() + + blockTime := 100 * time.Millisecond + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(blockTime) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // install contract + contractHex, err := os.ReadFile("contracts/SimpleCoin.bin") + require.NoError(t, err) + + contract, err := hex.DecodeString(string(contractHex)) + require.NoError(t, err) + + fromAddr, err := client.WalletDefaultAddress(ctx) + require.NoError(t, err) + + nonce, err := client.MpoolGetNonce(ctx, fromAddr) + if err != nil { + nonce = 0 // assume a zero nonce on error (e.g. sender doesn't exist). + } + + var salt [32]byte + binary.BigEndian.PutUint64(salt[:], nonce) + + method := builtintypes.MethodsEAM.Create2 + params, err := actors.SerializeParams(&eam.Create2Params{ + Initcode: contract, + Salt: salt, + }) + require.NoError(t, err) + + msg := &types.Message{ + To: builtintypes.EthereumAddressManagerActorAddr, + From: fromAddr, + Value: big.Zero(), + Method: method, + Params: params, + } + + t.Log("sending create message") + smsg, err := client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + + t.Log("waiting for message to execute") + wait, err := client.StateWaitMsg(ctx, smsg.Cid(), 0, 0, false) + require.NoError(t, err) + + require.True(t, wait.Receipt.ExitCode.IsSuccess(), "contract installation failed") + + var result eam.CreateReturn + r := bytes.NewReader(wait.Receipt.Return) + err = result.UnmarshalCBOR(r) + require.NoError(t, err) + + idAddr, err := address.NewIDAddress(result.ActorID) + require.NoError(t, err) + t.Logf("actor ID address is %s", idAddr) + + // invoke the contract with owner + { + entryPoint, err := hex.DecodeString("f8b2cb4f") + require.NoError(t, err) + + inputData, err := hex.DecodeString("000000000000000000000000ff00000000000000000000000000000000000064") + require.NoError(t, err) + + params := append(entryPoint, inputData...) + var buffer bytes.Buffer + err = cbg.WriteByteArray(&buffer, params) + require.NoError(t, err) + params = buffer.Bytes() + + msg := &types.Message{ + To: idAddr, + From: fromAddr, + Value: big.Zero(), + Method: abi.MethodNum(2), + Params: params, + } + + t.Log("sending invoke message") + smsg, err := client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + + t.Log("waiting for message to execute") + wait, err := client.StateWaitMsg(ctx, smsg.Cid(), 0, 0, false) + require.NoError(t, err) + + require.True(t, wait.Receipt.ExitCode.IsSuccess(), "contract execution failed") + + result, err := cbg.ReadByteArray(bytes.NewBuffer(wait.Receipt.Return), uint64(len(wait.Receipt.Return))) + require.NoError(t, err) + + expectedResult, err := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000002710") + require.NoError(t, err) + require.Equal(t, result, expectedResult) + } + + // invoke the contract with non owner + { + entryPoint, err := hex.DecodeString("f8b2cb4f") + require.NoError(t, err) + + inputData, err := hex.DecodeString("000000000000000000000000ff00000000000000000000000000000000000065") + require.NoError(t, err) + + params := append(entryPoint, inputData...) + var buffer bytes.Buffer + err = cbg.WriteByteArray(&buffer, params) + require.NoError(t, err) + params = buffer.Bytes() + + msg := &types.Message{ + To: idAddr, + From: fromAddr, + Value: big.Zero(), + Method: abi.MethodNum(2), + Params: params, + } + + t.Log("sending invoke message") + smsg, err := client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + + t.Log("waiting for message to execute") + wait, err := client.StateWaitMsg(ctx, smsg.Cid(), 0, 0, false) + require.NoError(t, err) + + require.True(t, wait.Receipt.ExitCode.IsSuccess(), "contract execution failed") + + result, err := cbg.ReadByteArray(bytes.NewBuffer(wait.Receipt.Return), uint64(len(wait.Receipt.Return))) + require.NoError(t, err) + + expectedResult, err := hex.DecodeString("0000000000000000000000000000000000000000000000000000000000000000") + require.NoError(t, err) + require.Equal(t, result, expectedResult) + + } +} diff --git a/node/impl/full/state.go b/node/impl/full/state.go index f9b4f741c..7e63c5249 100644 --- a/node/impl/full/state.go +++ b/node/impl/full/state.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "strconv" @@ -617,14 +618,22 @@ func (m *StateModule) StateWaitMsg(ctx context.Context, msg cid.Cid, confidence t, err := stmgr.GetReturnType(ctx, m.StateManager, vmsg.To, vmsg.Method, ts) if err != nil { - return nil, xerrors.Errorf("failed to get return type: %w", err) + if errors.Is(err, stmgr.ErrMetadataNotFound) { + // This is not nececessary an error -- EVM methods (and in the future native actors) may + // return just bytes, and in the not so distant future we'll have native wasm actors + // that are by definition not in the registry. + // So in this case, log a debug message and retun the raw bytes. + log.Debugf("failed to get return type: %s", err) + returndec = recpt.Return + } else { + return nil, xerrors.Errorf("failed to get return type: %w", err) + } + } else { + if err := t.UnmarshalCBOR(bytes.NewReader(recpt.Return)); err != nil { + return nil, err + } + returndec = t } - - if err := t.UnmarshalCBOR(bytes.NewReader(recpt.Return)); err != nil { - return nil, err - } - - returndec = t } return &api.MsgLookup{