refactor(accounts): Remove UserOperation in favour of using Tx for abstracted accounts (#19395)

This commit is contained in:
testinginprod
2024-02-15 12:24:33 +00:00
committed by GitHub
parent e46d526b46
commit 4a88a58e1b
54 changed files with 5867 additions and 10464 deletions
+12 -311
View File
@@ -7,13 +7,6 @@ import (
"testing"
"cosmossdk.io/simapp"
"cosmossdk.io/x/accounts"
rotationv1 "cosmossdk.io/x/accounts/testing/rotation/v1"
accountsv1 "cosmossdk.io/x/accounts/v1"
"cosmossdk.io/x/bank/testutil"
banktypes "cosmossdk.io/x/bank/types"
"cosmossdk.io/x/nft"
stakingtypes "cosmossdk.io/x/staking/types"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
@@ -28,6 +21,7 @@ var (
aliceAddr = secp256k1.GenPrivKey().PubKey().Address()
)
/*
func TestAccountAbstraction(t *testing.T) {
app := setupApp(t)
ak := app.AccountsKeeper
@@ -59,316 +53,23 @@ func TestAccountAbstraction(t *testing.T) {
aliceAddrStr, err := app.AuthKeeper.AddressCodec().BytesToString(aliceAddr)
require.NoError(t, err)
t.Run("fail - tx compat in bundle is not allowed", func(t *testing.T) {
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1stake"), // the sender is the AA, so it has the coins and wants to pay the bundler for the gas
}),
TxCompat: &accountsv1.TxCompat{},
})
require.Contains(t, resp.Error, accounts.ErrDisallowedTxCompatInBundle.Error())
})
t.Run("ok - pay bundler not implemented", func(t *testing.T) {})
t.Run("pay bundle impersonation", func(t *testing.T) {})
t.Run("auth failure", func(t *testing.T) {})
t.Run("pay bundle failure", func(t *testing.T) {})
t.Run("exec message failure", func(t *testing.T) {})
t.Run("ok - pay bundler and exec not implemented", func(t *testing.T) {
// we simulate executing an user operation in an abstracted account
// which only implements the authentication.
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1stake"), // the sender is the AA, so it has the coins and wants to pay the bundler for the gas
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: aliceAddrStr,
Amount: coins(t, "2000stake"), // as the real action the sender wants to send coins to alice
}),
ExecutionGasLimit: 38000,
})
require.Empty(t, resp.Error) // no error
require.Len(t, resp.BundlerPaymentResponses, 1)
require.Len(t, resp.ExecutionResponses, 1)
require.NotZero(t, resp.ExecutionGasUsed)
require.NotZero(t, resp.BundlerPaymentGasUsed)
require.NotZero(t, resp.AuthenticationGasUsed)
// assert there were state changes
balanceIs(t, ctx, app, bundlerAddr.Bytes(), "1stake") // pay bundler state change
balanceIs(t, ctx, app, aliceAddr.Bytes(), "2000stake") // execute messages state change.
})
t.Run("pay bundle impersonation", func(t *testing.T) {
// we simulate the execution of an abstracted account
// which only implements authentication and tries in the pay
// bundler messages the account tries to impersonate another one.
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: bundlerAddrStr, // abstracted account tries to send money from bundler to itself.
ToAddress: aaAddrStr,
Amount: coins(t, "1stake"),
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: aliceAddrStr,
Amount: coins(t, "2000stake"), // as the real action the sender wants to send coins to alice
}),
ExecutionGasLimit: 36000,
})
require.Contains(t, resp.Error, accounts.ErrUnauthorized.Error()) // error is unauthorized
require.Empty(t, resp.BundlerPaymentResponses) // no bundler payment responses, since the atomic exec failed
require.Empty(t, resp.ExecutionResponses) // no execution responses, since the atomic exec failed
require.Zero(t, resp.ExecutionGasUsed) // no execution gas used, since the atomic exec failed
require.NotZero(t, resp.BundlerPaymentGasUsed) // bundler payment gas used, even if the atomic exec failed
})
t.Run("exec message impersonation", func(t *testing.T) {
// we simulate a case in which the abstracted account tries to impersonate
// someone else in the execution of messages.
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1stake"),
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aliceAddrStr, // abstracted account attempts to send money from alice to itself
ToAddress: aaAddrStr,
Amount: coins(t, "2000stake"),
}),
ExecutionGasLimit: 36000,
})
require.Contains(t, resp.Error, accounts.ErrUnauthorized.Error()) // error is unauthorized
require.NotEmpty(t, resp.BundlerPaymentResponses) // bundler payment responses, since the bundler payment succeeded
require.Empty(t, resp.ExecutionResponses) // no execution responses, since the atomic exec failed
require.NotZero(t, resp.ExecutionGasUsed) // execution gas used, even if the atomic exec failed
require.NotZero(t, resp.BundlerPaymentGasUsed) // bundler payment gas used, even if the atomic exec failed
})
t.Run("auth failure", func(t *testing.T) {
// if auth fails nothing more should be attempted, the authentication
// should have spent gas and the error should be returned.
// we simulate a case in which the abstracted account tries to impersonate
// someone else in the execution of messages.
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "invalid",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1stake"),
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aliceAddrStr, // abstracted account attempts to send money from alice to itself
ToAddress: aaAddrStr,
Amount: coins(t, "2000stake"),
}),
ExecutionGasLimit: 36000,
})
require.Contains(t, resp.Error, accounts.ErrAuthentication.Error()) // error is authentication
require.Empty(t, resp.BundlerPaymentResponses) // no bundler payment responses, since the atomic exec failed
require.Empty(t, resp.ExecutionResponses) // no execution responses, since the atomic exec failed
require.Zero(t, resp.ExecutionGasUsed) // no execution gas used, since the atomic exec failed
require.Zero(t, resp.BundlerPaymentGasUsed) // no bundler payment gas used, since the atomic exec failed
require.NotZero(t, resp.AuthenticationGasUsed) // authentication gas used, even if the atomic exec failed
})
t.Run("pay bundle failure", func(t *testing.T) {
// pay bundler fails, nothing more should be attempted, the authentication
// succeeded. We expect gas used in auth and pay bundler step.
// we simulate a case in which the abstracted account tries to impersonate
// someone else in the execution of messages.
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1atom"), // abstracted account does not have enough money to pay the bundler, since it does not hold atom
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aliceAddrStr, // abstracted account attempts to send money from alice to itself
ToAddress: aaAddrStr,
Amount: coins(t, "2000stake"),
}),
ExecutionGasLimit: 36000,
})
require.Contains(t, resp.Error, accounts.ErrBundlerPayment.Error()) // error is bundler payment
require.Empty(t, resp.BundlerPaymentResponses) // no bundler payment responses, since the atomic exec failed
require.Empty(t, resp.ExecutionResponses) // no execution responses, since the atomic exec failed
require.Zero(t, resp.ExecutionGasUsed) // no execution gas used, since the atomic exec failed
require.NotZero(t, resp.BundlerPaymentGasUsed) // bundler payment gas used, even if the atomic exec failed
require.NotZero(t, resp.AuthenticationGasUsed) // authentication gas used, even if the atomic exec failed
})
t.Run("exec message failure", func(t *testing.T) {
// execution message fails, nothing more should be attempted, the authentication
// and pay bundler succeeded. We expect gas used in auth, pay bundler and
// execution step.
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1stake"),
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: aliceAddrStr,
Amount: coins(t, "2000atom"), // abstracted account does not have enough money to pay alice, since it does not hold atom
}),
ExecutionGasLimit: 36000,
})
require.Contains(t, resp.Error, accounts.ErrExecution.Error()) // error is execution
require.Len(t, resp.BundlerPaymentResponses, 1) // bundler payment response, since the pay bundler succeeded
require.Empty(t, resp.ExecutionResponses) // no execution responses, since the atomic exec failed
require.NotZero(t, resp.ExecutionGasUsed) // execution gas used, even if the atomic exec failed
require.NotZero(t, resp.BundlerPaymentGasUsed) // bundler payment gas used, even if the atomic exec failed
require.NotZero(t, resp.AuthenticationGasUsed) // authentication gas used, even if the atomic exec failed
})
t.Run("implements bundler payment - fail ", func(t *testing.T) {})
t.Run("implements bundler payment - fail ", func(t *testing.T) {
// we assert that if an aa implements the bundler payment interface, then
// that is called.
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaFullAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaFullAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1stake"), // we expect this to fail since the account is implement in such a way not to allow bank sends.
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaFullAddrStr,
ToAddress: aliceAddrStr,
Amount: coins(t, "2000stake"),
}),
ExecutionGasLimit: 36000,
})
// in order to assert the call we expect an error to be returned.
require.Contains(t, resp.Error, accounts.ErrBundlerPayment.Error()) // error is bundler payment
require.Contains(t, resp.Error, "this account does not allow bank send messages") // error is bundler payment
})
t.Run("implements execution - fail", func(t *testing.T) {})
t.Run("implements execution - fail", func(t *testing.T) {
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaFullAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: nil,
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &stakingtypes.MsgDelegate{
DelegatorAddress: aaFullAddrStr,
ValidatorAddress: "some-validator",
Amount: coins(t, "2000stake")[0],
}),
ExecutionGasLimit: 36000,
})
// in order to assert the call we expect an error to be returned.
require.Contains(t, resp.Error, accounts.ErrExecution.Error()) // error is in execution
require.Contains(t, resp.Error, "this account does not allow delegation messages")
})
t.Run("implements bundler payment and execution - success", func(t *testing.T) {})
t.Run("implements bundler payment and execution - success", func(t *testing.T) {
// we simulate the abstracted account pays the bundler using an NFT.
require.NoError(t, app.NFTKeeper.SaveClass(ctx, nft.Class{
Id: "omega-rare",
}))
require.NoError(t, app.NFTKeeper.Mint(ctx, nft.NFT{
ClassId: "omega-rare",
Id: "the-most-rare",
}, aaFullAddr))
t.Run("Simulate - OK", func(t *testing.T) {})
resp := ak.ExecuteUserOperation(ctx, bundlerAddrStr, &accountsv1.UserOperation{
Sender: aaFullAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
AuthenticationGasLimit: 10000,
BundlerPaymentMessages: intoAny(t, &nft.MsgSend{
ClassId: "omega-rare",
Id: "the-most-rare",
Sender: aaFullAddrStr,
Receiver: bundlerAddrStr,
}),
BundlerPaymentGasLimit: 50000,
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaFullAddrStr,
ToAddress: aliceAddrStr,
Amount: coins(t, "2000stake"),
}),
ExecutionGasLimit: 38000,
})
require.Empty(t, resp.Error) // no error
})
t.Run("Simulate - OK", func(t *testing.T) {
queryServer := accounts.NewQueryServer(ak)
// gas in unspecified
op := &accountsv1.UserOperation{
Sender: aaAddrStr,
AuthenticationMethod: "secp256k1",
AuthenticationData: mockSignature,
BundlerPaymentMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: bundlerAddrStr,
Amount: coins(t, "1stake"),
}),
ExecutionMessages: intoAny(t, &banktypes.MsgSend{
FromAddress: aaAddrStr,
ToAddress: aliceAddrStr,
Amount: coins(t, "2000stake"),
}),
}
queryResp, err := queryServer.SimulateUserOperation(ctx, &accountsv1.SimulateUserOperationRequest{
Bundler: bundlerAddrStr,
UserOperation: op,
})
require.NoError(t, err)
resp := queryResp.UserOperationResponse
require.Empty(t, resp.Error) // no error
require.Len(t, resp.BundlerPaymentResponses, 1)
require.Len(t, resp.ExecutionResponses, 1)
// assess gas is filled
require.NotZero(t, resp.ExecutionGasUsed)
require.NotZero(t, resp.BundlerPaymentGasUsed)
require.NotZero(t, resp.AuthenticationGasUsed)
})
t.Run("Simulate - Fail empty user operation", func(t *testing.T) {
queryServer := accounts.NewQueryServer(ak)
_, err := queryServer.SimulateUserOperation(ctx, &accountsv1.SimulateUserOperationRequest{})
require.Error(t, err)
})
t.Run("Simulate - Fail empty user operation", func(t *testing.T) {})
}
*/
func intoAny(t *testing.T, msgs ...gogoproto.Message) (anys []*codectypes.Any) {
t.Helper()
+84
View File
@@ -0,0 +1,84 @@
//go:build app_v1
package accounts
import (
"math/rand"
"testing"
"cosmossdk.io/simapp"
baseaccountv1 "cosmossdk.io/x/accounts/defaults/base/v1"
"cosmossdk.io/x/bank/testutil"
banktypes "cosmossdk.io/x/bank/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestBaseAccount(t *testing.T) {
app := setupApp(t)
ak := app.AccountsKeeper
ctx := sdk.NewContext(app.CommitMultiStore(), false, app.Logger())
_, baseAccountAddr, err := ak.Init(ctx, "base", accCreator, &baseaccountv1.MsgInit{
PubKey: privKey.PubKey().Bytes(),
}, nil)
require.NoError(t, err)
// fund base account! this will also cause an auth base account to be created
// by the bank module.
// TODO: fixed by letting x/auth rely on x/accounts for acc existence checks.
fundAccount(t, app, ctx, baseAccountAddr, "1000000stake")
// now we make the account send a tx, public key not present.
// so we know it will default to x/accounts calling.
msg := &banktypes.MsgSend{
FromAddress: bechify(t, app, baseAccountAddr),
ToAddress: bechify(t, app, []byte("random-addr")),
Amount: coins(t, "100stake"),
}
sendTx(t, ctx, app, baseAccountAddr, msg)
}
func sendTx(t *testing.T, ctx sdk.Context, app *simapp.SimApp, sender []byte, msg sdk.Msg) {
tx := sign(t, ctx, app, sender, privKey, msg)
res, _, err := app.SimDeliver(app.TxEncode, tx)
require.NoError(t, err)
t.Log(res)
}
func sign(t *testing.T, ctx sdk.Context, app *simapp.SimApp, from sdk.AccAddress, privKey cryptotypes.PrivKey, msg sdk.Msg) sdk.Tx {
r := rand.New(rand.NewSource(0))
accNum, err := app.AccountsKeeper.AccountByNumber.Get(ctx, from)
require.NoError(t, err)
accSeq, err := app.AccountsKeeper.Query(ctx, from, &baseaccountv1.QuerySequence{})
require.NoError(t, err)
tx, err := sims.GenSignedMockTx(
r,
app.TxConfig(),
[]sdk.Msg{msg},
coins(t, "100stake"),
1_000_000,
app.ChainID(),
[]uint64{accNum},
[]uint64{accSeq.(*baseaccountv1.QuerySequenceResponse).Sequence},
privKey,
)
require.NoError(t, err)
return tx
}
func bechify(t *testing.T, app *simapp.SimApp, addr []byte) string {
bech32, err := app.AuthKeeper.AddressCodec().BytesToString(addr)
require.NoError(t, err)
return bech32
}
func fundAccount(t *testing.T, app *simapp.SimApp, ctx sdk.Context, addr sdk.AccAddress, amt string) {
require.NoError(t, testutil.FundAccount(ctx, app.BankKeeper, addr, coins(t, amt)))
}