Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R4R: Implement command/REST endpoint for offline signing #1953 #2216

Merged
merged 2 commits into from
Sep 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
alessio marked this conversation as resolved.
Show resolved Hide resolved
} 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))
alessio marked this conversation as resolved.
Show resolved Hide resolved
}

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)
alessio marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
41 changes: 40 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,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

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
alessio marked this conversation as resolved.
Show resolved Hide resolved
```

### Staking
Expand Down
Loading