Skip to content

Commit

Permalink
Implement command/REST endpoint for offline tx sign off #1953
Browse files Browse the repository at this point in the history
* Add sign CLI command to sign transactions generated with the
  --generate-only flag.
* Add /sign REST endpoint for Voyager support.

Redirect password prompt to STDERR to avoid messing up cli
commands output. As a rule of thumb, program's output should
always go to STDOUT, whilst errors&diagnostics go to STDERR
as per POSIX's philosophy and specs.
  • Loading branch information
Alessio Treglia committed Sep 5, 2018
1 parent a612068 commit 0ada320
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 19 deletions.
2 changes: 2 additions & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ FEATURES
* [lcd] Endpoints to query staking pool and params
* [lcd] \#2110 Add support for `simulate=true` requests query argument to endpoints that send txs to run simulations of transactions
* [lcd] \#966 Add support for `generate_only=true` query argument to generate offline unsigned transactions
* [lcd] \#1953 Add /sign endpoint to sign transactions generated with `generate_only=true`.

* Gaia CLI (`gaiacli`)
* [cli] Cmds to query staking pool and params
Expand All @@ -57,6 +58,7 @@ FEATURES
* [cli] \#2047 The --gas-adjustment flag can be used to adjust the estimate obtained via the simulation triggered by --gas=0.
* [cli] \#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 Add --generate-only flag to build an unsigned transaction and write it to STDOUT.
* [cli] \#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`
Expand Down
2 changes: 1 addition & 1 deletion client/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
34 changes: 33 additions & 1 deletion client/lcd/lcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ func TestCoinSendGenerateOnly(t *testing.T) {
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
Expand All @@ -327,6 +328,37 @@ 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 := 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"`
}{
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) {
Expand Down
42 changes: 41 additions & 1 deletion cmd/gaia/cli_test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package clitest
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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)
}

//___________________________________________________________________________________
Expand Down Expand Up @@ -408,6 +439,15 @@ 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)
// defer os.Remove(signedTxFile.Name())
_, err = fp.WriteString(s)
require.Nil(t, err)
return fp
}

//___________________________________________________________________________________
// executors

Expand Down
1 change: 1 addition & 0 deletions cmd/gaia/cmd/gaiacli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 11 additions & 1 deletion docs/sdk/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,19 @@ gaiacli send \
--chain-id=<chain_id> \
--name=<key_name> \
--to=<destination_cosmosaccaddr> \
--generate-only
--generate-only > unsignedSendTx.json
```

You can sign transactions file generated with `--generate-only` later on with the `sign` command and a key:

```bash
gaiacli sign \
--chain-id=<chain_id> \
--name=<key_name>
unsignedSendTx.json > signedSendTx.json
```


### Staking

#### Set up a Validator
Expand Down
124 changes: 124 additions & 0 deletions x/auth/client/cli/sign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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/keys"
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 (
flagOverwriteSigs = "overwrite"
flagPrintSigs = "print-sigs"
)

// GetSignCommand returns the sign command
func GetSignCommand(codec *amino.Codec, decoder auth.AccountDecoder) *cobra.Command {
cmd := &cobra.Command{
Use: "sign <file>",
Short: "Sign transactions",
Long: `Sign transactions created with the --generate-only flag.
Read a transaction from <file>, 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(flagOverwriteSigs, false, "Overwrite the signatures that are already attached to the transaction")
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)
keybase, err := keys.GetKeyBase()
if err != nil {
return
}
info, err := keybase.Get(name)
if err != nil {
return
}

cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(decoder)
acc, err := cliCtx.GetAccount(sdk.AccAddress(info.GetPubKey().Address()))
if err != nil {
return err
}

passphrase, err := keys.GetPassphrase(name)
if err != nil {
return err
}
newTx, err := signStdTx(stdTx, name, passphrase, acc)
if err != nil {
return err
}
json, err := cdc.MarshalJSON(newTx)
if err != nil {
return err
}
fmt.Printf("%s\n", json)
return
}
}

func signStdTx(stdTx auth.StdTx, name, passphrase string, acc auth.Account) (signedStdTx auth.StdTx, err error) {
stdSignature, err := authctx.MakeSignature(name, passphrase, auth.StdSignMsg{
ChainID: viper.GetString(client.FlagChainID),
AccountNumber: acc.GetAccountNumber(),
Sequence: acc.GetSequence(),
Fee: stdTx.Fee,
Msgs: stdTx.GetMsgs(),
Memo: stdTx.GetMemo(),
})
if err != nil {
return
}

signedStdTx = authctx.SignStdTx(stdTx, stdSignature, viper.GetBool(flagOverwriteSigs))
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
}
47 changes: 32 additions & 15 deletions x/auth/client/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -177,3 +164,33 @@ func (ctx TxContext) BuildWithPubKey(name string, msgs []sdk.Msg) ([]byte, error

return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, sigs, msg.Memo))
}

// 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
}

// SignStdTx attach a signature to a StdTx and returns a copy of a it. If overwriteSigs is true,
// it replaces the signatures already attached if there's any with the given signature.
func SignStdTx(stdTx auth.StdTx, stdSignature auth.StdSignature, overwriteSigs bool) auth.StdTx {
sigs := stdTx.GetSignatures()
if len(sigs) == 0 || overwriteSigs {
sigs = []auth.StdSignature{stdSignature}
} else {
sigs = append(sigs, stdSignature)
}
return auth.NewStdTx(stdTx.GetMsgs(), stdTx.Fee, sigs, stdTx.GetMemo())
}
4 changes: 4 additions & 0 deletions x/auth/client/rest/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 0ada320

Please sign in to comment.