diff --git a/itests/eth_account_abstraction_test.go b/itests/eth_account_abstraction_test.go index b6ad48720..c727c2a73 100644 --- a/itests/eth_account_abstraction_test.go +++ b/itests/eth_account_abstraction_test.go @@ -2,16 +2,19 @@ package itests import ( "context" - "fmt" + "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" @@ -25,9 +28,8 @@ import ( func TestEthAccountAbstraction(t *testing.T) { kit.QuietMiningLogs() - blockTime := 100 * time.Millisecond client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) - ens.InterconnectAll().BeginMining(blockTime) + ens.InterconnectAll().BeginMining(10 * time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -38,8 +40,6 @@ func TestEthAccountAbstraction(t *testing.T) { placeholderAddress, err := client.WalletImport(ctx, &secpKey.KeyInfo) require.NoError(t, err) - fmt.Println(placeholderAddress) - // create an placeholder actor at the target address msgCreatePlaceholder := &types.Message{ From: client.DefaultKey.Address, @@ -50,7 +50,8 @@ func TestEthAccountAbstraction(t *testing.T) { require.NoError(t, err) mLookup, err := client.StateWaitMsg(ctx, smCreatePlaceholder.Cid(), 3, api.LookbackNoLimit, true) require.NoError(t, err) - require.Equal(t, exitcode.Ok, mLookup.Receipt.ExitCode) + + require.True(t, mLookup.Receipt.ExitCode.IsSuccess()) // confirm the placeholder is an placeholder placeholderActor, err := client.StateGetActor(ctx, placeholderAddress, types.EmptyTSK) @@ -58,6 +59,7 @@ func TestEthAccountAbstraction(t *testing.T) { 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{ @@ -82,7 +84,7 @@ func TestEthAccountAbstraction(t *testing.T) { mLookup, err = client.StateWaitMsg(ctx, smFromPlaceholderCid, 3, api.LookbackNoLimit, true) require.NoError(t, err) - require.Equal(t, exitcode.Ok, mLookup.Receipt.ExitCode) + require.True(t, mLookup.Receipt.ExitCode.IsSuccess()) // confirm ugly Placeholder duckling has turned into a beautiful EthAccount swan @@ -118,7 +120,7 @@ func TestEthAccountAbstraction(t *testing.T) { mLookup, err = client.StateWaitMsg(ctx, smFromPlaceholderCid, 3, api.LookbackNoLimit, true) require.NoError(t, err) - require.Equal(t, exitcode.Ok, mLookup.Receipt.ExitCode) + require.True(t, mLookup.Receipt.ExitCode.IsSuccess()) // confirm no changes in code CID @@ -129,3 +131,185 @@ func TestEthAccountAbstraction(t *testing.T) { 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.NewEthTxArgsFromMessage(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.NewEthTxArgsFromMessage(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: ðAddr, + 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") +}