diff --git a/go.mod b/go.mod index 4eac684..94bb462 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( cosmossdk.io/api v0.3.1 + cosmossdk.io/errors v1.0.0-beta.7 github.com/cometbft/cometbft v0.37.1 github.com/cometbft/cometbft-db v0.7.0 github.com/cosmos/cosmos-sdk v0.47.3 @@ -31,7 +32,6 @@ require ( cloud.google.com/go/storage v1.29.0 // indirect cosmossdk.io/core v0.5.1 // indirect cosmossdk.io/depinject v1.0.0-alpha.3 // indirect - cosmossdk.io/errors v1.0.0-beta.7 // indirect cosmossdk.io/log v1.1.0 // indirect cosmossdk.io/math v1.0.1 // indirect cosmossdk.io/tools/rosetta v0.2.1 // indirect diff --git a/tests/e2e/asset/cli_test.go b/tests/e2e/asset/cli_test.go new file mode 100644 index 0000000..950d764 --- /dev/null +++ b/tests/e2e/asset/cli_test.go @@ -0,0 +1,14 @@ +package asset + +import ( + "planetmint-go/testutil/network" + "testing" + + "github.com/stretchr/testify/suite" +) + +func TestE2ETestSuite(t *testing.T) { + cfg := network.DefaultConfig() + cfg.NumValidators = 1 + suite.Run(t, NewE2ETestSuite(cfg)) +} diff --git a/tests/e2e/asset/rest.go b/tests/e2e/asset/rest.go new file mode 100644 index 0000000..b1f6231 --- /dev/null +++ b/tests/e2e/asset/rest.go @@ -0,0 +1,87 @@ +package asset + +import ( + "encoding/hex" + "fmt" + "planetmint-go/testutil" + "planetmint-go/testutil/sample" + + assettypes "planetmint-go/x/asset/types" + + txtypes "github.com/cosmos/cosmos-sdk/types/tx" +) + +// TestNotarizeAssetREST notarizes asset over REST endpoint +func (s *E2ETestSuite) TestNotarizeAssetREST() { + val := s.network.Validators[0] + + // Create Msg + k, err := val.ClientCtx.Keyring.Key("machine") + s.Require().NoError(err) + + addr, err := k.GetAddress() + s.Require().NoError(err) + + privKey, err := val.ClientCtx.Keyring.(unsafeExporter).ExportPrivateKeyObject("machine") + s.Require().NoError(err) + + sk := hex.EncodeToString(privKey.Bytes()) + cidHash, signature := sample.Asset(sk) + + testCases := []struct { + name string + msg assettypes.MsgNotarizeAsset + rawLog string + }{ + { + "machine not found", + assettypes.MsgNotarizeAsset{ + Creator: addr.String(), + Hash: cidHash, + Signature: signature, + PubKey: "human pubkey", + }, + "machine not found", + }, + { + "invalid signature", + assettypes.MsgNotarizeAsset{ + Creator: addr.String(), + Hash: cidHash, + Signature: "invalid signature", + PubKey: hex.EncodeToString(privKey.PubKey().Bytes()), + }, + "invalid signature", + }, + { + "valid notarization", + assettypes.MsgNotarizeAsset{ + Creator: addr.String(), + Hash: cidHash, + Signature: signature, + PubKey: hex.EncodeToString(privKey.PubKey().Bytes()), + }, + "planetmintgo.asset.MsgNotarizeAsset", + }, + } + + for _, tc := range testCases { + // Prepare Tx + txBytes, err := testutil.PrepareTx(val, &tc.msg, "machine") + s.Require().NoError(err) + + // Broadcast Tx + broadcastTxResponse, err := testutil.BroadcastTx(val, txBytes) + s.Require().NoError(err) + + s.network.WaitForNextBlock() + + tx, err := testutil.GetRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", val.APIAddress, broadcastTxResponse.TxResponse.TxHash)) + s.Require().NoError(err) + + var txRes txtypes.GetTxResponse + err = val.ClientCtx.Codec.UnmarshalJSON(tx, &txRes) + s.Require().NoError(err) + s.Require().Contains(txRes.TxResponse.RawLog, tc.rawLog) + } +} diff --git a/tests/e2e/asset/suite.go b/tests/e2e/asset/suite.go new file mode 100644 index 0000000..661fabe --- /dev/null +++ b/tests/e2e/asset/suite.go @@ -0,0 +1,196 @@ +package asset + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "planetmint-go/testutil/network" + "planetmint-go/testutil/sample" + "regexp" + + clitestutil "planetmint-go/testutil/cli" + assetcli "planetmint-go/x/asset/client/cli" + machinecli "planetmint-go/x/machine/client/cli" + + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + + machinetypes "planetmint-go/x/machine/types" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/client/cli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "sigs.k8s.io/yaml" +) + +// Queryable pubkey for TestNotarizeAsset +const mnemonic = "helmet hedgehog lab actor weekend elbow pelican valid obtain hungry rocket decade tower gallery fit practice cart cherry giggle hair snack glance bulb farm" + +// E2ETestSuite struct definition of asset suite +type E2ETestSuite struct { + suite.Suite + + cfg network.Config + network *network.Network +} + +// NewE2ETestSuite returns configured asset E2ETestSuite +func NewE2ETestSuite(cfg network.Config) *E2ETestSuite { + return &E2ETestSuite{cfg: cfg} +} + +// SetupSuite initializes asset E2ETestSuite +func (s *E2ETestSuite) SetupSuite() { + s.T().Log("setting up e2e test suite") + + s.network = network.New(s.T()) + val := s.network.Validators[0] + + kb := val.ClientCtx.Keyring + account, err := kb.NewAccount("machine", mnemonic, keyring.DefaultBIP39Passphrase, sdk.FullFundraiserPath, hd.Secp256k1) + s.Require().NoError(err) + pk, err := account.GetPubKey() + pkHex := hex.EncodeToString(pk.Bytes()) + s.Require().NoError(err) + + addr, _ := account.GetAddress() + + // sending funds to machine to initialize account on chain + args := []string{ + "node0", + addr.String(), + "1000stake", + "--yes", + fmt.Sprintf("--%s=%s", flags.FlagFees, "2stake"), + } + _, err = clitestutil.ExecTestCLICmd(val.ClientCtx, bank.NewSendTxCmd(), args) + s.Require().NoError(err) + + s.Require().NoError(s.network.WaitForNextBlock()) + + machine := machinetypes.Machine{ + Name: "machine", + Ticker: "machine_ticker", + Issued: 1, + Amount: 1000, + Precision: 8, + IssuerPlanetmint: pkHex, + IssuerLiquid: pkHex, + MachineId: pkHex, + Metadata: &machinetypes.Metadata{ + AdditionalDataCID: "CID", + Gps: "{\"Latitude\":\"-48.876667\",\"Longitude\":\"-123.393333\"}", + }, + } + machineJSON, err := json.Marshal(&machine) + s.Require().NoError(err) + + args = []string{ + fmt.Sprintf("--%s=%s", flags.FlagFrom, "machine"), + fmt.Sprintf("--%s=%s", flags.FlagFees, "2stake"), + "--yes", + string(machineJSON), + } + + _, err = clitestutil.ExecTestCLICmd(val.ClientCtx, machinecli.CmdAttestMachine(), args) + s.Require().NoError(err) + s.Require().NoError(s.network.WaitForNextBlock()) +} + +// TearDownSuite clean up after testing +func (s *E2ETestSuite) TearDownSuite() { + s.T().Log("tearing down e2e test suite") +} + +// Needed to export private key from Keyring +type unsafeExporter interface { + ExportPrivateKeyObject(uid string) (types.PrivKey, error) +} + +// TestNotarizeAsset notarizes asset over cli +func (s *E2ETestSuite) TestNotarizeAsset() { + val := s.network.Validators[0] + + privKey, err := val.ClientCtx.Keyring.(unsafeExporter).ExportPrivateKeyObject("machine") + s.Require().NoError(err) + + sk := hex.EncodeToString(privKey.Bytes()) + + cidHash, signature := sample.Asset(sk) + + testCases := []struct { + name string + args []string + rawLog string + }{ + { + "machine not found", + []string{ + cidHash, + signature, + "pubkey", + fmt.Sprintf("--%s=%s", flags.FlagFrom, "machine"), + fmt.Sprintf("--%s=%s", flags.FlagFees, "2stake"), + "--yes", + }, + "machine not found", + }, + { + "invalid signature", + []string{ + "cid", + "signature", + hex.EncodeToString(privKey.PubKey().Bytes()), + fmt.Sprintf("--%s=%s", flags.FlagFrom, "machine"), + fmt.Sprintf("--%s=%s", flags.FlagFees, "2stake"), + "--yes", + }, + "invalid signature", + }, + { + "valid notarization", + []string{ + cidHash, + signature, + hex.EncodeToString(privKey.PubKey().Bytes()), + fmt.Sprintf("--%s=%s", flags.FlagFrom, "machine"), + fmt.Sprintf("--%s=%s", flags.FlagFees, "2stake"), + "--yes", + }, + "planetmintgo.asset.MsgNotarizeAsset", + }, + } + + for _, tc := range testCases { + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, assetcli.CmdNotarizeAsset(), tc.args) + s.Require().NoError(err) + // Hack: numbers come back as strings and cannot be unmarshalled into TxResponse struct + m := regexp.MustCompile(`"([0-9]+?)"`) + str := m.ReplaceAllString(out.String(), "${1}") + + var txResponse sdk.TxResponse + err = json.Unmarshal([]byte(str), &txResponse) + s.Require().NoError(err) + + s.Require().NoError(s.network.WaitForNextBlock()) + args := []string{ + txResponse.TxHash, + } + out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, authcmd.QueryTxCmd(), args) + s.Require().NoError(err) + + str = m.ReplaceAllString(out.String(), "${1}") + // Need to convert to JSON first, because TxResponse struct lacks `yaml:"height,omitempty"`, etc. + j, err := yaml.YAMLToJSON([]byte(str)) + s.Require().NoError(err) + + err = json.Unmarshal(j, &txResponse) + s.Require().NoError(err) + + assert.Contains(s.T(), txResponse.RawLog, tc.rawLog) + } +} diff --git a/tests/e2e/machine/rest.go b/tests/e2e/machine/rest.go new file mode 100644 index 0000000..c52b6d8 --- /dev/null +++ b/tests/e2e/machine/rest.go @@ -0,0 +1,66 @@ +package machine + +import ( + "fmt" + "planetmint-go/testutil" + machinetypes "planetmint-go/x/machine/types" + + txtypes "github.com/cosmos/cosmos-sdk/types/tx" +) + +func (s *E2ETestSuite) TestAttestMachineREST() { + val := s.network.Validators[0] + baseURL := val.APIAddress + + // Query Sequence Number + k, err := val.ClientCtx.Keyring.Key("machine") + s.Require().NoError(err) + + addr, err := k.GetAddress() + s.Require().NoError(err) + + // Create Attest Machine TX + machine := machinetypes.Machine{ + Name: "machine", + Ticker: "machine_ticker", + Issued: 1, + Amount: 1000, + Precision: 8, + IssuerPlanetmint: pubKey, + IssuerLiquid: pubKey, + MachineId: pubKey, + Metadata: &machinetypes.Metadata{ + AdditionalDataCID: "CID", + Gps: "{\"Latitude\":\"-48.876667\",\"Longitude\":\"-123.393333\"}", + }, + } + + msg := machinetypes.MsgAttestMachine{ + Creator: addr.String(), + Machine: &machine, + } + + txBytes, err := testutil.PrepareTx(val, &msg, "machine") + s.Require().NoError(err) + + broadcastTxResponse, err := testutil.BroadcastTx(val, txBytes) + s.Require().NoError(err) + + s.network.WaitForNextBlock() + tx, err := testutil.GetRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", val.APIAddress, broadcastTxResponse.TxResponse.TxHash)) + s.Require().NoError(err) + + var txRes txtypes.GetTxResponse + err = val.ClientCtx.Codec.UnmarshalJSON(tx, &txRes) + s.Require().NoError(err) + s.Require().Equal(uint32(0), txRes.TxResponse.Code) + + queryMachineUrl := fmt.Sprintf("%s/planetmint-go/machine/get_machine_by_public_key/%s", baseURL, pubKey) + queryMachineRes, err := testutil.GetRequest(queryMachineUrl) + s.Require().NoError(err) + + var qmRes machinetypes.QueryGetMachineByPublicKeyResponse + err = val.ClientCtx.Codec.UnmarshalJSON(queryMachineRes, &qmRes) + s.Require().NoError(err) + s.Require().Equal(&machine, qmRes.Machine) +} diff --git a/tests/e2e/machine/suite.go b/tests/e2e/machine/suite.go index c8cfd05..8863ffa 100644 --- a/tests/e2e/machine/suite.go +++ b/tests/e2e/machine/suite.go @@ -19,7 +19,8 @@ import ( ) // Queryable pubkey for TestAttestMachine -const pubKey = "A/ZrbETECRq5DNGJZ0aH0DjlV4Y1opMlRfGoEJH454eB" +const pubKey = "AjKN6HiWucu1EBwzX0ACnkvomJiLRwq79oPxoLMY1zRw" +const mnemonic = "helmet hedgehog lab actor weekend elbow pelican valid obtain hungry rocket decade tower gallery fit practice cart cherry giggle hair snack glance bulb farm" // Struct definition of machine E2ETestSuite type E2ETestSuite struct { @@ -42,7 +43,7 @@ func (s *E2ETestSuite) SetupSuite() { val := s.network.Validators[0] kb := val.ClientCtx.Keyring - account, _, err := kb.NewMnemonic("machine", keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + account, err := kb.NewAccount("machine", mnemonic, keyring.DefaultBIP39Passphrase, sdk.FullFundraiserPath, hd.Secp256k1) s.Require().NoError(err) addr, _ := account.GetAddress() diff --git a/testutil/rest.go b/testutil/rest.go new file mode 100644 index 0000000..31bdd65 --- /dev/null +++ b/testutil/rest.go @@ -0,0 +1,164 @@ +package testutil + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/testutil/network" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" +) + +// GetRequest defines a wrapper around an HTTP GET request with a provided URL. +// An error is returned if the request or reading the body fails. +func GetRequest(url string) ([]byte, error) { + res, err := http.Get(url) //nolint:gosec // only used for testing + if err != nil { + return nil, err + } + defer func() { + _ = res.Body.Close() + }() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + return body, nil +} + +// PostRequest defines a wrapper around an HTTP POST request with a provided URL and data. +// An error is returned if the request or reading the body fails. +func PostRequest(url, contentType string, data []byte) ([]byte, error) { + res, err := http.Post(url, contentType, bytes.NewBuffer(data)) //nolint:gosec // only used for testing + if err != nil { + return nil, fmt.Errorf("error while sending post request: %w", err) + } + defer func() { + _ = res.Body.Close() + }() + + bz, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + return bz, nil +} + +func PrepareTx(val *network.Validator, msg sdk.Msg, signer string) ([]byte, error) { + k, err := val.ClientCtx.Keyring.Key(signer) + if err != nil { + return nil, err + } + + addr, err := k.GetAddress() + if err != nil { + return nil, err + } + + reqAccountInfo := fmt.Sprintf("%s/cosmos/auth/v1beta1/account_info/%s", val.APIAddress, addr.String()) + respAccountInfo, err := GetRequest(reqAccountInfo) + if err != nil { + return nil, err + } + + var resAccountInfo authtypes.QueryAccountInfoResponse + err = val.ClientCtx.Codec.UnmarshalJSON(respAccountInfo, &resAccountInfo) + if err != nil { + return nil, err + } + + txBuilder := val.ClientCtx.TxConfig.NewTxBuilder() + txBuilder.SetMsgs(msg) + txBuilder.SetGasLimit(200000) + txBuilder.SetFeeAmount(sdk.Coins{sdk.NewInt64Coin("stake", 2)}) + txBuilder.SetTimeoutHeight(0) + + pk, err := k.GetPubKey() + if err != nil { + return nil, err + } + + sk := k.GetLocal().PrivKey + + var priv cryptotypes.PrivKey + err = val.ClientCtx.Codec.UnpackAny(sk, &priv) + if err != nil { + return nil, err + } + + sigV2 := signing.SignatureV2{ + PubKey: pk, + Data: &signing.SingleSignatureData{ + SignMode: val.ClientCtx.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: resAccountInfo.Info.Sequence, + } + + err = txBuilder.SetSignatures(sigV2) + if err != nil { + return nil, err + } + + signerData := xauthsigning.SignerData{ + ChainID: val.ClientCtx.ChainID, + AccountNumber: resAccountInfo.Info.AccountNumber, + Sequence: resAccountInfo.Info.Sequence, + } + sigV2, err = tx.SignWithPrivKey( + val.ClientCtx.TxConfig.SignModeHandler().DefaultMode(), signerData, + txBuilder, priv, val.ClientCtx.TxConfig, resAccountInfo.Info.Sequence, + ) + if err != nil { + return nil, err + } + + err = txBuilder.SetSignatures(sigV2) + if err != nil { + return nil, err + } + + txBytes, err := val.ClientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return nil, err + } + + return txBytes, nil +} + +func BroadcastTx(val *network.Validator, txBytes []byte) (*txtypes.BroadcastTxResponse, error) { + broadcastTxUrl := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs", val.APIAddress) + req := txtypes.BroadcastTxRequest{ + TxBytes: txBytes, + Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC, + } + + broadCastTxBody, err := val.ClientCtx.Codec.MarshalJSON(&req) + if err != nil { + return nil, err + } + broadCastTxResponse, err := PostRequest(broadcastTxUrl, "application/json", broadCastTxBody) + if err != nil { + return nil, err + } + + var bctRes txtypes.BroadcastTxResponse + err = val.ClientCtx.Codec.UnmarshalJSON(broadCastTxResponse, &bctRes) + if err != nil { + return nil, err + } + + return &bctRes, nil +} diff --git a/testutil/sample/sample.go b/testutil/sample/sample.go index 0cfe279..0c09db7 100644 --- a/testutil/sample/sample.go +++ b/testutil/sample/sample.go @@ -1,6 +1,7 @@ package sample import ( + "crypto/sha256" "encoding/hex" machinetypes "planetmint-go/x/machine/types" @@ -55,3 +56,19 @@ func Metadata() machinetypes.Metadata { AdditionalDataCID: "CID", } } + +func Asset(sk string) (string, string) { + cid := "cid" + + skBytes, _ := hex.DecodeString(sk) + privKey := &secp256k1.PrivKey{Key: skBytes} + + cidBytes, _ := hex.DecodeString(cid) + hash := sha256.Sum256(cidBytes) + + sign, _ := privKey.Sign(hash[:]) + + signatureHex := hex.EncodeToString(sign) + + return cid, signatureHex +} diff --git a/x/asset/keeper/msg_server_notarize_asset.go b/x/asset/keeper/msg_server_notarize_asset.go index 6e96f71..d3d5711 100644 --- a/x/asset/keeper/msg_server_notarize_asset.go +++ b/x/asset/keeper/msg_server_notarize_asset.go @@ -18,12 +18,12 @@ func (k msgServer) NotarizeAsset(goCtx context.Context, msg *types.MsgNotarizeAs _, found := k.machineKeeper.GetMachineIndex(ctx, msg.PubKey) if !found { - return &types.MsgNotarizeAssetResponse{}, errors.New("machine not found") + return nil, errors.New("machine not found") } valid := ValidateSignature(msg.Hash, msg.Signature, msg.PubKey) if !valid { - return &types.MsgNotarizeAssetResponse{}, errors.New("invalid signature") + return nil, errors.New("invalid signature") } var asset = types.Asset{ diff --git a/x/asset/keeper/msg_server_test.go b/x/asset/keeper/msg_server_test.go index 07493c6..8f72e79 100644 --- a/x/asset/keeper/msg_server_test.go +++ b/x/asset/keeper/msg_server_test.go @@ -2,8 +2,6 @@ package keeper_test import ( "context" - "crypto/sha256" - "encoding/hex" "testing" keepertest "planetmint-go/testutil/keeper" @@ -11,7 +9,6 @@ import ( "planetmint-go/x/asset/keeper" "planetmint-go/x/asset/types" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,23 +27,7 @@ func TestMsgServer(t *testing.T) { func TestMsgServerNotarizeAsset(t *testing.T) { sk, pk := sample.KeyPair() - cid := "cid" - - skBytes, err := hex.DecodeString(sk) - if err != nil { - assert.Equal(t, true, false) - } - privKey := &secp256k1.PrivKey{Key: skBytes} - - cidBytes, _ := hex.DecodeString(cid) - hash := sha256.Sum256(cidBytes) - - sign, err := privKey.Sign(hash[:]) - if err != nil { - assert.Equal(t, true, false) - } - - signatureHex := hex.EncodeToString(sign) + cid, signatureHex := sample.Asset(sk) msg := types.NewMsgNotarizeAsset(pk, cid, signatureHex, pk) msgServer, ctx := setupMsgServer(t)