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
This commit is contained in:
Steven Allen 2023-11-17 20:46:49 +01:00 committed by GitHub
parent 9b4df6a4d0
commit d5fd4cdcc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 205 additions and 46 deletions

View File

@ -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))

View File

@ -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

View File

@ -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)
}

View File

@ -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, &ethAddr, 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, &ethAddr, 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, &ethAddr, receipt.ContractAddress)
}

View File

@ -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()

View File

@ -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),