diff --git a/.circleci/config.yml b/.circleci/config.yml index f04d01a9f38..4be9c710b79 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -764,6 +764,11 @@ workflows: suite: itest-dup_mpool_messages target: "./itests/dup_mpool_messages_test.go" + - test: + name: test-itest-eth_account_abstraction + suite: itest-eth_account_abstraction + target: "./itests/eth_account_abstraction_test.go" + - test: name: test-itest-fevm suite: itest-fevm diff --git a/chain/actors/builtin/builtin.go b/chain/actors/builtin/builtin.go index 7811aab6c07..8ab188317da 100644 --- a/chain/actors/builtin/builtin.go +++ b/chain/actors/builtin/builtin.go @@ -274,6 +274,24 @@ func IsPaymentChannelActor(c cid.Cid) bool { return false } +func IsEmbryoActor(c cid.Cid) bool { + name, _, ok := actors.GetActorMetaByCode(c) + if ok { + return name == "embryo" + } + + return false +} + +func IsEthAccountActor(c cid.Cid) bool { + name, _, ok := actors.GetActorMetaByCode(c) + if ok { + return name == "ethaccount" + } + + return false +} + func makeAddress(addr string) address.Address { ret, err := address.NewFromString(addr) if err != nil { diff --git a/chain/actors/builtin/builtin.go.template b/chain/actors/builtin/builtin.go.template index 76627945139..5cd9b1f7ffa 100644 --- a/chain/actors/builtin/builtin.go.template +++ b/chain/actors/builtin/builtin.go.template @@ -153,6 +153,24 @@ func IsPaymentChannelActor(c cid.Cid) bool { return false } +func IsEmbryoActor(c cid.Cid) bool { + name, _, ok := actors.GetActorMetaByCode(c) + if ok { + return name == "embryo" + } + + return false +} + +func IsEthAccountActor(c cid.Cid) bool { + name, _, ok := actors.GetActorMetaByCode(c) + if ok { + return name == "ethaccount" + } + + return false +} + func makeAddress(addr string) address.Address { ret, err := address.NewFromString(addr) if err != nil { diff --git a/chain/actors/builtin/embryo.go b/chain/actors/builtin/embryo.go deleted file mode 100644 index 6879b7dc519..00000000000 --- a/chain/actors/builtin/embryo.go +++ /dev/null @@ -1,12 +0,0 @@ -package builtin - -import ( - "github.com/ipfs/go-cid" - - "github.com/filecoin-project/lotus/chain/actors" -) - -func IsEmbryo(c cid.Cid) bool { - name, _, ok := actors.GetActorMetaByCode(c) - return ok && name == "embryo" -} diff --git a/chain/consensus/filcns/filecoin.go b/chain/consensus/filcns/filecoin.go index c2cc3aa83d3..0eab28340a8 100644 --- a/chain/consensus/filcns/filecoin.go +++ b/chain/consensus/filcns/filecoin.go @@ -436,15 +436,15 @@ func (filec *FilecoinEC) VerifyWinningPoStProof(ctx context.Context, nv network. return nil } -func isValidForSending(act *types.Actor) bool { - if builtin.IsAccountActor(act.Code) { +func IsValidForSending(act *types.Actor) bool { + if builtin.IsAccountActor(act.Code) || builtin.IsEthAccountActor(act.Code) { return true } - // HACK: Allow Eth embryos to send messages - if !builtin.IsEmbryo(act.Code) || act.Address == nil || act.Address.Protocol() != address.Delegated { + if !builtin.IsEmbryoActor(act.Code) || act.Address == nil || act.Address.Protocol() != address.Delegated { return false } + id, _, err := varint.FromUvarint(act.Address.Payload()) return err == nil && id == builtintypes.EthereumAddressManagerActorID } @@ -521,7 +521,7 @@ func (filec *FilecoinEC) checkBlockMessages(ctx context.Context, b *types.FullBl return xerrors.Errorf("failed to get actor: %w", err) } - if !isValidForSending(act) { + if !IsValidForSending(act) { return xerrors.New("Sender must be an account actor") } nonces[sender] = act.Nonce diff --git a/chain/messagepool/messagepool.go b/chain/messagepool/messagepool.go index c64813078bb..ffcd4c68f0b 100644 --- a/chain/messagepool/messagepool.go +++ b/chain/messagepool/messagepool.go @@ -34,6 +34,7 @@ import ( "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/consensus/filcns" "github.com/filecoin-project/lotus/chain/eth" "github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/store" @@ -806,7 +807,7 @@ func (mp *MessagePool) VerifyMsgSig(m *types.SignedMessage) error { if m.Signature.Type == crypto.SigTypeDelegated { txArgs, err := eth.NewEthTxArgsFromMessage(&m.Message) if err != nil { - return err + return xerrors.Errorf("failed to convert to eth tx args: %w", err) } msg, err := txArgs.OriginalRlpMsg() if err != nil { @@ -873,18 +874,28 @@ func (mp *MessagePool) addTs(ctx context.Context, m *types.SignedMessage, curTs mp.lk.Lock() defer mp.lk.Unlock() + senderAct, err := mp.api.GetActorAfter(m.Message.From, curTs) + if err != nil { + return false, xerrors.Errorf("failed to get sender actor: %w", err) + } + + // TODO: I'm not thrilled about depending on filcns here, but I prefer this to duplicating logic + if !filcns.IsValidForSending(senderAct) { + return false, xerrors.Errorf("sender actor %s is not a valid top-level sender", m.Message.From) + } + publish, err := mp.verifyMsgBeforeAdd(ctx, m, curTs, local) if err != nil { - return false, err + return false, xerrors.Errorf("verify msg failed: %w", err) } if err := mp.checkBalance(ctx, m, curTs); err != nil { - return false, err + return false, xerrors.Errorf("failed to check balance: %w", err) } err = mp.addLocked(ctx, m, !local, untrusted) if err != nil { - return false, err + return false, xerrors.Errorf("failed to add locked: %w", err) } if local { diff --git a/chain/messagepool/messagepool_test.go b/chain/messagepool/messagepool_test.go index 35e13e13d69..c205bbc6a6d 100644 --- a/chain/messagepool/messagepool_test.go +++ b/chain/messagepool/messagepool_test.go @@ -155,7 +155,7 @@ func (tma *testMpoolAPI) GetActorAfter(addr address.Address, ts *types.TipSet) ( } return &types.Actor{ - Code: builtin2.StorageMarketActorCodeID, + Code: builtin2.AccountActorCodeID, Nonce: nonce, Balance: balance, }, nil diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index 75846e10a2e..f9793e0543a 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -284,10 +284,15 @@ func TryEthAddressFromFilecoinAddress(addr address.Address, allowId bool) (EthAd func EthAddressFromFilecoinAddress(addr address.Address) (EthAddress, error) { ethAddr, ok, err := TryEthAddressFromFilecoinAddress(addr, true) - if !ok && err == nil { - err = xerrors.Errorf("failed to convert filecoin address %s to an equivalent eth address", addr) + if err != nil { + return EthAddress{}, xerrors.Errorf("failed to try converting filecoin to eth addr: %w", err) + } + + if !ok { + return EthAddress{}, xerrors.Errorf("failed to convert filecoin address %s to an equivalent eth address", addr) } - return ethAddr, err + + return ethAddr, nil } func EthAddressFromHex(s string) (EthAddress, error) { diff --git a/chain/vm/invoker.go b/chain/vm/invoker.go index 77c57b9c24e..ebf8dd73416 100644 --- a/chain/vm/invoker.go +++ b/chain/vm/invoker.go @@ -285,7 +285,7 @@ func DecodeParams(b []byte, out interface{}) error { func DumpActorState(i *ActorRegistry, act *types.Actor, b []byte) (interface{}, error) { // Account & Embryo code special case - if builtin.IsAccountActor(act.Code) || builtin.IsEmbryo(act.Code) { + if builtin.IsAccountActor(act.Code) || builtin.IsEmbryoActor(act.Code) { return nil, nil } diff --git a/itests/eth_account_abstraction_test.go b/itests/eth_account_abstraction_test.go new file mode 100644 index 00000000000..1ea7e140e46 --- /dev/null +++ b/itests/eth_account_abstraction_test.go @@ -0,0 +1,127 @@ +package itests + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/exitcode" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/eth" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/wallet/key" + "github.com/filecoin-project/lotus/itests/kit" +) + +// TestEthAccountAbstraction goes over the account abstraction workflow: +// - an embryo is created when it receives a message +// - the embryo turns into an EOA when it sends a message +func TestEthAccountAbstraction(t *testing.T) { + kit.QuietMiningLogs() + + blockTime := 100 * 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() + + secpKey, err := key.GenerateKey(types.KTDelegated) + require.NoError(t, err) + + embryoAddress, err := client.WalletImport(ctx, &secpKey.KeyInfo) + require.NoError(t, err) + + fmt.Println(embryoAddress) + + // create an embryo actor at the target address + msgCreateEmbryo := &types.Message{ + From: client.DefaultKey.Address, + To: embryoAddress, + Value: abi.TokenAmount(types.MustParseFIL("100")), + } + smCreateEmbryo, err := client.MpoolPushMessage(ctx, msgCreateEmbryo, nil) + require.NoError(t, err) + mLookup, err := client.StateWaitMsg(ctx, smCreateEmbryo.Cid(), 3, api.LookbackNoLimit, true) + require.NoError(t, err) + require.Equal(t, exitcode.Ok, mLookup.Receipt.ExitCode) + + // confirm the embryo is an embryo + embryoActor, err := client.StateGetActor(ctx, embryoAddress, types.EmptyTSK) + require.NoError(t, err) + + require.True(t, builtin.IsEmbryoActor(embryoActor.Code)) + + // send a message from the embryo address + msgFromEmbryo := &types.Message{ + From: embryoAddress, + // self-send because an "eth tx payload" can't be to a filecoin address? + To: embryoAddress, + } + msgFromEmbryo, err = client.GasEstimateMessageGas(ctx, msgFromEmbryo, nil, types.EmptyTSK) + require.NoError(t, err) + + txArgs, err := eth.NewEthTxArgsFromMessage(msgFromEmbryo) + require.NoError(t, err) + + digest, err := txArgs.OriginalRlpMsg() + require.NoError(t, err) + + siggy, err := client.WalletSign(ctx, embryoAddress, digest) + require.NoError(t, err) + + smFromEmbryoCid, err := client.MpoolPush(ctx, &types.SignedMessage{Message: *msgFromEmbryo, Signature: *siggy}) + require.NoError(t, err) + + mLookup, err = client.StateWaitMsg(ctx, smFromEmbryoCid, 3, api.LookbackNoLimit, true) + require.NoError(t, err) + require.Equal(t, exitcode.Ok, mLookup.Receipt.ExitCode) + + // confirm ugly Embryo duckling has turned into a beautiful EthAccount swan + + eoaActor, err := client.StateGetActor(ctx, embryoAddress, types.EmptyTSK) + require.NoError(t, err) + + require.False(t, builtin.IsEmbryoActor(eoaActor.Code)) + require.True(t, builtin.IsEthAccountActor(eoaActor.Code)) + + // Send another message, it should succeed without any code CID changes + + msgFromEmbryo = &types.Message{ + From: embryoAddress, + To: embryoAddress, + } + + msgFromEmbryo, err = client.GasEstimateMessageGas(ctx, msgFromEmbryo, nil, types.EmptyTSK) + require.NoError(t, err) + + txArgs, err = eth.NewEthTxArgsFromMessage(msgFromEmbryo) + require.NoError(t, err) + + digest, err = txArgs.OriginalRlpMsg() + require.NoError(t, err) + + siggy, err = client.WalletSign(ctx, embryoAddress, digest) + require.NoError(t, err) + + smFromEmbryoCid, err = client.MpoolPush(ctx, &types.SignedMessage{Message: *msgFromEmbryo, Signature: *siggy}) + require.NoError(t, err) + + mLookup, err = client.StateWaitMsg(ctx, smFromEmbryoCid, 3, api.LookbackNoLimit, true) + require.NoError(t, err) + require.Equal(t, exitcode.Ok, mLookup.Receipt.ExitCode) + + // confirm no changes in code CID + + eoaActor, err = client.StateGetActor(ctx, embryoAddress, types.EmptyTSK) + require.NoError(t, err) + + require.False(t, builtin.IsEmbryoActor(eoaActor.Code)) + require.True(t, builtin.IsEthAccountActor(eoaActor.Code)) +}