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 7, 2018
1 parent 94b86f8 commit e6a8a4d
Show file tree
Hide file tree
Showing 13 changed files with 409 additions and 23 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ test_cover:

test_lint:
gometalinter.v2 --config=tools/gometalinter.json ./...
!(gometalinter.v2 --disable-all --enable='errcheck' --vendor ./... | grep -v "client/")
!(gometalinter.v2 --disable-all --enable='errcheck' --vendor ./... | grep -v -e "client/" -e "fmt\.Fprintf")
find . -name '*.go' -type f -not -path "./vendor*" -not -path "*.git*" | xargs gofmt -d -s
dep status >> /dev/null
!(grep -n branch Gopkg.toml)
Expand Down
7 changes: 5 additions & 2 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ BREAKING CHANGES
* 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] [\#1807](https://github.com/cosmos/cosmos-sdk/issues/1807) Switch from use of rational to decimal
* [types] [\#1901](https://github.com/cosmos/cosmos-sdk/issues/1901) Validator interface's GetOwner() renamed to GetOperator()
Expand All @@ -48,6 +48,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
Expand All @@ -57,7 +58,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`
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
30 changes: 28 additions & 2 deletions client/lcd/lcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
53 changes: 53 additions & 0 deletions client/utils/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
"bytes"
"fmt"
"os"

Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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
}
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
69 changes: 69 additions & 0 deletions docs/light/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion docs/sdk/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,16 @@ gaiacli send \
--chain-id=<chain_id> \
--name=<key_name> \
--to=<destination_cosmosaccaddr> \
--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=<chain_id> \
--name=<key_name>
unsignedSendTx.json > signedSendTx.json
```

### Staking
Expand Down
Loading

0 comments on commit e6a8a4d

Please sign in to comment.