lotus/itests/eth_account_abstraction_test.go

316 lines
11 KiB
Go

package itests
import (
"context"
"encoding/hex"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/exitcode"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/actors/builtin"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/types/ethtypes"
"github.com/filecoin-project/lotus/chain/wallet/key"
"github.com/filecoin-project/lotus/itests/kit"
)
// TestEthAccountAbstraction goes over the account abstraction workflow:
// - an placeholder is created when it receives a message
// - the placeholder turns into an EOA when it sends a message
func TestEthAccountAbstraction(t *testing.T) {
kit.QuietMiningLogs()
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
ens.InterconnectAll().BeginMining(10 * time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
secpKey, err := key.GenerateKey(types.KTDelegated)
require.NoError(t, err)
placeholderAddress, err := client.WalletImport(ctx, &secpKey.KeyInfo)
require.NoError(t, err)
// create an placeholder actor at the target address
msgCreatePlaceholder := &types.Message{
From: client.DefaultKey.Address,
To: placeholderAddress,
Value: abi.TokenAmount(types.MustParseFIL("100")),
}
smCreatePlaceholder, err := client.MpoolPushMessage(ctx, msgCreatePlaceholder, nil)
require.NoError(t, err)
mLookup, err := client.StateWaitMsg(ctx, smCreatePlaceholder.Cid(), 3, api.LookbackNoLimit, true)
require.NoError(t, err)
require.True(t, mLookup.Receipt.ExitCode.IsSuccess())
// confirm the placeholder is an placeholder
placeholderActor, err := client.StateGetActor(ctx, placeholderAddress, types.EmptyTSK)
require.NoError(t, err)
require.Equal(t, uint64(0), placeholderActor.Nonce)
require.True(t, builtin.IsPlaceholderActor(placeholderActor.Code))
require.Equal(t, msgCreatePlaceholder.Value, placeholderActor.Balance)
// send a message from the placeholder address
msgFromPlaceholder := &types.Message{
From: placeholderAddress,
// self-send because an "eth tx payload" can't be to a filecoin address?
To: placeholderAddress,
}
msgFromPlaceholder, err = client.GasEstimateMessageGas(ctx, msgFromPlaceholder, nil, types.EmptyTSK)
require.NoError(t, err)
txArgs, err := ethtypes.EthTxArgsFromMessage(msgFromPlaceholder)
require.NoError(t, err)
digest, err := txArgs.ToRlpUnsignedMsg()
require.NoError(t, err)
siggy, err := client.WalletSign(ctx, placeholderAddress, digest)
require.NoError(t, err)
smFromPlaceholderCid, err := client.MpoolPush(ctx, &types.SignedMessage{Message: *msgFromPlaceholder, Signature: *siggy})
require.NoError(t, err)
mLookup, err = client.StateWaitMsg(ctx, smFromPlaceholderCid, 3, api.LookbackNoLimit, true)
require.NoError(t, err)
require.True(t, mLookup.Receipt.ExitCode.IsSuccess())
// confirm ugly Placeholder duckling has turned into a beautiful EthAccount swan
eoaActor, err := client.StateGetActor(ctx, placeholderAddress, types.EmptyTSK)
require.NoError(t, err)
require.False(t, builtin.IsPlaceholderActor(eoaActor.Code))
require.True(t, builtin.IsEthAccountActor(eoaActor.Code))
require.Equal(t, uint64(1), eoaActor.Nonce)
// Send another message, it should succeed without any code CID changes
msgFromPlaceholder = &types.Message{
From: placeholderAddress,
To: placeholderAddress,
Nonce: 1,
}
msgFromPlaceholder, err = client.GasEstimateMessageGas(ctx, msgFromPlaceholder, nil, types.EmptyTSK)
require.NoError(t, err)
txArgs, err = ethtypes.EthTxArgsFromMessage(msgFromPlaceholder)
require.NoError(t, err)
digest, err = txArgs.ToRlpUnsignedMsg()
require.NoError(t, err)
siggy, err = client.WalletSign(ctx, placeholderAddress, digest)
require.NoError(t, err)
smFromPlaceholderCid, err = client.MpoolPush(ctx, &types.SignedMessage{Message: *msgFromPlaceholder, Signature: *siggy})
require.NoError(t, err)
mLookup, err = client.StateWaitMsg(ctx, smFromPlaceholderCid, 3, api.LookbackNoLimit, true)
require.NoError(t, err)
require.True(t, mLookup.Receipt.ExitCode.IsSuccess())
// confirm no changes in code CID
eoaActor, err = client.StateGetActor(ctx, placeholderAddress, types.EmptyTSK)
require.NoError(t, err)
require.Equal(t, uint64(2), eoaActor.Nonce)
require.False(t, builtin.IsPlaceholderActor(eoaActor.Code))
require.True(t, builtin.IsEthAccountActor(eoaActor.Code))
}
// Tests that an placeholder turns into an EthAccout even if the message fails
func TestEthAccountAbstractionFailure(t *testing.T) {
kit.QuietMiningLogs()
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
ens.InterconnectAll().BeginMining(10 * time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
secpKey, err := key.GenerateKey(types.KTDelegated)
require.NoError(t, err)
placeholderAddress, err := client.WalletImport(ctx, &secpKey.KeyInfo)
require.NoError(t, err)
// create a placeholder actor at the target address
msgCreatePlaceholder := &types.Message{
From: client.DefaultKey.Address,
To: placeholderAddress,
Value: abi.TokenAmount(types.MustParseFIL("100")),
}
smCreatePlaceholder, err := client.MpoolPushMessage(ctx, msgCreatePlaceholder, nil)
require.NoError(t, err)
mLookup, err := client.StateWaitMsg(ctx, smCreatePlaceholder.Cid(), 3, api.LookbackNoLimit, true)
require.NoError(t, err)
require.True(t, mLookup.Receipt.ExitCode.IsSuccess())
// confirm the placeholder is an placeholder
placeholderActor, err := client.StateGetActor(ctx, placeholderAddress, types.EmptyTSK)
require.NoError(t, err)
require.Equal(t, uint64(0), placeholderActor.Nonce)
require.True(t, builtin.IsPlaceholderActor(placeholderActor.Code))
require.Equal(t, msgCreatePlaceholder.Value, placeholderActor.Balance)
// send a message from the placeholder address
msgFromPlaceholder := &types.Message{
From: placeholderAddress,
To: placeholderAddress,
Value: abi.TokenAmount(types.MustParseFIL("20")),
}
msgFromPlaceholder, err = client.GasEstimateMessageGas(ctx, msgFromPlaceholder, nil, types.EmptyTSK)
require.NoError(t, err)
msgFromPlaceholder.Value = abi.TokenAmount(types.MustParseFIL("1000"))
txArgs, err := ethtypes.EthTxArgsFromMessage(msgFromPlaceholder)
require.NoError(t, err)
digest, err := txArgs.ToRlpUnsignedMsg()
require.NoError(t, err)
siggy, err := client.WalletSign(ctx, placeholderAddress, digest)
require.NoError(t, err)
smFromPlaceholderCid, err := client.MpoolPush(ctx, &types.SignedMessage{Message: *msgFromPlaceholder, Signature: *siggy})
require.NoError(t, err)
mLookup, err = client.StateWaitMsg(ctx, smFromPlaceholderCid, 3, api.LookbackNoLimit, true)
require.NoError(t, err)
// message should have failed because we didn't have enough $$$
require.Equal(t, exitcode.SysErrInsufficientFunds, mLookup.Receipt.ExitCode)
// BUT, ugly Placeholder duckling should have turned into a beautiful EthAccount swan anyway
eoaActor, err := client.StateGetActor(ctx, placeholderAddress, types.EmptyTSK)
require.NoError(t, err)
require.False(t, builtin.IsPlaceholderActor(eoaActor.Code))
require.True(t, builtin.IsEthAccountActor(eoaActor.Code))
require.Equal(t, uint64(1), eoaActor.Nonce)
// Send a valid message now, it should succeed without any code CID changes
msgFromPlaceholder = &types.Message{
From: placeholderAddress,
To: placeholderAddress,
Nonce: 1,
Value: abi.NewTokenAmount(1),
}
msgFromPlaceholder, err = client.GasEstimateMessageGas(ctx, msgFromPlaceholder, nil, types.EmptyTSK)
require.NoError(t, err)
txArgs, err = ethtypes.EthTxArgsFromMessage(msgFromPlaceholder)
require.NoError(t, err)
digest, err = txArgs.ToRlpUnsignedMsg()
require.NoError(t, err)
siggy, err = client.WalletSign(ctx, placeholderAddress, digest)
require.NoError(t, err)
smFromPlaceholderCid, err = client.MpoolPush(ctx, &types.SignedMessage{Message: *msgFromPlaceholder, Signature: *siggy})
require.NoError(t, err)
mLookup, err = client.StateWaitMsg(ctx, smFromPlaceholderCid, 3, api.LookbackNoLimit, true)
require.NoError(t, err)
require.True(t, mLookup.Receipt.ExitCode.IsSuccess())
// confirm no changes in code CID
eoaActor, err = client.StateGetActor(ctx, placeholderAddress, types.EmptyTSK)
require.NoError(t, err)
require.Equal(t, uint64(2), eoaActor.Nonce)
require.False(t, builtin.IsPlaceholderActor(eoaActor.Code))
require.True(t, builtin.IsEthAccountActor(eoaActor.Code))
}
// Tests that f4 addresess that aren't placeholders/ethaccounts can't be top-level senders
func TestEthAccountAbstractionFailsFromEvmActor(t *testing.T) {
kit.QuietMiningLogs()
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
ens.InterconnectAll().BeginMining(10 * time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
// create a new Ethereum account
key, ethAddr, deployer := client.EVM().NewAccount()
// send some funds to the f410 address
kit.SendFunds(ctx, t, client, deployer, types.FromFil(10))
// install a contract from the placeholder
contractHex, err := os.ReadFile("./contracts/SimpleCoin.hex")
require.NoError(t, err)
contract, err := hex.DecodeString(string(contractHex))
require.NoError(t, err)
gaslimit, err := client.EthEstimateGas(ctx, ethtypes.EthCall{
From: &ethAddr,
Data: contract,
})
require.NoError(t, err)
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
require.NoError(t, err)
tx := ethtypes.EthTxArgs{
ChainID: build.Eip155ChainId,
Value: big.Zero(),
Nonce: 0,
MaxFeePerGas: types.NanoFil,
MaxPriorityFeePerGas: big.Int(maxPriorityFeePerGas),
GasLimit: int(gaslimit),
Input: contract,
V: big.Zero(),
R: big.Zero(),
S: big.Zero(),
}
client.EVM().SignTransaction(&tx, key.PrivateKey)
client.EVM().SubmitTransaction(ctx, &tx)
smsg, err := tx.ToSignedMessage()
require.NoError(t, err)
ml, err := client.StateWaitMsg(ctx, smsg.Cid(), 1, api.LookbackNoLimit, true)
require.NoError(t, err)
require.True(t, ml.Receipt.ExitCode.IsSuccess())
// Get contract address, assert it's an EVM actor
contractAddr, err := client.EVM().ComputeContractAddress(ethAddr, 0).ToFilecoinAddress()
require.NoError(t, err)
client.AssertActorType(ctx, contractAddr, "evm")
msgFromContract := &types.Message{
From: contractAddr,
To: contractAddr,
}
_, err = client.GasEstimateMessageGas(ctx, msgFromContract, nil, types.EmptyTSK)
require.Error(t, err, "expected gas estimation to fail")
require.Contains(t, err.Error(), "SysErrSenderInvalid")
}