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 a simulate-only CLI flag/field for REST endpoints #2181

Merged
merged 5 commits into from
Aug 31, 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
2 changes: 2 additions & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ 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
Expand All @@ -48,6 +49,7 @@ FEATURES
provide desired Bech32 prefix encoding
* [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
* [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address`
Expand Down
8 changes: 8 additions & 0 deletions client/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type CLIContext struct {
JSON bool
PrintResponse bool
Certifier tmlite.Certifier
DryRun bool
}

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

Expand Down Expand Up @@ -159,3 +161,9 @@ func (ctx CLIContext) WithCertifier(certifier tmlite.Certifier) CLIContext {
ctx.Certifier = certifier
return ctx
}

// WithGasAdjustment returns a copy of the context with an updated GasAdjustment flag.
func (ctx CLIContext) WithGasAdjustment(adjustment float64) CLIContext {
ctx.GasAdjustment = adjustment
return ctx
}
9 changes: 7 additions & 2 deletions client/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import "github.com/spf13/cobra"

// nolint
const (
// 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.
DefaultGasAdjustment = 1.0
DefaultGasLimit = 200000
DefaultGasAdjustment = 1.2

FlagUseLedger = "ledger"
FlagChainID = "chain-id"
Expand All @@ -23,6 +26,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,11 +58,12 @@ 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(FlagTrustNode, true, "Don't verify proofs for query responses")
c.Flags().Bool(FlagDryRun, false, "ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it")
}
return cmds
}
33 changes: 25 additions & 8 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,21 @@ 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, 0, "")
require.Equal(t, http.StatusInternalServerError, res.StatusCode, body)

// test success with just enough gas
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 3000)
// test failure with wrong adjustment
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 0, 0.1, "")
require.Equal(t, http.StatusInternalServerError, res.StatusCode, body)

// run simulation and test success with estimated gas
res, body, _ = doSendWithGas(t, port, seed, name, password, addr, 0, 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, 0, "")
require.Equal(t, http.StatusOK, res.StatusCode, body)
}

Expand Down Expand Up @@ -720,7 +731,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, gasAdjustment float64, queryStr string) (res *http.Response, body string, receiveAddr sdk.AccAddress) {

// create receive address
kb := client.MockKeyBase()
Expand All @@ -744,22 +755,28 @@ func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.Acc
"gas":"%v",
`, gas)
}
gasAdjustmentStr := ""
if gasAdjustment > 0 {
gasStr = fmt.Sprintf(`
"gas_adjustment":"%v",
`, gasAdjustment)
}
jsonStr := []byte(fmt.Sprintf(`{
%v
%v%v
"name":"%s",
"password":"%s",
"account_number":"%d",
"sequence":"%d",
"amount":[%s],
"chain_id":"%s"
}`, gasStr, name, password, accnum, sequence, coinbz, chainID))
}`, gasStr, gasAdjustmentStr, 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, 0, "")
require.Equal(t, http.StatusOK, res.StatusCode, body)

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

import (
"fmt"
"net/http"
"strconv"
)

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))
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"
}

// ParseFloat64OrReturnBadRequest converts s to a float64 value. It returns a default
// value if the string is empty. Write
func ParseFloat64OrReturnBadRequest(w http.ResponseWriter, s string, defaultIfEmpty float64) (n float64, ok bool) {
if len(s) == 0 {
return defaultIfEmpty, true
}
n, err := strconv.ParseFloat(s, 64)
if err != nil {
WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return n, false
}
return n, true
}
101 changes: 51 additions & 50 deletions client/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,33 @@ import (
"github.com/tendermint/tendermint/libs/common"
)

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

// 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 +48,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,14 +82,10 @@ 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))
}
return int64(adjustment * float64(estimate))
}

Expand All @@ -127,3 +96,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
}
Loading