diff --git a/testutil/cli/cmt_mocks.go b/testutil/cli/cmt_mocks.go index 71e0cb9739..df1482fe75 100644 --- a/testutil/cli/cmt_mocks.go +++ b/testutil/cli/cmt_mocks.go @@ -27,6 +27,14 @@ func NewMockCometRPC(respQuery abci.QueryResponse) MockCometRPC { return MockCometRPC{responseQuery: respQuery} } +// NewMockCometRPCWithValue returns a mock CometBFT RPC implementation with value only. +// It is used for CLI testing. +func NewMockCometRPCWithValue(bz []byte) MockCometRPC { + return MockCometRPC{responseQuery: abci.QueryResponse{ + Value: bz, + }} +} + func (MockCometRPC) BroadcastTxSync(context.Context, cmttypes.Tx) (*coretypes.ResultBroadcastTx, error) { return &coretypes.ResultBroadcastTx{Code: 0}, nil } diff --git a/x/accounts/cli/cli_test.go b/x/accounts/cli/cli_test.go new file mode 100644 index 0000000000..45b753a7f6 --- /dev/null +++ b/x/accounts/cli/cli_test.go @@ -0,0 +1,145 @@ +package cli_test + +import ( + "context" + "fmt" + "io" + "testing" + + "github.com/gogo/protobuf/types" + "github.com/stretchr/testify/suite" + + "cosmossdk.io/math" + "cosmossdk.io/x/accounts/cli" + v1 "cosmossdk.io/x/accounts/v1" + "cosmossdk.io/x/bank" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + addresscodec "github.com/cosmos/cosmos-sdk/codec/address" + codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" + "github.com/cosmos/cosmos-sdk/testutil" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + testutilmod "github.com/cosmos/cosmos-sdk/types/module/testutil" +) + +type CLITestSuite struct { + suite.Suite + + kr keyring.Keyring + encCfg testutilmod.TestEncodingConfig + baseCtx client.Context + clientCtx client.Context +} + +func TestCLITestSuite(t *testing.T) { + suite.Run(t, new(CLITestSuite)) +} + +func (s *CLITestSuite) SetupSuite() { + s.encCfg = testutilmod.MakeTestEncodingConfig(codectestutil.CodecOptions{}, bank.AppModule{}) + s.kr = keyring.NewInMemory(s.encCfg.Codec) + + s.baseCtx = client.Context{}. + WithKeyring(s.kr). + WithTxConfig(s.encCfg.TxConfig). + WithCodec(s.encCfg.Codec). + WithAccountRetriever(client.MockAccountRetriever{}). + WithOutput(io.Discard). + WithAddressCodec(addresscodec.NewBech32Codec("cosmos")). + WithValidatorAddressCodec(addresscodec.NewBech32Codec("cosmosvaloper")). + WithConsensusAddressCodec(addresscodec.NewBech32Codec("cosmosvalcons")) +} + +func (s *CLITestSuite) TestTxInitCmd() { + accounts := testutil.CreateKeyringAccounts(s.T(), s.kr, 1) + accountStr := make([]string, len(accounts)) + for i, acc := range accounts { + addrStr, err := s.baseCtx.AddressCodec.BytesToString(acc.Address) + s.Require().NoError(err) + accountStr[i] = addrStr + } + + s.baseCtx = s.baseCtx.WithFromAddress(accounts[0].Address) + + extraArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin("photon", math.NewInt(10))).String()), + fmt.Sprintf("--%s=test-chain", flags.FlagChainID), + fmt.Sprintf("--%s=%s", flags.FlagFrom, accountStr[0]), + } + + cmd := cli.GetTxInitCmd() + cmd.SetOutput(io.Discard) + + ctxGen := func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&v1.SchemaResponse{ + InitSchema: &v1.SchemaResponse_Handler{ + Request: sdk.MsgTypeURL(&types.Empty{})[1:], + Response: sdk.MsgTypeURL(&types.Empty{})[1:], + }, + }) + c := clitestutil.NewMockCometRPCWithValue(bz) + return s.baseCtx.WithClient(c) + } + s.clientCtx = ctxGen() + + testCases := []struct { + name string + accountType string + jsonMsg string + extraArgs []string + expectErrMsg string + }{ + { + name: "valid json msg", + accountType: "test", + jsonMsg: `{}`, + extraArgs: extraArgs, + expectErrMsg: "", + }, + { + name: "invalid json msg", + accountType: "test", + jsonMsg: `{"test": "jsonmsg"}`, + extraArgs: extraArgs, + expectErrMsg: "provided message is not valid", + }, + { + name: "invalid sender", + accountType: "test", + jsonMsg: `{}`, + extraArgs: append(extraArgs, fmt.Sprintf("--%s=%s", flags.FlagFrom, "bar")), + expectErrMsg: "failed to convert address field to address", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + ctx := svrcmd.CreateExecuteContext(context.Background()) + + var args []string + args = append(args, tc.accountType) + args = append(args, tc.jsonMsg) + args = append(args, tc.extraArgs...) + + cmd.SetContext(ctx) + cmd.SetArgs(args) + + out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } + }) + } +} diff --git a/x/accounts/defaults/base/account_test.go b/x/accounts/defaults/base/account_test.go new file mode 100644 index 0000000000..a895d0f68e --- /dev/null +++ b/x/accounts/defaults/base/account_test.go @@ -0,0 +1,253 @@ +package base + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/core/store" + "cosmossdk.io/x/accounts/accountstd" + v1 "cosmossdk.io/x/accounts/defaults/base/v1" + aa_interface_v1 "cosmossdk.io/x/accounts/interfaces/account_abstraction/v1" + "cosmossdk.io/x/tx/signing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/cosmos/cosmos-sdk/types/tx" +) + +func setupBaseAccount(t *testing.T, ss store.KVStoreService) Account { + t.Helper() + deps := makeMockDependencies(ss) + handler := directHandler{} + + createAccFn := NewAccount("base", signing.NewHandlerMap(handler)) + _, acc, err := createAccFn(deps) + baseAcc := acc.(Account) + require.NoError(t, err) + + return baseAcc +} + +func TestInit(t *testing.T) { + ctx, ss := newMockContext(t) + baseAcc := setupBaseAccount(t, ss) + _, err := baseAcc.Init(ctx, &v1.MsgInit{ + PubKey: secp256k1.GenPrivKey().PubKey().Bytes(), + }) + require.NoError(t, err) + + testcases := []struct { + name string + msg *v1.MsgInit + isExpErr bool + }{ + { + "valid init", + &v1.MsgInit{ + PubKey: secp256k1.GenPrivKey().PubKey().Bytes(), + }, + false, + }, + { + "invalid pubkey", + &v1.MsgInit{ + PubKey: []byte("invalid_pk"), + }, + true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + _, err := baseAcc.Init(ctx, tc.msg) + if tc.isExpErr { + require.NotNil(t, err, tc.name) + return + } + require.NoError(t, err) + }) + } +} + +func TestSwapKey(t *testing.T) { + ctx, ss := newMockContext(t) + baseAcc := setupBaseAccount(t, ss) + _, err := baseAcc.Init(ctx, &v1.MsgInit{ + PubKey: secp256k1.GenPrivKey().PubKey().Bytes(), + }) + require.NoError(t, err) + + testcases := []struct { + name string + genCtx func(ctx context.Context) context.Context + msg *v1.MsgSwapPubKey + isExpErr bool + expErr error + }{ + { + "valid transaction", + func(ctx context.Context) context.Context { + return accountstd.SetSender(ctx, []byte("mock_base_account")) + }, + &v1.MsgSwapPubKey{ + NewPubKey: secp256k1.GenPrivKey().PubKey().Bytes(), + }, + false, + nil, + }, + { + "invalid transaction, sender is not self", + func(ctx context.Context) context.Context { + return accountstd.SetSender(ctx, []byte("sender")) + }, + &v1.MsgSwapPubKey{ + NewPubKey: secp256k1.GenPrivKey().PubKey().Bytes(), + }, + true, + errors.New("unauthorized"), + }, + { + "invalid transaction, invalid pubkey", + func(ctx context.Context) context.Context { + return accountstd.SetSender(ctx, []byte("mock_base_account")) + }, + &v1.MsgSwapPubKey{ + NewPubKey: []byte("invalid_pk"), + }, + true, + nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + if tc.genCtx != nil { + ctx = tc.genCtx(ctx) + } + _, err := baseAcc.SwapPubKey(ctx, tc.msg) + if tc.isExpErr { + if tc.expErr != nil { + require.Equal(t, tc.expErr, err, tc.name) + } else { + require.NotNil(t, err, tc.name) + } + return + } + require.NoError(t, err) + }) + } +} + +func TestAuthenticate(t *testing.T) { + ctx, ss := newMockContext(t) + baseAcc := setupBaseAccount(t, ss) + privKey := secp256k1.GenPrivKey() + pkAny, err := codectypes.NewAnyWithValue(privKey.PubKey()) + require.NoError(t, err) + _, err = baseAcc.Init(ctx, &v1.MsgInit{ + PubKey: privKey.PubKey().Bytes(), + }) + require.NoError(t, err) + + ctx = accountstd.SetSender(ctx, address.Module("accounts")) + require.NoError(t, err) + + transaction := tx.Tx{ + Body: &tx.TxBody{}, + AuthInfo: &tx.AuthInfo{ + SignerInfos: []*tx.SignerInfo{ + { + PublicKey: pkAny, + ModeInfo: &tx.ModeInfo{ + Sum: &tx.ModeInfo_Single_{ + Single: &tx.ModeInfo_Single{ + Mode: 1, + }, + }, + }, + Sequence: 0, + }, + }, + }, + Signatures: [][]byte{}, + } + + bodyByte, err := transaction.Body.Marshal() + require.NoError(t, err) + authByte, err := transaction.AuthInfo.Marshal() + require.NoError(t, err) + + txDoc := tx.SignDoc{ + BodyBytes: bodyByte, + AuthInfoBytes: authByte, + ChainId: "test", + AccountNumber: 1, + } + signBytes, err := txDoc.Marshal() + require.NoError(t, err) + + sig, err := privKey.Sign(signBytes) + require.NoError(t, err) + + transaction.Signatures = append(transaction.Signatures, sig) + + rawTx := tx.TxRaw{ + BodyBytes: bodyByte, + AuthInfoBytes: authByte, + Signatures: transaction.Signatures, + } + + _, err = baseAcc.Authenticate(ctx, &aa_interface_v1.MsgAuthenticate{ + RawTx: &rawTx, + Tx: &transaction, + SignerIndex: 0, + }) + require.NoError(t, err) + + // testing with invalid signature + + // update sequence number + transaction = tx.Tx{ + Body: &tx.TxBody{}, + AuthInfo: &tx.AuthInfo{ + SignerInfos: []*tx.SignerInfo{ + { + PublicKey: pkAny, + ModeInfo: &tx.ModeInfo{ + Sum: &tx.ModeInfo_Single_{ + Single: &tx.ModeInfo_Single{ + Mode: 1, + }, + }, + }, + Sequence: 1, + }, + }, + }, + Signatures: [][]byte{}, + } + authByte, err = transaction.AuthInfo.Marshal() + require.NoError(t, err) + + txDoc.BodyBytes = []byte("invalid_msg") + txDoc.AuthInfoBytes = authByte + signBytes, err = txDoc.Marshal() + require.NoError(t, err) + invalidSig, err := privKey.Sign(signBytes) + require.NoError(t, err) + + transaction.Signatures = append([][]byte{}, invalidSig) + + rawTx.Signatures = transaction.Signatures + + _, err = baseAcc.Authenticate(ctx, &aa_interface_v1.MsgAuthenticate{ + RawTx: &rawTx, + Tx: &transaction, + SignerIndex: 0, + }) + require.Equal(t, errors.New("signature verification failed"), err) +} diff --git a/x/accounts/defaults/base/utils_test.go b/x/accounts/defaults/base/utils_test.go new file mode 100644 index 0000000000..90c2c9deb5 --- /dev/null +++ b/x/accounts/defaults/base/utils_test.go @@ -0,0 +1,122 @@ +package base + +import ( + "context" + "testing" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/runtime/protoiface" + + signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" + "cosmossdk.io/collections" + "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/event" + "cosmossdk.io/core/header" + "cosmossdk.io/core/store" + "cosmossdk.io/x/accounts/accountstd" + accountsv1 "cosmossdk.io/x/accounts/v1" + "cosmossdk.io/x/tx/signing" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/runtime" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" +) + +type ProtoMsg = protoiface.MessageV1 + +// mock statecodec +type mockStateCodec struct { + codec.Codec +} + +var _ codec.Codec = mockStateCodec{} + +func (c mockStateCodec) Marshal(m gogoproto.Message) ([]byte, error) { + // Size() check can catch the typed nil value. + if m == nil || gogoproto.Size(m) == 0 { + // return empty bytes instead of nil, because nil has special meaning in places like store.Set + return []byte{}, nil + } + + return gogoproto.Marshal(m) +} + +func (c mockStateCodec) Unmarshal(bz []byte, ptr gogoproto.Message) error { + err := gogoproto.Unmarshal(bz, ptr) + + return err +} + +// mock address codec +type addressCodec struct{} + +func (a addressCodec) StringToBytes(text string) ([]byte, error) { return []byte(text), nil } +func (a addressCodec) BytesToString(bz []byte) (string, error) { return string(bz), nil } + +func newMockContext(t *testing.T) (context.Context, store.KVStoreService) { + t.Helper() + return accountstd.NewMockContext( + 0, []byte("mock_base_account"), []byte("sender"), nil, func(ctx context.Context, sender []byte, msg, msgResp ProtoMsg) error { + return nil + }, func(ctx context.Context, sender []byte, msg ProtoMsg) (ProtoMsg, error) { + return nil, nil + }, func(ctx context.Context, req, resp ProtoMsg) error { + _, ok := req.(*accountsv1.AccountNumberRequest) + require.True(t, ok) + gogoproto.Merge(resp.(gogoproto.Message), &accountsv1.AccountNumberResponse{ + Number: 1, + }) + return nil + }, + ) +} + +func makeMockDependencies(storeservice store.KVStoreService) accountstd.Dependencies { + sb := collections.NewSchemaBuilder(storeservice) + + return accountstd.Dependencies{ + SchemaBuilder: sb, + AddressCodec: addressCodec{}, + LegacyStateCodec: mockStateCodec{}, + Environment: appmodule.Environment{ + EventService: eventService{}, + HeaderService: headerService{}, + }, + } +} + +type headerService struct{} + +func (h headerService) HeaderInfo(context.Context) header.Info { + return header.Info{ + ChainID: "test", + } +} + +type eventService struct{} + +// EventManager implements event.Service. +func (eventService) EventManager(context.Context) event.Manager { + return runtime.EventService{Events: runtime.Events{EventManagerI: sdk.NewEventManager()}} +} + +var _ signing.SignModeHandler = directHandler{} + +type directHandler struct{} + +func (s directHandler) Mode() signingv1beta1.SignMode { + return signingv1beta1.SignMode_SIGN_MODE_DIRECT +} + +func (s directHandler) GetSignBytes(_ context.Context, signerData signing.SignerData, txData signing.TxData) ([]byte, error) { + txDoc := tx.SignDoc{ + BodyBytes: txData.BodyBytes, + AuthInfoBytes: txData.AuthInfoBytes, + ChainId: signerData.ChainID, + AccountNumber: signerData.AccountNumber, + } + + return txDoc.Marshal() +} diff --git a/x/accounts/go.mod b/x/accounts/go.mod index 18cd512606..0eda9957ad 100644 --- a/x/accounts/go.mod +++ b/x/accounts/go.mod @@ -19,6 +19,8 @@ require ( google.golang.org/protobuf v1.34.2 ) +require github.com/golang/mock v1.6.0 // indirect + require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect require ( @@ -76,7 +78,7 @@ require ( github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/googleapis v1.4.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect + github.com/gogo/protobuf v1.3.2 github.com/golang/glog v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/x/accounts/go.sum b/x/accounts/go.sum index 44c6c31a92..da53d76dd3 100644 --- a/x/accounts/go.sum +++ b/x/accounts/go.sum @@ -482,6 +482,7 @@ github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EU github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= @@ -517,6 +518,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -549,6 +551,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -611,6 +614,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=