diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc2b0cc6..409029d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#10962](https://github.com/cosmos/cosmos-sdk/pull/10962) ADR-040: Add state migration from iavl (v1Store) to smt (v2Store) * (types) [\#10948](https://github.com/cosmos/cosmos-sdk/issues/10948) Add `app-db-backend` to the `app.toml` config to replace the compile-time `types.DBbackend` variable. * (authz)[\#11060](https://github.com/cosmos/cosmos-sdk/pull/11060) Support grant with no expire time. +* (rosetta) [\#11590](https://github.com/cosmos/cosmos-sdk/pull/11590) Add fee suggestion for rosetta and enable offline mode. Also force set events about Fees to Success to pass reconciliation test. ### API Breaking Changes @@ -266,6 +267,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (cli) [\#11337](https://github.com/cosmos/cosmos-sdk/pull/11337) Fixes `show-adress` cli cmd * (crypto) [\#11298](https://github.com/cosmos/cosmos-sdk/pull/11298) Fix cgo secp signature verification and update libscep256k1 library. * (x/authz) [\#11512](https://github.com/cosmos/cosmos-sdk/pull/11512) Fix response of a panic to error, when subtracting balances. +* (rosetta) [\#11590](https://github.com/cosmos/cosmos-sdk/pull/11590) `/block` returns an error with nil pointer when a request has both of index and hash and increase timeout for huge genesis. * (x/feegrant) [\#11813](https://github.com/cosmos/cosmos-sdk/pull/11813) Fix pagination total count in `AllowancesByGranter` query. ### State Machine Breaking diff --git a/server/config/config.go b/server/config/config.go index 2329018f8..103244218 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/viper" + clientflags "github.com/cosmos/cosmos-sdk/client/flags" pruningtypes "github.com/cosmos/cosmos-sdk/pruning/types" "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" @@ -131,6 +132,15 @@ type RosettaConfig struct { // Offline defines if the server must be run in offline mode Offline bool `mapstructure:"offline"` + + // EnableFeeSuggestion defines if the server should suggest fee by default + EnableFeeSuggestion bool `mapstructure:"enable-fee-suggestion"` + + // GasToSuggest defines gas limit when calculating the fee + GasToSuggest int `mapstructure:"gas-to-suggest"` + + // DenomToSuggest defines the defult denom for fee suggestion + DenomToSuggest string `mapstructure:"denom-to-suggest"` } // GRPCConfig defines configuration for the gRPC server. @@ -236,12 +246,15 @@ func DefaultConfig() *Config { Address: DefaultGRPCAddress, }, Rosetta: RosettaConfig{ - Enable: false, - Address: ":8080", - Blockchain: "app", - Network: "network", - Retries: 3, - Offline: false, + Enable: false, + Address: ":8080", + Blockchain: "app", + Network: "network", + Retries: 3, + Offline: false, + EnableFeeSuggestion: false, + GasToSuggest: clientflags.DefaultGasLimit, + DenomToSuggest: "uatom", }, GRPCWeb: GRPCWebConfig{ Enable: true, @@ -299,12 +312,15 @@ func GetConfig(v *viper.Viper) Config { EnableUnsafeCORS: v.GetBool("api.enabled-unsafe-cors"), }, Rosetta: RosettaConfig{ - Enable: v.GetBool("rosetta.enable"), - Address: v.GetString("rosetta.address"), - Blockchain: v.GetString("rosetta.blockchain"), - Network: v.GetString("rosetta.network"), - Retries: v.GetInt("rosetta.retries"), - Offline: v.GetBool("rosetta.offline"), + Enable: v.GetBool("rosetta.enable"), + Address: v.GetString("rosetta.address"), + Blockchain: v.GetString("rosetta.blockchain"), + Network: v.GetString("rosetta.network"), + Retries: v.GetInt("rosetta.retries"), + Offline: v.GetBool("rosetta.offline"), + EnableFeeSuggestion: v.GetBool("rosetta.enable-fee-suggestion"), + GasToSuggest: v.GetInt("rosetta.gas-to-suggest"), + DenomToSuggest: v.GetString("rosetta.denom-to-suggest"), }, GRPC: GRPCConfig{ Enable: v.GetBool("grpc.enable"), diff --git a/server/config/toml.go b/server/config/toml.go index a9664db55..e3e92fcf1 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -170,6 +170,18 @@ retries = {{ .Rosetta.Retries }} # Offline defines if Rosetta server should run in offline mode. offline = {{ .Rosetta.Offline }} +# EnableDefaultSuggestedFee defines if the server should suggest fee by default. +# If 'construction/medata' is called without gas limit and gas price, +# suggested fee based on gas-to-suggest and denom-to-suggest will be given. +enable-fee-suggestion = {{ .Rosetta.EnableFeeSuggestion }} + +# GasToSuggest defines gas limit when calculating the fee +gas-to-suggest = {{ .Rosetta.GasToSuggest }} + +# DenomToSuggest defines the defult denom for fee suggestion. +# Price must be in minimum-gas-prices. +denom-to-suggest = "{{ .Rosetta.DenomToSuggest }}" + ############################################################################### ### gRPC Configuration ### ############################################################################### diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index dc46e187e..010e9eeee 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -11,27 +11,32 @@ import ( "strconv" "time" - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" + "github.com/cosmos/cosmos-sdk/version" + abcitypes "github.com/tendermint/tendermint/abci/types" - tmrpc "github.com/tendermint/tendermint/rpc/client" + + rosettatypes "github.com/coinbase/rosetta-sdk-go/types" + "google.golang.org/grpc/metadata" + "github.com/tendermint/tendermint/rpc/client/http" "google.golang.org/grpc" - "google.golang.org/grpc/metadata" crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types" + sdk "github.com/cosmos/cosmos-sdk/types" grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" - "github.com/cosmos/cosmos-sdk/version" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" auth "github.com/cosmos/cosmos-sdk/x/auth/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" + + tmrpc "github.com/tendermint/tendermint/rpc/client" ) // interface assertion var _ crgtypes.Client = (*Client)(nil) -const defaultNodeTimeout = 15 * time.Second +const defaultNodeTimeout = time.Minute // Client implements a single network client to interact with cosmos based chains type Client struct { @@ -121,6 +126,14 @@ func (c *Client) Ready() error { if err != nil { return err } + + // to prevent timeout of reading genesis block + var height int64 = -1 + _, err = c.BlockByHeight(ctx, &height) + if err != nil { + return err + } + _, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{}) if err != nil { return err @@ -395,6 +408,28 @@ func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options ma return nil, err } + // if default fees suggestion is enabled and gas limit or price is unset, use default + if c.config.EnableFeeSuggestion { + if constructionOptions.GasLimit <= 0 { + constructionOptions.GasLimit = uint64(c.config.GasToSuggest) + } + if constructionOptions.GasPrice == "" { + denom := c.config.DenomToSuggest + constructionOptions.GasPrice = c.config.SuggestPrices.AmountOf(denom).String() + denom + } + } + + if constructionOptions.GasLimit > 0 && constructionOptions.GasPrice != "" { + gasPrice, err := sdk.ParseDecCoin(constructionOptions.GasPrice) + if err != nil { + return nil, err + } + // check gasPrice is in the list + if !c.config.SuggestPrices.AmountOf(gasPrice.Denom).IsPositive() { + return nil, crgerrs.ErrBadArgument + } + } + signersData := make([]*SignerData, len(constructionOptions.ExpectedSigners)) for i, signer := range constructionOptions.ExpectedSigners { diff --git a/server/rosetta/config.go b/server/rosetta/config.go index dac86e25d..a6dc905d1 100644 --- a/server/rosetta/config.go +++ b/server/rosetta/config.go @@ -10,8 +10,10 @@ import ( crg "github.com/cosmos/cosmos-sdk/server/rosetta/lib/server" + clientflags "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) // configuration defaults constants @@ -30,17 +32,27 @@ const ( DefaultNetwork = "network" // DefaultOffline defines the default offline value DefaultOffline = false + // DefaultEnableFeeSuggestion indicates to use fee suggestion if `construction/metadata` is called without gas limit and price + DefaultEnableFeeSuggestion = false + // DenomToSuggest defines the default denom for fee suggestion + DenomToSuggest = "uatom" + // DefaultPrices defines the default list of prices to suggest + DefaultPrices = "0.0uatom" ) // configuration flags const ( - FlagBlockchain = "blockchain" - FlagNetwork = "network" - FlagTendermintEndpoint = "tendermint" - FlagGRPCEndpoint = "grpc" - FlagAddr = "addr" - FlagRetries = "retries" - FlagOffline = "offline" + FlagBlockchain = "blockchain" + FlagNetwork = "network" + FlagTendermintEndpoint = "tendermint" + FlagGRPCEndpoint = "grpc" + FlagAddr = "addr" + FlagRetries = "retries" + FlagOffline = "offline" + FlagEnableFeeSuggestion = "enable-fee-suggestion" + FlagGasToSuggest = "gas-to-suggest" + FlagDenomToSuggest = "denom-to-suggest" + FlagPricesToSuggest = "prices-to-suggest" ) // Config defines the configuration of the rosetta server @@ -65,6 +77,14 @@ type Config struct { Retries int // Offline defines if the server must be run in offline mode Offline bool + // EnableFeeSuggestion indicates to use fee suggestion when `construction/metadata` is called without gas limit and price + EnableFeeSuggestion bool + // GasToSuggest defines the gas limit for fee suggestion + GasToSuggest int + // DenomToSuggest defines the default denom for fee suggestion + DenomToSuggest string + // SuggestPrices defines the gas prices for fee suggestion + SuggestPrices sdk.DecCoins // Codec overrides the default data and construction api client codecs Codec *codec.ProtoCodec // InterfaceRegistry overrides the default data and construction api interface registry @@ -99,8 +119,18 @@ func (c *Config) validate() error { if c.Network == "" { return fmt.Errorf("network not provided") } - if c.Offline { - return fmt.Errorf("offline mode is not supported for stargate implementation due to how sigv2 works") + if c.GasToSuggest <= 0 { + c.GasToSuggest = clientflags.DefaultGasLimit + } + found := false + for i := 0; i < c.SuggestPrices.Len(); i++ { + if c.SuggestPrices.GetDenomByIndex(i) == c.DenomToSuggest { + found = true + break + } + } + if !found { + return fmt.Errorf("default suggest denom is not found in minimum-gas-prices") } // these are optional but it must be online @@ -153,14 +183,39 @@ func FromFlags(flags *pflag.FlagSet) (*Config, error) { if err != nil { return nil, err } + enableDefaultFeeSuggestion, err := flags.GetBool(FlagEnableFeeSuggestion) + if err != nil { + return nil, err + } + suggestGas, err := flags.GetInt(FlagGasToSuggest) + if err != nil { + return nil, err + } + suggestDenom, err := flags.GetString(FlagDenomToSuggest) + if err != nil { + return nil, err + } + suggestPrices, err := flags.GetString(FlagPricesToSuggest) + if err != nil { + return nil, err + } + prices, err := sdk.ParseDecCoins(suggestPrices) + if err != nil { + return nil, err + } + conf := &Config{ - Blockchain: blockchain, - Network: network, - TendermintRPC: tendermintRPC, - GRPCEndpoint: gRPCEndpoint, - Addr: addr, - Retries: retries, - Offline: offline, + Blockchain: blockchain, + Network: network, + TendermintRPC: tendermintRPC, + GRPCEndpoint: gRPCEndpoint, + Addr: addr, + Retries: retries, + Offline: offline, + EnableFeeSuggestion: enableDefaultFeeSuggestion, + GasToSuggest: suggestGas, + DenomToSuggest: suggestDenom, + SuggestPrices: prices, } err = conf.validate() if err != nil { @@ -201,4 +256,8 @@ func SetFlags(flags *pflag.FlagSet) { flags.String(FlagAddr, DefaultAddr, "the address rosetta will bind to") flags.Int(FlagRetries, DefaultRetries, "the number of retries that will be done before quitting") flags.Bool(FlagOffline, DefaultOffline, "run rosetta only with construction API") + flags.Bool(FlagEnableFeeSuggestion, DefaultEnableFeeSuggestion, "enable default fee suggestion") + flags.Int(FlagGasToSuggest, clientflags.DefaultGasLimit, "default gas for fee suggestion") + flags.String(FlagDenomToSuggest, DenomToSuggest, "default denom for fee suggestion") + flags.String(FlagPricesToSuggest, DefaultPrices, "default prices for fee suggestion") } diff --git a/server/rosetta/converter.go b/server/rosetta/converter.go index 6782073aa..9c0e6a715 100644 --- a/server/rosetta/converter.go +++ b/server/rosetta/converter.go @@ -297,7 +297,7 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose var balanceOps []*rosettatypes.Operation // tx result might be nil, in case we're querying an unconfirmed tx from the mempool if txResult != nil { - balanceOps = c.BalanceOps(status, txResult.Events) + balanceOps = c.BalanceOps(StatusTxSuccess, txResult.Events) // force set to success because no events for failed tx } // now normalize indexes diff --git a/server/rosetta/lib/internal/service/construction.go b/server/rosetta/lib/internal/service/construction.go index 4be6e9461..45c51df2a 100644 --- a/server/rosetta/lib/internal/service/construction.go +++ b/server/rosetta/lib/internal/service/construction.go @@ -4,8 +4,11 @@ import ( "context" "crypto/sha256" "encoding/hex" + "strconv" "strings" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/coinbase/rosetta-sdk-go/types" "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" @@ -69,9 +72,43 @@ func (on OnlineNetwork) ConstructionMetadata(ctx context.Context, request *types return nil, errors.ToRosetta(err) } - return &types.ConstructionMetadataResponse{ + response := &types.ConstructionMetadataResponse{ Metadata: metadata, - }, nil + } + + if metadata["gas_price"] != nil && metadata["gas_limit"] != nil { + gasPrice, ok := metadata["gas_price"].(string) + if !ok { + return nil, errors.ToRosetta(errors.WrapError(errors.ErrBadArgument, "invalid gas_price")) + } + if gasPrice == "" { // gas_price is unset. skip fee suggestion + return response, nil + } + price, err := sdk.ParseDecCoin(gasPrice) + if err != nil { + return nil, errors.ToRosetta(err) + } + + gasLimit, ok := metadata["gas_limit"].(float64) + if !ok { + return nil, errors.ToRosetta(errors.WrapError(errors.ErrBadArgument, "invalid gas_limit")) + } + if gasLimit == 0 { // gas_limit is unset. skip fee suggestion + return response, nil + } + gas := sdk.NewIntFromUint64(uint64(gasLimit)) + + suggestedFee := types.Amount{ + Value: strconv.FormatInt(price.Amount.MulInt64(gas.Int64()).Ceil().TruncateInt64(), 10), + Currency: &(types.Currency{ + Symbol: price.Denom, + Decimals: 0, + }), + } + response.SuggestedFee = []*types.Amount{&suggestedFee} + } + + return response, nil } // ConstructionParse Parse is called on both unsigned and signed transactions to understand the diff --git a/server/rosetta/lib/internal/service/data.go b/server/rosetta/lib/internal/service/data.go index cda420487..c814ff4c7 100644 --- a/server/rosetta/lib/internal/service/data.go +++ b/server/rosetta/lib/internal/service/data.go @@ -4,7 +4,6 @@ import ( "context" "github.com/coinbase/rosetta-sdk-go/types" - "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types" ) @@ -56,9 +55,14 @@ func (on OnlineNetwork) Block(ctx context.Context, request *types.BlockRequest) blockResponse crgtypes.BlockTransactionsResponse err error ) - // block identifier is assumed not to be nil as rosetta will do this check for us - // check if we have to query via hash or block number + + // When fetching data by BlockIdentifier, it may be possible to only specify the index or hash. + // If neither property is specified, it is assumed that the client is making a request at the current block. switch { + case request.BlockIdentifier == nil: // unlike AccountBalance(), BlockIdentifer is mandatory by spec 1.4.10. + err := errors.WrapError(errors.ErrBadArgument, "block identifier needs to be specified") + return nil, errors.ToRosetta(err) + case request.BlockIdentifier.Hash != nil: blockResponse, err = on.client.BlockTransactionsByHash(ctx, *request.BlockIdentifier.Hash) if err != nil { @@ -69,8 +73,23 @@ func (on OnlineNetwork) Block(ctx context.Context, request *types.BlockRequest) if err != nil { return nil, errors.ToRosetta(err) } + default: - err := errors.WrapError(errors.ErrBadArgument, "at least one of hash or index needs to be specified") + // both empty + blockResponse, err = on.client.BlockTransactionsByHeight(ctx, nil) + if err != nil { + return nil, errors.ToRosetta(err) + } + } + + // Both of index and hash can be specified in reuqest, so make sure they are not mismatching. + if request.BlockIdentifier.Index != nil && *request.BlockIdentifier.Index != blockResponse.Block.Index { + err := errors.WrapError(errors.ErrBadArgument, "mismatching index") + return nil, errors.ToRosetta(err) + } + + if request.BlockIdentifier.Hash != nil && *request.BlockIdentifier.Hash != blockResponse.Block.Hash { + err := errors.WrapError(errors.ErrBadArgument, "mismatching hash") return nil, errors.ToRosetta(err) } diff --git a/server/start.go b/server/start.go index 63ed31fe8..ae2c88451 100644 --- a/server/start.go +++ b/server/start.go @@ -32,6 +32,7 @@ import ( "github.com/cosmos/cosmos-sdk/server/rosetta" crgserver "github.com/cosmos/cosmos-sdk/server/rosetta/lib/server" "github.com/cosmos/cosmos-sdk/server/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" ) const ( @@ -403,16 +404,25 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App offlineMode = true } + minGasPrices, err := sdktypes.ParseDecCoins(config.MinGasPrices) + if err != nil { + ctx.Logger.Error("failed to parse minimum-gas-prices: ", err) + return err + } + conf := &rosetta.Config{ - Blockchain: config.Rosetta.Blockchain, - Network: config.Rosetta.Network, - TendermintRPC: ctx.Config.RPC.ListenAddress, - GRPCEndpoint: config.GRPC.Address, - Addr: config.Rosetta.Address, - Retries: config.Rosetta.Retries, - Offline: offlineMode, - Codec: clientCtx.Codec.(*codec.ProtoCodec), - InterfaceRegistry: clientCtx.InterfaceRegistry, + Blockchain: config.Rosetta.Blockchain, + Network: config.Rosetta.Network, + TendermintRPC: ctx.Config.RPC.ListenAddress, + GRPCEndpoint: config.GRPC.Address, + Addr: config.Rosetta.Address, + Retries: config.Rosetta.Retries, + Offline: offlineMode, + GasToSuggest: config.Rosetta.GasToSuggest, + EnableFeeSuggestion: config.Rosetta.EnableFeeSuggestion, + SuggestPrices: minGasPrices.Sort(), + Codec: clientCtx.Codec.(*codec.ProtoCodec), + InterfaceRegistry: clientCtx.InterfaceRegistry, } rosettaSrv, err = rosetta.ServerFromConfig(conf)