Skip to content

Commit

Permalink
Introduce simulate mode
Browse files Browse the repository at this point in the history
Add a simulate only flag '--dry-run' to both CLI tx commands
and RESTful endpoints to trigger the simulation of unsigned
transactions.

* Turning --dry-run on causes the --gas flag to be ignored.
  The simulation will return the estimate of the gas required
  to actually run the transaction.
* Adjustment is no longer required. It now defaults to 1.0.
* In some test cases accounts retrieved from the state do not
  come with a PubKey. In such cases, a fake secp256k1 key is
  generated and gas consumption calculated accordingly.

Closes: #2110
  • Loading branch information
alessio committed Aug 30, 2018
1 parent fd8c1e5 commit e460e3e
Show file tree
Hide file tree
Showing 25 changed files with 375 additions and 164 deletions.
2 changes: 2 additions & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ FEATURES

* Gaia REST API (`gaiacli advanced rest-server`)
* [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

* Gaia CLI (`gaiacli`)
* [cli] Cmds to query staking pool and params
* [gov][cli] #2062 added `--proposal` flag to `submit-proposal` that allows a JSON file containing a proposal to be passed in
* [cli] \#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 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.

* Gaia

Expand Down
2 changes: 2 additions & 0 deletions client/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type CLIContext struct {
Async bool
JSON bool
PrintResponse bool
DryRun bool
}

// NewCLIContext returns a new initialized CLIContext with parameters from the
Expand All @@ -57,6 +58,7 @@ func NewCLIContext() CLIContext {
Async: viper.GetBool(client.FlagAsync),
JSON: viper.GetBool(client.FlagJson),
PrintResponse: viper.GetBool(client.FlagPrintResponse),
DryRun: viper.GetBool(client.FlagDryRun),
}
}

Expand Down
6 changes: 4 additions & 2 deletions client/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "github.com/spf13/cobra"
// nolint
const (
DefaultGasLimit = 200000
DefaultGasAdjustment = 1.2
DefaultGasAdjustment = 1.0

FlagUseLedger = "ledger"
FlagChainID = "chain-id"
Expand All @@ -23,6 +23,7 @@ const (
FlagAsync = "async"
FlagJson = "json"
FlagPrintResponse = "print-response"
FlagDryRun = "dry-run"
)

// LineBreak can be included in a command list to provide a blank line
Expand Down Expand Up @@ -54,10 +55,11 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command {
c.Flags().String(FlagNode, "tcp://localhost:26657", "<host>:<port> to tendermint rpc interface for this chain")
c.Flags().Bool(FlagUseLedger, false, "Use a connected Ledger device")
c.Flags().Int64(FlagGas, DefaultGasLimit, "gas limit to set per-transaction; set to 0 to calculate required gas automatically")
c.Flags().Float64(FlagGasAdjustment, DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation")
c.Flags().Float64(FlagGasAdjustment, DefaultGasAdjustment, "adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored ")
c.Flags().Bool(FlagAsync, false, "broadcast transactions asynchronously")
c.Flags().Bool(FlagJson, false, "return output in json format")
c.Flags().Bool(FlagPrintResponse, true, "return tx response (only works with async = false)")
c.Flags().Bool(FlagDryRun, false, "ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it")
}
return cmds
}
19 changes: 13 additions & 6 deletions client/lcd/lcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lcd

import (
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"regexp"
Expand Down Expand Up @@ -265,11 +266,17 @@ func TestCoinSend(t *testing.T) {
require.Equal(t, int64(1), mycoins.Amount.Int64())

// test failure with too little gas
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 100)
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 100, "")
require.Equal(t, http.StatusInternalServerError, res.StatusCode, body)

// test success with just enough gas
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 3000)
// run simulation and test success with estimated gas
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 0, "?simulate=true")
require.Equal(t, http.StatusOK, res.StatusCode, body)
var responseBody struct {
GasEstimate int64 `json:"gas_estimate"`
}
require.Nil(t, json.Unmarshal([]byte(body), &responseBody))
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, responseBody.GasEstimate, "")
require.Equal(t, http.StatusOK, res.StatusCode, body)
}

Expand Down Expand Up @@ -720,7 +727,7 @@ func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account {
return acc
}

