From d5fd4cdcc007f4249b671c4819cdbbd043c4c614 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 17 Nov 2023 20:46:49 +0100 Subject: [PATCH] feat: eth: encode eth tx input as solidity ABI (#11402) When translating "native" messages to Ethereum transactions, correctly handle parameters: 1. If the message looks like a valid "create external", treat it as a contract creation. 2. If it looks like a valid EVM invocation, decode it as such. 3. Otherwise, ABI-encode the parameters to make them look like a "handle_filecoin_method" call. This will help chain explorers recognize these messages. Part of #11355 --- CHANGELOG.md | 3 + itests/eth_hash_lookup_test.go | 24 ++++++ itests/eth_transactions_test.go | 126 ++++++++++++++++++++++++++++---- itests/fevm_address_test.go | 42 ++++------- itests/kit/evm.go | 12 +++ node/impl/full/eth_utils.go | 44 ++++++++++- 6 files changed, 205 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb54b5f3..aa7535df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ - feat: Added instructions on how to setup Prometheus/Grafana for monitoring a local Lotus node [filecoin-project/lotus#11276](https://github.com/filecoin-project/lotus/pull/11276) - fix: Exclude reverted events in `eth_getLogs` results [filecoin-project/lotus#11318](https://github.com/filecoin-project/lotus/pull/11318) - fix: The Ethereum API will now use the correct state-tree when resolving "native" addresses into masked ID addresses. Additionally, pending messages from native account types won't be visible in the Ethereum API because there is no "correct" state-tree to pick in this case. However, pending _Ethereum_ transactions and native messages that have landed on-chain will still be visible through the Ethereum API. +- feat: Unambiguously translate native messages to Ethereum transactions by: + - Detecting native messages that "look" like Ethereum transactions (creating smart contracts, invoking a smart contract, etc.), and decoding them as such. + - Otherwise, ABI-encoding the inputs as if they were calls to a `handle_filecoin_method` Solidity method. ## New features - feat: Add move-partition command ([filecoin-project/lotus#11290](https://github.com/filecoin-project/lotus/pull/11290)) diff --git a/itests/eth_hash_lookup_test.go b/itests/eth_hash_lookup_test.go index 0bf321088..e1474a90d 100644 --- a/itests/eth_hash_lookup_test.go +++ b/itests/eth_hash_lookup_test.go @@ -187,6 +187,18 @@ func TestTransactionHashLookupBlsFilecoinMessage(t *testing.T) { toEth, err := client.FilecoinAddressToEthAddress(ctx, toId) require.NoError(t, err) require.Equal(t, &toEth, chainTx.To) + + const expectedHex = "868e10c4" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000060" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // verify that the params are correctly encoded. + expected, err := hex.DecodeString(expectedHex) + require.NoError(t, err) + + require.Equal(t, ethtypes.EthBytes(expected), chainTx.Input) } // TestTransactionHashLookupSecpFilecoinMessage tests to see if lotus can find a Secp Filecoin Message using the transaction hash @@ -266,6 +278,18 @@ func TestTransactionHashLookupSecpFilecoinMessage(t *testing.T) { toEth, err := client.FilecoinAddressToEthAddress(ctx, toId) require.NoError(t, err) require.Equal(t, &toEth, chainTx.To) + + const expectedHex = "868e10c4" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000060" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // verify that the params are correctly encoded. + expected, err := hex.DecodeString(expectedHex) + require.NoError(t, err) + + require.Equal(t, ethtypes.EthBytes(expected), chainTx.Input) } // TestTransactionHashLookupSecpFilecoinMessage tests to see if lotus can find a Secp Filecoin Message using the transaction hash diff --git a/itests/eth_transactions_test.go b/itests/eth_transactions_test.go index 6d60f6786..026ac2387 100644 --- a/itests/eth_transactions_test.go +++ b/itests/eth_transactions_test.go @@ -9,11 +9,13 @@ import ( "github.com/stretchr/testify/require" + "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" + builtin2 "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/manifest" - "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" @@ -78,7 +80,7 @@ func TestValueTransferValidSignature(t *testing.T) { client.EVM().SignTransaction(&tx, key.PrivateKey) hash := client.EVM().SubmitTransaction(ctx, &tx) - receipt, err := waitForEthTxReceipt(ctx, client, hash) + receipt, err := client.EVM().WaitTransaction(ctx, hash) require.NoError(t, err) require.NotNil(t, receipt) require.EqualValues(t, ethAddr, receipt.From) @@ -166,7 +168,7 @@ func TestContractDeploymentValidSignature(t *testing.T) { client.EVM().SignTransaction(tx, key.PrivateKey) hash := client.EVM().SubmitTransaction(ctx, tx) - receipt, err := waitForEthTxReceipt(ctx, client, hash) + receipt, err := client.EVM().WaitTransaction(ctx, hash) require.NoError(t, err) require.NotNil(t, receipt) @@ -213,7 +215,7 @@ func TestContractInvocation(t *testing.T) { client.EVM().SignTransaction(tx, key.PrivateKey) hash := client.EVM().SubmitTransaction(ctx, tx) - receipt, err := waitForEthTxReceipt(ctx, client, hash) + receipt, err := client.EVM().WaitTransaction(ctx, hash) require.NoError(t, err) require.NotNil(t, receipt) require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status) @@ -267,7 +269,7 @@ func TestContractInvocation(t *testing.T) { client.EVM().SignTransaction(&invokeTx, key.PrivateKey) hash = client.EVM().SubmitTransaction(ctx, &invokeTx) - receipt, err = waitForEthTxReceipt(ctx, client, hash) + receipt, err = client.EVM().WaitTransaction(ctx, hash) require.NoError(t, err) require.NotNil(t, receipt) @@ -376,16 +378,108 @@ func deployContractTx(ctx context.Context, client *kit.TestFullNode, ethAddr eth }, nil } -func waitForEthTxReceipt(ctx context.Context, client *kit.TestFullNode, hash ethtypes.EthHash) (*api.EthTxReceipt, error) { - var receipt *api.EthTxReceipt - var err error - for i := 0; i < 10000000000; i++ { - receipt, err = client.EthGetTransactionReceipt(ctx, hash) - if err != nil || receipt == nil { - time.Sleep(500 * time.Millisecond) - continue - } - break +func TestEthTxFromNativeAccount(t *testing.T) { + blockTime := 10 * 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() + + msg := &types.Message{ + From: client.DefaultKey.Address, + To: client.DefaultKey.Address, + Value: abi.TokenAmount(types.MustParseFIL("100")), + Method: builtin2.MethodsEVM.InvokeContract, } - return receipt, err + + // Send a message with no input. + + sMsg, err := client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + client.WaitMsg(ctx, sMsg.Cid()) + + hash, err := client.EthGetTransactionHashByCid(ctx, sMsg.Cid()) + require.NoError(t, err) + tx, err := client.EthGetTransactionByHash(ctx, hash) + require.NoError(t, err) + + // Expect empty input params given that we "invoked" the contract (well, invoked ourselves). + require.Equal(t, ethtypes.EthBytes{}, tx.Input) + + // Send a message with some input. + + input := abi.CborBytes([]byte{0x1, 0x2, 0x3, 0x4}) + msg.Params, err = actors.SerializeParams(&input) + require.NoError(t, err) + + sMsg, err = client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + client.WaitMsg(ctx, sMsg.Cid()) + hash, err = client.EthGetTransactionHashByCid(ctx, sMsg.Cid()) + require.NoError(t, err) + tx, err = client.EthGetTransactionByHash(ctx, hash) + require.NoError(t, err) + + // Expect the decoded input. + require.EqualValues(t, input, tx.Input) + + // Invoke the contract, but with incorrectly encoded input. We expect this to be abi-encoded + // as if it were any other method call. + + msg.Params = input + require.NoError(t, err) + + sMsg, err = client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + client.WaitMsg(ctx, sMsg.Cid()) + hash, err = client.EthGetTransactionHashByCid(ctx, sMsg.Cid()) + require.NoError(t, err) + tx, err = client.EthGetTransactionByHash(ctx, hash) + require.NoError(t, err) + + const expectedHex1 = "868e10c4" + // "handle filecoin method" function selector + // InvokeEVM method number + "00000000000000000000000000000000000000000000000000000000e525aa15" + + // CBOR multicodec (0x51) + "0000000000000000000000000000000000000000000000000000000000000051" + + // Offset + "0000000000000000000000000000000000000000000000000000000000000060" + + // Number of bytes in the input (4) + "0000000000000000000000000000000000000000000000000000000000000004" + + // Input: 1, 2, 3, 4 + "0102030400000000000000000000000000000000000000000000000000000000" + + input, err = hex.DecodeString(expectedHex1) + require.NoError(t, err) + require.EqualValues(t, input, tx.Input) + + // Invoke a random method with the same input. We expect the same result as above, but with + // a different method number. + + msg.Method++ + + sMsg, err = client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + client.WaitMsg(ctx, sMsg.Cid()) + hash, err = client.EthGetTransactionHashByCid(ctx, sMsg.Cid()) + require.NoError(t, err) + tx, err = client.EthGetTransactionByHash(ctx, hash) + require.NoError(t, err) + + const expectedHex2 = "868e10c4" + // "handle filecoin method" function selector + // InvokeEVM+1 + "00000000000000000000000000000000000000000000000000000000e525aa16" + + // CBOR multicodec (0x51) + "0000000000000000000000000000000000000000000000000000000000000051" + + // Offset + "0000000000000000000000000000000000000000000000000000000000000060" + + // Number of bytes in the input (4) + "0000000000000000000000000000000000000000000000000000000000000004" + + // Input: 1, 2, 3, 4 + "0102030400000000000000000000000000000000000000000000000000000000" + input, err = hex.DecodeString(expectedHex2) + require.NoError(t, err) + require.EqualValues(t, input, tx.Input) } diff --git a/itests/fevm_address_test.go b/itests/fevm_address_test.go index 9eaac4647..aea56264e 100644 --- a/itests/fevm_address_test.go +++ b/itests/fevm_address_test.go @@ -1,7 +1,6 @@ package itests import ( - "bytes" "context" "encoding/hex" "os" @@ -14,8 +13,6 @@ import ( "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/go-state-types/exitcode" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors" @@ -43,7 +40,7 @@ func effectiveEthAddressForCreate(t *testing.T, sender address.Address) ethtypes panic("unreachable") } -func createAndDeploy(ctx context.Context, t *testing.T, client *kit.TestFullNode, fromAddr address.Address, contract []byte) *api.MsgLookup { +func createAndDeploy(ctx context.Context, t *testing.T, client *kit.TestFullNode, fromAddr address.Address, contract []byte) *api.EthTxReceipt { // Create and deploy evm actor method := builtintypes.MethodsEAM.CreateExternal @@ -61,21 +58,13 @@ func createAndDeploy(ctx context.Context, t *testing.T, client *kit.TestFullNode smsg, err := client.MpoolPushMessage(ctx, createMsg, nil) require.NoError(t, err) - wait, err := client.StateWaitMsg(ctx, smsg.Cid(), 0, 0, false) - require.NoError(t, err) - require.Equal(t, exitcode.Ok, wait.Receipt.ExitCode) - return wait -} - -func getEthAddressTX(ctx context.Context, t *testing.T, client *kit.TestFullNode, wait *api.MsgLookup, ethAddr ethtypes.EthAddress) ethtypes.EthAddress { - // Check if eth address returned from CreateExternal is the same as eth address predicted at the start - var createExternalReturn eam.CreateExternalReturn - err := createExternalReturn.UnmarshalCBOR(bytes.NewReader(wait.Receipt.Return)) + txHash, err := client.EthGetTransactionHashByCid(ctx, smsg.Cid()) require.NoError(t, err) - createdEthAddr, err := ethtypes.CastEthAddress(createExternalReturn.EthAddress[:]) + receipt, err := client.EVM().WaitTransaction(ctx, *txHash) require.NoError(t, err) - return createdEthAddr + require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status) + return receipt } func TestAddressCreationBeforeDeploy(t *testing.T) { @@ -112,11 +101,11 @@ func TestAddressCreationBeforeDeploy(t *testing.T) { require.True(t, builtin.IsPlaceholderActor(actor.Code)) // Create and deploy evm actor - wait := createAndDeploy(ctx, t, client, fromAddr, contract) + receipt := createAndDeploy(ctx, t, client, fromAddr, contract) - // Check if eth address returned from CreateExternal is the same as eth address predicted at the start - createdEthAddr := getEthAddressTX(ctx, t, client, wait, ethAddr) - require.Equal(t, ethAddr, createdEthAddr) + // Check if eth address returned from CreateExternal is the same as eth address predicted at + // the start + require.Equal(t, ðAddr, receipt.ContractAddress) // Check if newly deployed actor still has funds actorPostCreate, err := client.StateGetActor(ctx, contractFilAddr, types.EmptyTSK) @@ -158,11 +147,11 @@ func TestDeployAddressMultipleTimes(t *testing.T) { require.True(t, builtin.IsPlaceholderActor(actor.Code)) // Create and deploy evm actor - wait := createAndDeploy(ctx, t, client, fromAddr, contract) + receipt := createAndDeploy(ctx, t, client, fromAddr, contract) - // Check if eth address returned from CreateExternal is the same as eth address predicted at the start - createdEthAddr := getEthAddressTX(ctx, t, client, wait, ethAddr) - require.Equal(t, ethAddr, createdEthAddr) + // Check if eth address returned from CreateExternal is the same as eth address predicted at + // the start + require.Equal(t, ðAddr, receipt.ContractAddress) // Check if newly deployed actor still has funds actorPostCreate, err := client.StateGetActor(ctx, contractFilAddr, types.EmptyTSK) @@ -171,10 +160,9 @@ func TestDeployAddressMultipleTimes(t *testing.T) { require.True(t, builtin.IsEvmActor(actorPostCreate.Code)) // Create and deploy evm actor - wait = createAndDeploy(ctx, t, client, fromAddr, contract) + receipt = createAndDeploy(ctx, t, client, fromAddr, contract) // Check that this time eth address returned from CreateExternal is not the same as eth address predicted at the start - createdEthAddr = getEthAddressTX(ctx, t, client, wait, ethAddr) - require.NotEqual(t, ethAddr, createdEthAddr) + require.NotEqual(t, ðAddr, receipt.ContractAddress) } diff --git a/itests/kit/evm.go b/itests/kit/evm.go index 99844ca30..0d7af2578 100644 --- a/itests/kit/evm.go +++ b/itests/kit/evm.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" cbg "github.com/whyrusleeping/cbor-gen" "golang.org/x/crypto/sha3" + "golang.org/x/xerrors" "github.com/filecoin-project/go-address" amt4 "github.com/filecoin-project/go-amt-ipld/v4" @@ -291,6 +292,17 @@ func (e *EVM) InvokeContractByFuncNameExpectExit(ctx context.Context, fromAddr a require.Equal(e.t, exit, wait.Receipt.ExitCode) } +func (e *EVM) WaitTransaction(ctx context.Context, hash ethtypes.EthHash) (*api.EthTxReceipt, error) { + if mcid, err := e.EthGetMessageCidByTransactionHash(ctx, &hash); err != nil { + return nil, err + } else if mcid == nil { + return nil, xerrors.Errorf("couldn't find message CID for txn hash: %s", hash) + } else { + e.WaitMsg(ctx, *mcid) + return e.EthGetTransactionReceipt(ctx, hash) + } +} + // function signatures are the first 4 bytes of the hash of the function name and types func CalcFuncSignature(funcName string) []byte { hasher := sha3.NewLegacyKeccak256() diff --git a/node/impl/full/eth_utils.go b/node/impl/full/eth_utils.go index 8bbb58b9b..7b5a35b40 100644 --- a/node/impl/full/eth_utils.go +++ b/node/impl/full/eth_utils.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/ipfs/go-cid" + "github.com/multiformats/go-multicodec" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" @@ -465,19 +466,23 @@ func newEthTxFromSignedMessage(smsg *types.SignedMessage, st *state.StateTree) ( tx = ethTxFromNativeMessage(smsg.VMMessage(), st) tx.Hash, err = ethtypes.EthHashFromCid(smsg.Cid()) if err != nil { - return tx, err + return ethtypes.EthTx{}, err } } else { // BLS Filecoin message tx = ethTxFromNativeMessage(smsg.VMMessage(), st) tx.Hash, err = ethtypes.EthHashFromCid(smsg.Message.Cid()) if err != nil { - return tx, err + return ethtypes.EthTx{}, err } } return tx, nil } +// Convert a native message to an eth transaction. +// +// - The state-tree must be from after the message was applied (ideally the following tipset). +// // ethTxFromNativeMessage does NOT populate: // - BlockHash // - BlockNumber @@ -487,9 +492,42 @@ func ethTxFromNativeMessage(msg *types.Message, st *state.StateTree) ethtypes.Et // We don't care if we error here, conversion is best effort for non-eth transactions from, _ := lookupEthAddress(msg.From, st) to, _ := lookupEthAddress(msg.To, st) + toPtr := &to + + // Convert the input parameters to "solidity ABI". + + // For empty, we use "0" as the codec. Otherwise, we use CBOR for message + // parameters. + var codec uint64 + if len(msg.Params) > 0 { + codec = uint64(multicodec.Cbor) + } + + // We try to decode the input as an EVM method invocation and/or a contract creation. If + // that fails, we encode the "native" parameters as Solidity ABI. + var input []byte + switch msg.Method { + case builtintypes.MethodsEVM.InvokeContract, builtintypes.MethodsEAM.CreateExternal: + inp, err := decodePayload(msg.Params, codec) + if err == nil { + // If this is a valid "create external", unset the "to" address. + if msg.Method == builtintypes.MethodsEAM.CreateExternal { + toPtr = nil + } + input = []byte(inp) + break + } + // Yeah, we're going to ignore errors here because the user can send whatever they + // want and may send garbage. + fallthrough + default: + input = encodeFilecoinParamsAsABI(msg.Method, codec, msg.Params) + } + return ethtypes.EthTx{ - To: &to, + To: toPtr, From: from, + Input: input, Nonce: ethtypes.EthUint64(msg.Nonce), ChainID: ethtypes.EthUint64(build.Eip155ChainId), Value: ethtypes.EthBigInt(msg.Value),