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:
parent
9b4df6a4d0
commit
d5fd4cdcc0
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user