func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.AccAddress, gas int64) (res *http.Response, body string, receiveAddr sdk.AccAddress) {
func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.AccAddress, gas int64, queryStr string) (res *http.Response, body string, receiveAddr sdk.AccAddress) {

// create receive address
kb := client.MockKeyBase()
Expand Down Expand Up @@ -754,12 +761,12 @@ func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.Acc
"chain_id":"%s"
}`, gasStr, name, password, accnum, sequence, coinbz, chainID))

res, body = Request(t, port, "POST", fmt.Sprintf("/accounts/%s/send", receiveAddr), jsonStr)
res, body = Request(t, port, "POST", fmt.Sprintf("/accounts/%s/send%v", receiveAddr, queryStr), jsonStr)
return
}

func doSend(t *testing.T, port, seed, name, password string, addr sdk.AccAddress) (receiveAddr sdk.AccAddress, resultTx ctypes.ResultBroadcastTxCommit) {
res, body, receiveAddr := doSendWithGas(t, port, seed, name, password, addr, 0)
res, body, receiveAddr := doSendWithGas(t, port, seed, name, password, addr, 0, "")
require.Equal(t, http.StatusOK, res.StatusCode, body)

err := cdc.UnmarshalJSON([]byte(body), &resultTx)
Expand Down
18 changes: 18 additions & 0 deletions client/utils/rest.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
package utils

import (
"fmt"
"net/http"
)

const (
queryArgDryRun = "simulate"
)

// WriteErrorResponse prepares and writes a HTTP error
// given a status code and an error message.
func WriteErrorResponse(w *http.ResponseWriter, status int, msg string) {
(*w).WriteHeader(status)
(*w).Write([]byte(msg))
}

// WriteGasEstimateResponse prepares and writes an HTTP
// response for transactions simulations.
func WriteSimulationResponse(w *http.ResponseWriter, gas int64) {
(*w).WriteHeader(http.StatusOK)
(*w).Write([]byte(fmt.Sprintf(`{"gas_estimate":%v}`, gas)))
}

// HasDryRunArg returns true if the request's URL query contains
// the dry run argument and its value is set to "true".
func HasDryRunArg(r *http.Request) bool {
return r.URL.Query().Get(queryArgDryRun) == "true"
}
97 changes: 53 additions & 44 deletions client/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,35 @@ import (
// DefaultGasAdjustment is applied to gas estimates to avoid tx
// execution failures due to state changes that might
// occur between the tx simulation and the actual run.
const DefaultGasAdjustment = 1.2
const DefaultGasAdjustment = 1.0

// SendTx implements a auxiliary handler that facilitates sending a series of
// messages in a signed transaction given a TxContext and a QueryContext. It
// ensures that the account exists, has a proper number and sequence set. In
// addition, it builds and signs a transaction with the supplied messages.
// Finally, it broadcasts the signed transaction to a node.
func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) error {
if err := cliCtx.EnsureAccountExists(); err != nil {
return err
}

from, err := cliCtx.GetFromAddress()
txCtx, err := prepareTxContext(txCtx, cliCtx)
if err != nil {
return err
}

// TODO: (ref #1903) Allow for user supplied account number without
// automatically doing a manual lookup.
if txCtx.AccountNumber == 0 {
accNum, err := cliCtx.GetAccountNumber(from)
autogas := cliCtx.DryRun || (cliCtx.Gas == 0)
if autogas {
txCtx, err = EnrichCtxWithGas(txCtx, cliCtx, cliCtx.FromAddressName, msgs)
if err != nil {
return err
}

txCtx = txCtx.WithAccountNumber(accNum)
fmt.Fprintf(os.Stdout, "estimated gas = %v\n", txCtx.Gas)
}

// TODO: (ref #1903) Allow for user supplied account sequence without
// automatically doing a manual lookup.
if txCtx.Sequence == 0 {
accSeq, err := cliCtx.GetAccountSequence(from)
if err != nil {
return err
}

txCtx = txCtx.WithSequence(accSeq)
if cliCtx.DryRun {
return nil
}

passphrase, err := keys.GetPassphrase(cliCtx.FromAddressName)
if err != nil {
return err
}

if cliCtx.Gas == 0 {
txCtx, err = EnrichCtxWithGas(txCtx, cliCtx, cliCtx.FromAddressName, passphrase, msgs)
if err != nil {
return err
}
}

// build and sign the transaction
txBytes, err := txCtx.BuildAndSign(cliCtx.FromAddressName, passphrase, msgs)
if err != nil {
Expand All @@ -75,26 +53,26 @@ func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg)
return cliCtx.EnsureBroadcastTx(txBytes)
}

// EnrichCtxWithGas calculates the gas estimate that would be consumed by the
// transaction and set the transaction's respective value accordingly.
func EnrichCtxWithGas(txCtx authctx.TxContext, cliCtx context.CLIContext, name, passphrase string, msgs []sdk.Msg) (authctx.TxContext, error) {
txBytes, err := BuildAndSignTxWithZeroGas(txCtx, name, passphrase, msgs)
// SimulateMsgs simulates the transaction and returns the gas estimate and the adjusted value.
func SimulateMsgs(txCtx authctx.TxContext, cliCtx context.CLIContext, name string, msgs []sdk.Msg, gas int64) (estimated, adjusted int64, err error) {
txBytes, err := txCtx.WithGas(gas).BuildWithPubKey(name, msgs)
if err != nil {
return txCtx, err
return
}
estimate, adjusted, err := CalculateGas(cliCtx.Query, cliCtx.Codec, txBytes, cliCtx.GasAdjustment)
estimated, adjusted, err = CalculateGas(cliCtx.Query, cliCtx.Codec, txBytes, cliCtx.GasAdjustment)
return
}

// EnrichCtxWithGas calculates the gas estimate that would be consumed by the
// transaction and set the transaction's respective value accordingly.
func EnrichCtxWithGas(txCtx authctx.TxContext, cliCtx context.CLIContext, name string, msgs []sdk.Msg) (authctx.TxContext, error) {
_, adjusted, err := SimulateMsgs(txCtx, cliCtx, name, msgs, 0)
if err != nil {
return txCtx, err
}
fmt.Fprintf(os.Stderr, "gas: [estimated = %v] [adjusted = %v]\n", estimate, adjusted)
return txCtx.WithGas(adjusted), nil
}

// BuildAndSignTxWithZeroGas builds transactions with GasWanted set to 0.
func BuildAndSignTxWithZeroGas(txCtx authctx.TxContext, name, passphrase string, msgs []sdk.Msg) ([]byte, error) {
return txCtx.WithGas(0).BuildAndSign(name, passphrase, msgs)
}

// CalculateGas simulates the execution of a transaction and returns
// both the estimate obtained by the query and the adjusted amount.
func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), cdc *amino.Codec, txBytes []byte, adjustment float64) (estimate, adjusted int64, err error) {
Expand All @@ -109,13 +87,12 @@ func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), cdc *
return
}
adjusted = adjustGasEstimate(estimate, adjustment)
fmt.Fprintf(os.Stderr, "gas: [estimated = %v] [adjusted = %v]\n", estimate, adjusted)
return
}

func adjustGasEstimate(estimate int64, adjustment float64) int64 {
if adjustment == 0 {
return int64(DefaultGasAdjustment * float64(estimate))
adjustment = DefaultGasAdjustment
}
return int64(adjustment * float64(estimate))
}
Expand All @@ -127,3 +104,35 @@ func parseQueryResponse(cdc *amino.Codec, rawRes []byte) (int64, error) {
}
return simulationResult.GasUsed, nil
}

func prepareTxContext(txCtx authctx.TxContext, cliCtx context.CLIContext) (authctx.TxContext, error) {
if err := cliCtx.EnsureAccountExists(); err != nil {
return txCtx, err
}

from, err := cliCtx.GetFromAddress()
if err != nil {
return txCtx, err
}

// TODO: (ref #1903) Allow for user supplied account number without
// automatically doing a manual lookup.
if txCtx.AccountNumber == 0 {
accNum, err := cliCtx.GetAccountNumber(from)
if err != nil {
return txCtx, err
}
txCtx = txCtx.WithAccountNumber(accNum)
}

// TODO: (ref #1903) Allow for user supplied account sequence without
// automatically doing a manual lookup.
if txCtx.Sequence == 0 {
accSeq, err := cliCtx.GetAccountSequence(from)
if err != nil {
return txCtx, err
}
txCtx = txCtx.WithSequence(accSeq)
}
return txCtx, nil
}
17 changes: 16 additions & 1 deletion cmd/gaia/cli_test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ func TestGaiaCLISend(t *testing.T) {
fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags))
require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf("steak").Int64())

// Test --dry-run
success := executeWrite(t, fmt.Sprintf("gaiacli send %v --amount=10steak --to=%s --from=foo --dry-run", flags, barAddr), app.DefaultKeyPass)
require.True(t, success)
// Check state didn't change
fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags))
require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf("steak").Int64())

// test autosequencing
executeWrite(t, fmt.Sprintf("gaiacli send %v --amount=10steak --to=%s --from=foo", flags, barAddr), app.DefaultKeyPass)
tests.WaitForNextNBlocksTM(2, port)
Expand Down Expand Up @@ -148,6 +155,10 @@ func TestGaiaCLICreateValidator(t *testing.T) {

initialPool.BondedTokens = initialPool.BondedTokens.Add(sdk.NewDec(1))

// Test --dry-run
success := executeWrite(t, cvStr+" --dry-run", app.DefaultKeyPass)
require.True(t, success)

executeWrite(t, cvStr, app.DefaultKeyPass)
tests.WaitForNextNBlocksTM(2, port)

Expand All @@ -164,7 +175,7 @@ func TestGaiaCLICreateValidator(t *testing.T) {
unbondStr += fmt.Sprintf(" --validator=%s", barAddr)
unbondStr += fmt.Sprintf(" --shares-amount=%v", "1")

success := executeWrite(t, unbondStr, app.DefaultKeyPass)
success = executeWrite(t, unbondStr, app.DefaultKeyPass)
require.True(t, success)
tests.WaitForNextNBlocksTM(2, port)

Expand Down Expand Up @@ -211,6 +222,10 @@ func TestGaiaCLISubmitProposal(t *testing.T) {
spStr += fmt.Sprintf(" --title=%s", "Test")
spStr += fmt.Sprintf(" --description=%s", "test")

// Test --dry-run
success := executeWrite(t, spStr+" --dry-run", app.DefaultKeyPass)
require.True(t, success)

executeWrite(t, spStr, app.DefaultKeyPass)
tests.WaitForNextNBlocksTM(2, port)

Expand Down
15 changes: 14 additions & 1 deletion docs/sdk/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ When you query an account balance with zero tokens, you will get this error: `No

