diff --git a/.gitignore b/.gitignore index eb10faca7..8498547cc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # Build vendor +.vendor-new build tools/bin/* examples/build/* diff --git a/Gopkg.lock b/Gopkg.lock index ee5481301..6c36fb53e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,14 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + branch = "master" + digest = "1:7736fc6da04620727f8f3aa2ced8d77be8e074a302820937aa5993848c769b27" + name = "github.com/ZondaX/hid-go" + packages = ["."] + pruneopts = "UT" + revision = "48b08affede2cea076a3cf13b2e3f72ed262b743" + [[projects]] digest = "1:09a7f74eb6bb3c0f14d8926610c87f569c5cff68e978d30e9a3540aeb626fdf0" name = "github.com/bartekn/go-bip39" @@ -24,14 +32,6 @@ revision = "4aabc24848ce5fd31929f7d1e4ea74d3709c14cd" version = "v0.1.0" -[[projects]] - branch = "master" - digest = "1:7736fc6da04620727f8f3aa2ced8d77be8e074a302820937aa5993848c769b27" - name = "github.com/brejski/hid" - packages = ["."] - pruneopts = "UT" - revision = "48b08affede2cea076a3cf13b2e3f72ed262b743" - [[projects]] branch = "master" digest = "1:2c00f064ba355903866cbfbf3f7f4c0fe64af6638cc7d1b8bdcf3181bc67f1d8" @@ -508,11 +508,12 @@ version = "v0.9.0" [[projects]] - digest = "1:4dcb0dd65feecb068ce23a234d1a07c7868a1e39f52a6defcae0bb371d03abf6" + digest = "1:7886f86064faff6f8d08a3eb0e8c773648ff5a2e27730831e2bfbf07467f6666" name = "github.com/zondax/ledger-goclient" packages = ["."] pruneopts = "UT" - revision = "4296ee5701e945f9b3a7dbe51f402e0b9be57259" + revision = "58598458c11bc0ad1c1b8dac3dc3e11eaf270b79" + version = "v0.1.0" [[projects]] branch = "master" diff --git a/Gopkg.toml b/Gopkg.toml index a0aeb869f..6df6665ad 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -65,7 +65,7 @@ [[constraint]] name = "github.com/zondax/ledger-goclient" - revision = "4296ee5701e945f9b3a7dbe51f402e0b9be57259" + version = "=v0.1.0" [prune] go-tests = true diff --git a/PENDING.md b/PENDING.md index 7af308bf4..abb97988c 100644 --- a/PENDING.md +++ b/PENDING.md @@ -28,6 +28,7 @@ BREAKING CHANGES * [x/stake] [\#2040](https://github.com/cosmos/cosmos-sdk/issues/2040) Validator operator type has now changed to `sdk.ValAddress` * A new bech32 prefix has been introduced for Tendermint signing keys and addresses, `cosmosconspub` and `cosmoscons` respectively. + * [x/gov] \#2195 Made governance use BFT Time instead of Block Heights for deposit and voting periods. * SDK * [core] \#2219 Update to Tendermint 0.24.0 @@ -51,6 +52,7 @@ FEATURES * [lcd] Endpoints to query staking pool and params * [lcd] [\#2110](https://github.com/cosmos/cosmos-sdk/issues/2110) Add support for `simulate=true` requests query argument to endpoints that send txs to run simulations of transactions * [lcd] [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add support for `generate_only=true` query argument to generate offline unsigned transactions + * [lcd] [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) Add /sign endpoint to sign transactions generated with `generate_only=true`. * Gaia CLI (`gaiacli`) * [cli] Cmds to query staking pool and params @@ -60,7 +62,9 @@ FEATURES * [cli] [\#2047](https://github.com/cosmos/cosmos-sdk/issues/2047) Setting the --gas flag value to 0 triggers a simulation of the tx before the actual execution. The gas estimate obtained via the simulation will be used as gas limit in the actual execution. * [cli] [\#2047](https://github.com/cosmos/cosmos-sdk/issues/2047) The --gas-adjustment flag can be used to adjust the estimate obtained via the simulation triggered by --gas=0. * [cli] [\#2110](https://github.com/cosmos/cosmos-sdk/issues/2110) Add --dry-run flag to perform a simulation of a transaction without broadcasting it. The --gas flag is ignored as gas would be automatically estimated. - * [cli] [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add --generate-only flag to build an unsigned transaction and write it to STDOUT. + * [cli] [\#2204](https://github.com/cosmos/cosmos-sdk/issues/2204) Support generating and broadcasting messages with multiple signatures via command line: + * [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add --generate-only flag to build an unsigned transaction and write it to STDOUT. + * [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) New `sign` command to sign transactions generated with the --generate-only flag. * Gaia * [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address` diff --git a/client/input.go b/client/input.go index e7d13f3bf..b10f65ce6 100644 --- a/client/input.go +++ b/client/input.go @@ -24,7 +24,7 @@ func BufferStdin() *bufio.Reader { // It enforces the password length func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) { if inputIsTty() { - pass, err = speakeasy.Ask(prompt) + pass, err = speakeasy.FAsk(os.Stderr, prompt) } else { pass, err = readLineFromBuf(buf) } diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 656362bcd..8b4e31868 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -26,6 +26,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" + authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/stake" @@ -313,12 +314,13 @@ func TestIBCTransfer(t *testing.T) { // TODO: query ibc egress packet state } -func TestCoinSendGenerateOnly(t *testing.T) { +func TestCoinSendGenerateAndSign(t *testing.T) { name, password := "test", "1234567890" addr, seed := CreateAddr(t, "test", password, GetKeyBase(t)) cleanup, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) defer cleanup() - // create TX + + // generate TX res, body, _ := doSendWithGas(t, port, seed, name, password, addr, 0, 0, "?generate_only=true") require.Equal(t, http.StatusOK, res.StatusCode, body) var msg auth.StdTx @@ -327,6 +329,30 @@ func TestCoinSendGenerateOnly(t *testing.T) { require.Equal(t, msg.Msgs[0].Type(), "bank") require.Equal(t, msg.Msgs[0].GetSigners(), []sdk.AccAddress{addr}) require.Equal(t, 0, len(msg.Signatures)) + + // sign tx + var signedMsg auth.StdTx + acc := getAccount(t, port, addr) + accnum := acc.GetAccountNumber() + sequence := acc.GetSequence() + + payload := authrest.SignBody{ + Tx: msg, + LocalAccountName: name, + Password: password, + ChainID: viper.GetString(client.FlagChainID), + AccountNumber: accnum, + Sequence: sequence, + } + json, err := cdc.MarshalJSON(payload) + require.Nil(t, err) + res, body = Request(t, port, "POST", "/sign", json) + require.Equal(t, http.StatusOK, res.StatusCode, body) + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &signedMsg)) + require.Equal(t, len(msg.Msgs), len(signedMsg.Msgs)) + require.Equal(t, msg.Msgs[0].Type(), signedMsg.Msgs[0].Type()) + require.Equal(t, msg.Msgs[0].GetSigners(), signedMsg.Msgs[0].GetSigners()) + require.Equal(t, 1, len(signedMsg.Signatures)) } func TestTxs(t *testing.T) { diff --git a/client/utils/utils.go b/client/utils/utils.go index fa5bcf817..554406b4a 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "bytes" "fmt" "os" @@ -99,6 +100,49 @@ func PrintUnsignedStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs return } +// SignStdTx appends a signature to a StdTx and returns a copy of a it. If appendSig +// is false, it replaces the signatures already attached with the new signature. +func SignStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, name string, stdTx auth.StdTx, appendSig bool) (auth.StdTx, error) { + var signedStdTx auth.StdTx + + keybase, err := keys.GetKeyBase() + if err != nil { + return signedStdTx, err + } + info, err := keybase.Get(name) + if err != nil { + return signedStdTx, err + } + addr := info.GetPubKey().Address() + + // Check whether the address is a signer + if !isTxSigner(sdk.AccAddress(addr), stdTx.GetSigners()) { + fmt.Fprintf(os.Stderr, "WARNING: The generated transaction's intended signer does not match the given signer: '%v'", name) + } + + if txCtx.AccountNumber == 0 { + accNum, err := cliCtx.GetAccountNumber(addr) + if err != nil { + return signedStdTx, err + } + txCtx = txCtx.WithAccountNumber(accNum) + } + + if txCtx.Sequence == 0 { + accSeq, err := cliCtx.GetAccountSequence(addr) + if err != nil { + return signedStdTx, err + } + txCtx = txCtx.WithSequence(accSeq) + } + + passphrase, err := keys.GetPassphrase(name) + if err != nil { + return signedStdTx, err + } + return txCtx.SignStdTx(name, passphrase, stdTx, appendSig) +} + func adjustGasEstimate(estimate int64, adjustment float64) int64 { return int64(adjustment * float64(estimate)) } @@ -163,3 +207,12 @@ func buildUnsignedStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs } return auth.NewStdTx(stdSignMsg.Msgs, stdSignMsg.Fee, nil, stdSignMsg.Memo), nil } + +func isTxSigner(user sdk.AccAddress, signers []sdk.AccAddress) bool { + for _, s := range signers { + if bytes.Equal(user.Bytes(), s.Bytes()) { + return true + } + } + return false +} diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index 61affb261..ca864bf79 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -93,9 +93,8 @@ func appStateFn(r *rand.Rand, keys []crypto.PrivKey, accs []sdk.AccAddress) json func testAndRunTxs(app *GaiaApp) []simulation.Operation { return []simulation.Operation{ banksim.SimulateSingleInputMsgSend(app.accountMapper), - govsim.SimulateMsgSubmitProposal(app.govKeeper, app.stakeKeeper), + govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, app.stakeKeeper), govsim.SimulateMsgDeposit(app.govKeeper, app.stakeKeeper), - govsim.SimulateMsgVote(app.govKeeper, app.stakeKeeper), stakesim.SimulateMsgCreateValidator(app.accountMapper, app.stakeKeeper), stakesim.SimulateMsgEditValidator(app.stakeKeeper), stakesim.SimulateMsgDelegate(app.accountMapper, app.stakeKeeper), diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 8b66ab4a1..d8d242e5b 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -5,6 +5,7 @@ package clitest import ( "encoding/json" "fmt" + "io/ioutil" "os" "testing" @@ -332,7 +333,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, " 2 - Apples", proposalsQuery) } -func TestGaiaCLISendGenerateOnly(t *testing.T) { +func TestGaiaCLISendGenerateAndSign(t *testing.T) { chainID, servAddr, port := initializeFixtures(t) flags := fmt.Sprintf("--home=%s --node=%v --chain-id=%v", gaiacliHome, servAddr, chainID) @@ -343,6 +344,7 @@ func TestGaiaCLISendGenerateOnly(t *testing.T) { tests.WaitForTMStart(port) tests.WaitForNextNBlocksTM(2, port) + fooAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show foo --output=json --home=%s", gaiacliHome)) barAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show bar --output=json --home=%s", gaiacliHome)) // Test generate sendTx with default gas @@ -376,6 +378,35 @@ func TestGaiaCLISendGenerateOnly(t *testing.T) { require.Equal(t, msg.Fee.Gas, int64(100)) require.Equal(t, len(msg.Msgs), 1) require.Equal(t, 0, len(msg.GetSignatures())) + + // Write the output to disk + unsignedTxFile := writeToNewTempFile(t, stdout) + defer os.Remove(unsignedTxFile.Name()) + + // Test sign --print-sigs + success, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf( + "gaiacli sign %v --print-sigs %v", flags, unsignedTxFile.Name())) + require.True(t, success) + require.Equal(t, fmt.Sprintf("Signers:\n 0: %v\n\nSignatures:\n", fooAddr.String()), stdout) + + // Test sign + success, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf( + "gaiacli sign %v --name=foo %v", flags, unsignedTxFile.Name()), app.DefaultKeyPass) + require.True(t, success) + msg = unmarshalStdTx(t, stdout) + require.Equal(t, len(msg.Msgs), 1) + require.Equal(t, 1, len(msg.GetSignatures())) + require.Equal(t, fooAddr.String(), msg.GetSigners()[0].String()) + + // Write the output to disk + signedTxFile := writeToNewTempFile(t, stdout) + defer os.Remove(signedTxFile.Name()) + + // Test sign --print-signatures + success, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf( + "gaiacli sign %v --print-sigs %v", flags, signedTxFile.Name())) + require.True(t, success) + require.Equal(t, fmt.Sprintf("Signers:\n 0: %v\n\nSignatures:\n 0: %v\n", fooAddr.String(), fooAddr.String()), stdout) } //___________________________________________________________________________________ @@ -408,6 +439,14 @@ func unmarshalStdTx(t *testing.T, s string) (stdTx auth.StdTx) { return } +func writeToNewTempFile(t *testing.T, s string) *os.File { + fp, err := ioutil.TempFile(os.TempDir(), "cosmos_cli_test_") + require.Nil(t, err) + _, err = fp.WriteString(s) + require.Nil(t, err) + return fp +} + //___________________________________________________________________________________ // executors diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index df0fd3c11..7a1af6221 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -127,6 +127,7 @@ func main() { rootCmd.AddCommand( client.GetCommands( authcmd.GetAccountCmd("acc", cdc, authcmd.GetAccountDecoder(cdc)), + authcmd.GetSignCommand(cdc, authcmd.GetAccountDecoder(cdc)), )...) rootCmd.AddCommand( client.PostCommands( diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 8ff722b33..c8968486a 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -5,7 +5,8 @@ - [ ] 3. Merge items in `PENDING.md` into the `CHANGELOG.md`. While doing this make sure that each entry contains links to issues/PRs for each item - [ ] 4. Summarize breaking API changes section under “Breaking Changes” section to the `CHANGELOG.md` to bring attention to any breaking API changes that affect RPC consumers. - [ ] 5. Tag the commit `{ .Release.Name }-rcN` -- [ ] 6. Kick off 1 day of automated fuzz testing -- [ ] 7. Release Lead assigns 2 people to perform [buddy testing script](/docs/RELEASE_TEST_SCRIPT.md) and update the relevant documentation -- [ ] 8. If errors are found in either #6 or #7 go back to #2 (*NOTE*: be sure to increment the `rcN`) -- [ ] 9. After #6 and #7 have successfully completed then merge the release PR and push the final release tag +- [ ] 6. Open a branch & PR to merge the pending release back into `develop`. If any changes are made to the release branch, they must also be made to the pending develop merge, and both must pass tests. +- [ ] 7. Kick off 1 day of automated fuzz testing +- [ ] 8. Release Lead assigns 2 people to perform [buddy testing script](/docs/RELEASE_TEST_SCRIPT.md) and update the relevant documentation +- [ ] 9. If errors are found in either #6 or #7 go back to #2 (*NOTE*: be sure to increment the `rcN`) +- [ ] 10. After #6 and #7 have successfully completed then merge the release PR and push the final release tag diff --git a/docs/light/api.md b/docs/light/api.md index 7168cf9d7..293b023cd 100644 --- a/docs/light/api.md +++ b/docs/light/api.md @@ -229,6 +229,75 @@ Returns on success: } ``` +### POST /auth/accounts/sign + +- **URL**: `/auth/sign` +- **Functionality**: Sign a transaction without broadcasting it. +- Returns on success: + +```json +{ + "rest api": "1.0", + "code": 200, + "error": "", + "result": { + "type": "auth/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/Send", + "value": { + "inputs": [ + { + "address": "cosmos1ql4ekxkujf3xllk8h5ldhhgh4ylpu7kwec6q3d", + "coins": [ + { + "denom": "steak", + "amount": "1" + } + ] + } + ], + "outputs": [ + { + "address": "cosmos1dhyqhg4px33ed3erqymls0hc7q2lxw9hhfwklj", + "coins": [ + { + "denom": "steak", + "amount": "1" + } + ] + } + ] + } + } + ], + "fee": { + "amount": [ + { + "denom": "", + "amount": "0" + } + ], + "gas": "2742" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A2A/f2IYnrPUMTMqhwN81oas9jurtfcsvxdeLlNw3gGy" + }, + "signature": "MEQCIGVn73y9QLwBa3vmsAD1bs3ygX75Wo+lAFSAUDs431ZPAiBWAf2amyqTCDXE9J87rL9QF9sd5JvVMt7goGSuamPJwg==", + "account_number": "1", + "sequence": "0" + } + ], + "memo": "" + } + } +} +``` + ## ICS20 - TokenAPI The TokenAPI exposes all functionality needed to query account balances and send transactions. diff --git a/docs/sdk/clients.md b/docs/sdk/clients.md index fdfbca7bd..0cc87cf21 100644 --- a/docs/sdk/clients.md +++ b/docs/sdk/clients.md @@ -147,7 +147,16 @@ gaiacli send \ --chain-id= \ --name= \ --to= \ - --generate-only + --generate-only > unsignedSendTx.json +``` + +You can now sign the transaction file generated through the `--generate-only` flag by providing your key to the following command: + +```bash +gaiacli sign \ + --chain-id= \ + --name= + unsignedSendTx.json > signedSendTx.json ``` ### Staking diff --git a/types/account.go b/types/address.go similarity index 100% rename from types/account.go rename to types/address.go diff --git a/types/account_test.go b/types/address_test.go similarity index 100% rename from types/account_test.go rename to types/address_test.go diff --git a/x/auth/client/cli/sign.go b/x/auth/client/cli/sign.go new file mode 100644 index 000000000..e5cd41235 --- /dev/null +++ b/x/auth/client/cli/sign.go @@ -0,0 +1,91 @@ +package cli + +import ( + "fmt" + "io/ioutil" + + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authctx "github.com/cosmos/cosmos-sdk/x/auth/client/context" + "github.com/spf13/cobra" + amino "github.com/tendermint/go-amino" +) + +const ( + flagAppend = "append" + flagPrintSigs = "print-sigs" +) + +// GetSignCommand returns the sign command +func GetSignCommand(codec *amino.Codec, decoder auth.AccountDecoder) *cobra.Command { + cmd := &cobra.Command{ + Use: "sign ", + Short: "Sign transactions", + Long: `Sign transactions created with the --generate-only flag. +Read a transaction from , sign it, and print its JSON encoding.`, + RunE: makeSignCmd(codec, decoder), + Args: cobra.ExactArgs(1), + } + cmd.Flags().String(client.FlagName, "", "Name of private key with which to sign") + cmd.Flags().Bool(flagAppend, true, "Append the signature to the existing ones. If disabled, old signatures would be overwritten") + cmd.Flags().Bool(flagPrintSigs, false, "Print the addresses that must sign the transaction and those who have already signed it, then exit") + return cmd +} + +func makeSignCmd(cdc *amino.Codec, decoder auth.AccountDecoder) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) (err error) { + stdTx, err := readAndUnmarshalStdTx(cdc, args[0]) + if err != nil { + return + } + + if viper.GetBool(flagPrintSigs) { + printSignatures(stdTx) + return nil + } + + name := viper.GetString(client.FlagName) + cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(decoder) + txCtx := authctx.NewTxContextFromCLI() + + newTx, err := utils.SignStdTx(txCtx, cliCtx, name, stdTx, viper.GetBool(flagAppend)) + if err != nil { + return err + } + json, err := cdc.MarshalJSON(newTx) + if err != nil { + return err + } + fmt.Printf("%s\n", json) + return + } +} + +func printSignatures(stdTx auth.StdTx) { + fmt.Println("Signers:") + for i, signer := range stdTx.GetSigners() { + fmt.Printf(" %v: %v\n", i, signer.String()) + } + fmt.Println("") + fmt.Println("Signatures:") + for i, sig := range stdTx.GetSignatures() { + fmt.Printf(" %v: %v\n", i, sdk.AccAddress(sig.Address()).String()) + } + return +} + +func readAndUnmarshalStdTx(cdc *amino.Codec, filename string) (stdTx auth.StdTx, err error) { + var bytes []byte + if bytes, err = ioutil.ReadFile(filename); err != nil { + return + } + if err = cdc.UnmarshalJSON(bytes, &stdTx); err != nil { + return + } + return +} diff --git a/x/auth/client/context/context.go b/x/auth/client/context/context.go index 5e55696b8..fe030449b 100644 --- a/x/auth/client/context/context.go +++ b/x/auth/client/context/context.go @@ -117,24 +117,11 @@ func (ctx TxContext) Build(msgs []sdk.Msg) (auth.StdSignMsg, error) { // Sign signs a transaction given a name, passphrase, and a single message to // signed. An error is returned if signing fails. func (ctx TxContext) Sign(name, passphrase string, msg auth.StdSignMsg) ([]byte, error) { - keybase, err := keys.GetKeyBase() + sig, err := MakeSignature(name, passphrase, msg) if err != nil { return nil, err } - - sig, pubkey, err := keybase.Sign(name, passphrase, msg.Bytes()) - if err != nil { - return nil, err - } - - sigs := []auth.StdSignature{{ - AccountNumber: msg.AccountNumber, - Sequence: msg.Sequence, - PubKey: pubkey, - Signature: sig, - }} - - return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, sigs, msg.Memo)) + return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, []auth.StdSignature{sig}, msg.Memo)) } // BuildAndSign builds a single message to be signed, and signs a transaction @@ -177,3 +164,46 @@ func (ctx TxContext) BuildWithPubKey(name string, msgs []sdk.Msg) ([]byte, error return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, sigs, msg.Memo)) } + +// SignStdTx appends a signature to a StdTx and returns a copy of a it. If append +// is false, it replaces the signatures already attached with the new signature. +func (ctx TxContext) SignStdTx(name, passphrase string, stdTx auth.StdTx, appendSig bool) (signedStdTx auth.StdTx, err error) { + stdSignature, err := MakeSignature(name, passphrase, auth.StdSignMsg{ + ChainID: ctx.ChainID, + AccountNumber: ctx.AccountNumber, + Sequence: ctx.Sequence, + Fee: stdTx.Fee, + Msgs: stdTx.GetMsgs(), + Memo: stdTx.GetMemo(), + }) + if err != nil { + return + } + + sigs := stdTx.GetSignatures() + if len(sigs) == 0 || !appendSig { + sigs = []auth.StdSignature{stdSignature} + } else { + sigs = append(sigs, stdSignature) + } + signedStdTx = auth.NewStdTx(stdTx.GetMsgs(), stdTx.Fee, sigs, stdTx.GetMemo()) + return +} + +// MakeSignature builds a StdSignature given key name, passphrase, and a StdSignMsg. +func MakeSignature(name, passphrase string, msg auth.StdSignMsg) (sig auth.StdSignature, err error) { + keybase, err := keys.GetKeyBase() + if err != nil { + return + } + sigBytes, pubkey, err := keybase.Sign(name, passphrase, msg.Bytes()) + if err != nil { + return + } + return auth.StdSignature{ + AccountNumber: msg.AccountNumber, + Sequence: msg.Sequence, + PubKey: pubkey, + Signature: sigBytes, + }, nil +} diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index 6ad50a14d..3a5ab756d 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -20,6 +20,10 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, s "/accounts/{address}", QueryAccountRequestHandlerFn(storeName, cdc, authcmd.GetAccountDecoder(cdc), cliCtx), ).Methods("GET") + r.HandleFunc( + "/sign", + SignTxRequestHandlerFn(cdc, cliCtx), + ).Methods("POST") } // query accountREST Handler diff --git a/x/auth/client/rest/sign.go b/x/auth/client/rest/sign.go new file mode 100644 index 000000000..7ec703884 --- /dev/null +++ b/x/auth/client/rest/sign.go @@ -0,0 +1,61 @@ +package rest + +import ( + "io/ioutil" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + authctx "github.com/cosmos/cosmos-sdk/x/auth/client/context" +) + +// SignBody defines the properties of a sign request's body. +type SignBody struct { + Tx auth.StdTx `json:"tx"` + LocalAccountName string `json:"name"` + Password string `json:"password"` + ChainID string `json:"chain_id"` + AccountNumber int64 `json:"account_number"` + Sequence int64 `json:"sequence"` + AppendSig bool `json:"append_sig"` +} + +// sign tx REST handler +func SignTxRequestHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + var m SignBody + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + txCtx := authctx.TxContext{ + ChainID: m.ChainID, + AccountNumber: m.AccountNumber, + Sequence: m.Sequence, + } + + signedTx, err := txCtx.SignStdTx(m.LocalAccountName, m.Password, m.Tx, m.AppendSig) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + output, err := wire.MarshalJSONIndent(cdc, signedTx) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(output) + } +} diff --git a/x/gov/simulation/msgs.go b/x/gov/simulation/msgs.go index 13e537efa..2bb4b07b0 100644 --- a/x/gov/simulation/msgs.go +++ b/x/gov/simulation/msgs.go @@ -2,6 +2,7 @@ package simulation import ( "fmt" + "math" "math/rand" "testing" @@ -18,30 +19,89 @@ const ( denom = "steak" ) +// SimulateSubmittingVotingAndSlashingForProposal simulates creating a msg Submit Proposal +// voting on the proposal, and subsequently slashing the proposal. It is implemented using +// future operations. +// TODO: Vote more intelligently, so we can actually do some checks regarding votes passing or failing +// TODO: Actually check that validator slashings happened +func SimulateSubmittingVotingAndSlashingForProposal(k gov.Keeper, sk stake.Keeper) simulation.Operation { + handler := gov.NewHandler(k) + // The states are: + // column 1: All validators vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, values chosen by @valardragon semi-arbitrarily, + // feel free to change. + numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { + // 1) submit proposal now + sender := simulation.RandomKey(r, keys) + msg := simulationCreateMsgSubmitProposal(tb, r, sender, log) + action = simulateHandleMsgSubmitProposal(msg, sk, handler, ctx, event) + proposalID := k.GetLastProposalID(ctx) + // 2) Schedule operations for votes + // 2.1) first pick a number of people to vote. + curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(keys)) * statePercentageArray[curNumVotesState])) + // 2.2) select who votes and when + whoVotes := r.Perm(len(keys)) + // didntVote := whoVotes[numVotes:] + whoVotes = whoVotes[:numVotes] + votingPeriod := k.GetVotingProcedure(ctx).VotingPeriod + fops := make([]simulation.FutureOperation, numVotes+1) + for i := 0; i < numVotes; i++ { + whenVote := ctx.BlockHeight() + r.Int63n(votingPeriod) + fops[i] = simulation.FutureOperation{int(whenVote), operationSimulateMsgVote(k, sk, keys[whoVotes[i]], proposalID)} + } + // 3) Make an operation to ensure slashes were done correctly. (Really should be a future invariant) + // TODO: Find a way to check if a validator was slashed other than just checking their balance a block + // before and after. + + return action, fops, nil + } +} + // SimulateMsgSubmitProposal simulates a msg Submit Proposal // Note: Currently doesn't ensure that the proposal txt is in JSON form func SimulateMsgSubmitProposal(k gov.Keeper, sk stake.Keeper) simulation.Operation { handler := gov.NewHandler(k) return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOps []simulation.FutureOperation, err sdk.Error) { - msg := simulationCreateMsgSubmitProposal(tb, r, keys, log) - ctx, write := ctx.CacheContext() - result := handler(ctx, msg) - if result.IsOK() { - // Update pool to keep invariants - pool := sk.GetPool(ctx) - pool.LooseTokens = pool.LooseTokens.Sub(sdk.NewDecFromInt(msg.InitialDeposit.AmountOf(denom))) - sk.SetPool(ctx, pool) - write() - } - event(fmt.Sprintf("gov/MsgSubmitProposal/%v", result.IsOK())) - action = fmt.Sprintf("TestMsgSubmitProposal: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + sender := simulation.RandomKey(r, keys) + msg := simulationCreateMsgSubmitProposal(tb, r, sender, log) + action = simulateHandleMsgSubmitProposal(msg, sk, handler, ctx, event) return action, nil, nil } } -func simulationCreateMsgSubmitProposal(tb testing.TB, r *rand.Rand, keys []crypto.PrivKey, log string) gov.MsgSubmitProposal { - key := simulation.RandomKey(r, keys) - addr := sdk.AccAddress(key.PubKey().Address()) +func simulateHandleMsgSubmitProposal(msg gov.MsgSubmitProposal, sk stake.Keeper, handler sdk.Handler, ctx sdk.Context, event func(string)) (action string) { + ctx, write := ctx.CacheContext() + result := handler(ctx, msg) + if result.IsOK() { + // Update pool to keep invariants + pool := sk.GetPool(ctx) + pool.LooseTokens = pool.LooseTokens.Sub(sdk.NewDecFromInt(msg.InitialDeposit.AmountOf(denom))) + sk.SetPool(ctx, pool) + write() + } + event(fmt.Sprintf("gov/MsgSubmitProposal/%v", result.IsOK())) + action = fmt.Sprintf("TestMsgSubmitProposal: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action +} + +func simulationCreateMsgSubmitProposal(tb testing.TB, r *rand.Rand, sender crypto.PrivKey, log string) gov.MsgSubmitProposal { + addr := sdk.AccAddress(sender.PubKey().Address()) deposit := randomDeposit(r) msg := gov.NewMsgSubmitProposal( simulation.RandStringOfLength(r, 5), @@ -88,13 +148,22 @@ func SimulateMsgDeposit(k gov.Keeper, sk stake.Keeper) simulation.Operation { // SimulateMsgVote // nolint: unparam func SimulateMsgVote(k gov.Keeper, sk stake.Keeper) simulation.Operation { + return operationSimulateMsgVote(k, sk, nil, -1) +} + +func operationSimulateMsgVote(k gov.Keeper, sk stake.Keeper, key crypto.PrivKey, proposalID int64) simulation.Operation { return func(tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, fOp []simulation.FutureOperation, err sdk.Error) { - key := simulation.RandomKey(r, keys) - addr := sdk.AccAddress(key.PubKey().Address()) - proposalID, ok := randomProposalID(r, k, ctx) - if !ok { - return "no-operation", nil, nil + if key == nil { + key = simulation.RandomKey(r, keys) } + var ok bool + if proposalID < 0 { + proposalID, ok = randomProposalID(r, k, ctx) + if !ok { + return "no-operation", nil, nil + } + } + addr := sdk.AccAddress(key.PubKey().Address()) option := randomVotingOption(r) msg := gov.NewMsgVote(addr, proposalID, option) if msg.ValidateBasic() != nil { diff --git a/x/gov/simulation/sim_test.go b/x/gov/simulation/sim_test.go index 6f0f9830c..540dcb447 100644 --- a/x/gov/simulation/sim_test.go +++ b/x/gov/simulation/sim_test.go @@ -53,6 +53,7 @@ func TestGovWithRandomMessages(t *testing.T) { gov.InitGenesis(ctx, govKeeper, gov.DefaultGenesisState()) } + // Test with unscheduled votes simulation.Simulate( t, mapp.BaseApp, appStateFn, []simulation.Operation{ @@ -66,4 +67,18 @@ func TestGovWithRandomMessages(t *testing.T) { }, 10, 100, false, ) + + // Test with scheduled votes + simulation.Simulate( + t, mapp.BaseApp, appStateFn, + []simulation.Operation{ + SimulateSubmittingVotingAndSlashingForProposal(govKeeper, stakeKeeper), + SimulateMsgDeposit(govKeeper, stakeKeeper), + }, []simulation.RandSetup{ + setup, + }, []simulation.Invariant{ + AllInvariants(), + }, 10, 100, + false, + ) }