### Send Tokens

The following command could be used to send coins from one account to another:

```bash
gaiacli send \
--amount=10faucetToken \
Expand All @@ -100,7 +102,7 @@ The `--amount` flag accepts the format `--amount=<value|coin_name>`.
::: tip Note
You may want to cap the maximum gas that can be consumed by the transaction via the `--gas` flag.
If set to 0, the gas limit will be automatically estimated.
Gas estimate might be inaccurate as state changes could occur in between the end of the simulation and the actual execution of a transaction, thus an adjustment is applied on top of the original estimate in order to ensure the transaction is broadcasted successfully. The adjustment can be controlled via the `--gas-adjustment` flag, whose default value is 1.2.
Gas estimate might be inaccurate as state changes could occur in between the end of the simulation and the actual execution of a transaction, thus an adjustment is applied on top of the original estimate in order to ensure the transaction is broadcasted successfully. The adjustment can be controlled via the `--gas-adjustment` flag, whose default value is 1.0.
:::

Now, view the updated balances of the origin and destination accounts:
Expand All @@ -116,6 +118,17 @@ You can also check your balance at a given block by using the `--block` flag:
gaiacli account <account_cosmosaccaddr> --block=<block_height>
```

You can simulate a transaction without actually broadcasting it by appending the `--dry-run` flag to the command line:

```bash
gaiacli send \
--amount=10faucetToken \
--chain-id=<chain_id> \
--name=<key_name> \
--to=<destination_cosmosaccaddr> \
--dry-run
```

### Staking

#### Set up a Validator
Expand Down
Loading

0 comments on commit e460e3e

Please sign in to comment.