From 932948612d96105fb081bdf506ed455b63730aed Mon Sep 17 00:00:00 2001 From: Justin Tieri <37750742+jtieri@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:08:30 -0600 Subject: [PATCH] feat: implement support for Osmosis EIP-1559 Add support for querying the dynamic gas price base fee on Osmosis. --- .../chains/cosmos/cosmos_chain_processor.go | 13 ++- relayer/chains/cosmos/fee_market.go | 79 +++++++++++++++++++ relayer/chains/cosmos/fee_market_test.go | 61 ++++++++++++++ relayer/chains/cosmos/grpc_query.go | 15 +++- relayer/chains/cosmos/provider.go | 6 +- relayer/chains/cosmos/tx.go | 56 ++++++++++--- 6 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 relayer/chains/cosmos/fee_market.go create mode 100644 relayer/chains/cosmos/fee_market_test.go diff --git a/relayer/chains/cosmos/cosmos_chain_processor.go b/relayer/chains/cosmos/cosmos_chain_processor.go index ea405e8e9..a88cf4f13 100644 --- a/relayer/chains/cosmos/cosmos_chain_processor.go +++ b/relayer/chains/cosmos/cosmos_chain_processor.go @@ -549,8 +549,15 @@ func (ccp *CosmosChainProcessor) CurrentBlockHeight(ctx context.Context, persist func (ccp *CosmosChainProcessor) CurrentRelayerBalance(ctx context.Context) { // memoize the current gas prices to only show metrics for "interesting" denoms + gasPrice := ccp.chainProvider.PCfg.GasPrices + if ccp.parsedGasPrices == nil { - gp, err := sdk.ParseDecCoins(ccp.chainProvider.PCfg.GasPrices) + dynamicFee := ccp.chainProvider.DynamicFee(ctx) + if dynamicFee != "" { + gasPrice = dynamicFee + } + + gp, err := sdk.ParseDecCoins(gasPrice) if err != nil { ccp.log.Error( "Failed to parse gas prices", @@ -575,11 +582,13 @@ func (ccp *CosmosChainProcessor) CurrentRelayerBalance(ctx context.Context) { zap.Error(err), ) } + // Print the relevant gas prices for _, gasDenom := range *ccp.parsedGasPrices { bal := relayerWalletBalances.AmountOf(gasDenom.Denom) + // Convert to a big float to get a float64 for metrics f, _ := big.NewFloat(0.0).SetInt(bal.BigInt()).Float64() - ccp.metrics.SetWalletBalance(ccp.chainProvider.ChainId(), ccp.chainProvider.PCfg.GasPrices, ccp.chainProvider.Key(), address, gasDenom.Denom, f) + ccp.metrics.SetWalletBalance(ccp.chainProvider.ChainId(), gasPrice, ccp.chainProvider.Key(), address, gasDenom.Denom, f) } } diff --git a/relayer/chains/cosmos/fee_market.go b/relayer/chains/cosmos/fee_market.go new file mode 100644 index 000000000..bc4cb85f1 --- /dev/null +++ b/relayer/chains/cosmos/fee_market.go @@ -0,0 +1,79 @@ +package cosmos + +import ( + "context" + "fmt" + "regexp" + "strings" + + sdkmath "cosmossdk.io/math" + "go.uber.org/zap" +) + +const queryPath = "/osmosis.txfees.v1beta1.Query/GetEipBaseFee" + +// DynamicFee queries the dynamic gas price base fee and returns a string with the base fee and token denom concatenated. +// If the chain does not have dynamic fees enabled in the config, nothing happens and an empty string is always returned. +func (cc *CosmosProvider) DynamicFee(ctx context.Context) string { + if !cc.PCfg.DynamicGasPrice { + return "" + } + + dynamicFee, err := cc.QueryBaseFee(ctx) + if err != nil { + // If there was an error querying the dynamic base fee, do nothing and fall back to configured gas price. + cc.log.Warn("Failed to query the dynamic gas price base fee", zap.Error(err)) + return "" + } + + return dynamicFee +} + +// QueryBaseFee attempts to make an ABCI query to retrieve the base fee on chains using the Osmosis EIP-1559 implementation. +// This is currently hardcoded to only work on Osmosis. +func (cc *CosmosProvider) QueryBaseFee(ctx context.Context) (string, error) { + resp, err := cc.RPCClient.ABCIQuery(ctx, queryPath, nil) + if err != nil || resp.Response.Code != 0 { + return "", err + } + + // The response value contains the data link escape control character which must be removed before parsing. + cleanedString := strings.ReplaceAll(strings.TrimSpace(string(resp.Response.Value)), "\u0010", "") + + decFee, err := sdkmath.LegacyNewDecFromStr(cleanedString) + if err != nil { + return "", err + } + + baseFee, err := decFee.Float64() + if err != nil { + return "", err + } + + // The current EIP-1559 implementation returns an integer and does not return any value that tells us how many + // decimal places we need to account for. + // + // This may be problematic because we are assuming that we always need to move the decimal 18 places. + fee := baseFee / 1e18 + + denom, err := parseTokenDenom(cc.PCfg.GasPrices) + if err != nil { + return "", err + } + + return fmt.Sprintf("%f%s", fee, denom), nil +} + +// parseTokenDenom takes a string in the format numericGasPrice + tokenDenom (e.g. 0.0025uosmo), +// and parses the tokenDenom portion (e.g. uosmo) before returning just the token denom. +func parseTokenDenom(gasPrice string) (string, error) { + regex := regexp.MustCompile(`^0\.\d+([a-zA-Z]+)$`) + + matches := regex.FindStringSubmatch(gasPrice) + + if len(matches) != 2 { + return "", fmt.Errorf("failed to parse token denom from string %s", gasPrice) + } + + return matches[1], nil +} diff --git a/relayer/chains/cosmos/fee_market_test.go b/relayer/chains/cosmos/fee_market_test.go new file mode 100644 index 000000000..746d31ed9 --- /dev/null +++ b/relayer/chains/cosmos/fee_market_test.go @@ -0,0 +1,61 @@ +package cosmos + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + coinType = 118 + + testCfg = CosmosProviderConfig{ + KeyDirectory: "", + Key: "default", + ChainName: "osmosis", + ChainID: "osmosis-1", + RPCAddr: "https://rpc.osmosis.strange.love:443", + AccountPrefix: "osmo", + KeyringBackend: "test", + DynamicGasPrice: true, + GasAdjustment: 1.2, + GasPrices: "0.0025uosmo", + MinGasAmount: 1, + MaxGasAmount: 0, + Debug: false, + Timeout: "30s", + BlockTimeout: "30s", + OutputFormat: "json", + SignModeStr: "direct", + ExtraCodecs: nil, + Modules: nil, + Slip44: &coinType, + SigningAlgorithm: "", + Broadcast: "batch", + MinLoopDuration: 0, + ExtensionOptions: nil, + FeeGrants: nil, + } +) + +func TestQueryBaseFee(t *testing.T) { + p, err := testCfg.NewProvider(nil, t.TempDir(), true, testCfg.ChainName) + require.NoError(t, err) + + ctx := context.Background() + err = p.Init(ctx) + require.NoError(t, err) + + cp := p.(*CosmosProvider) + + baseFee, err := cp.QueryBaseFee(ctx) + require.NoError(t, err) + require.NotEqual(t, "", baseFee) +} + +func TestParseDenom(t *testing.T) { + denom, err := parseTokenDenom(testCfg.GasPrices) + require.NoError(t, err) + require.Equal(t, "uosmo", denom) +} diff --git a/relayer/chains/cosmos/grpc_query.go b/relayer/chains/cosmos/grpc_query.go index cf70a17f4..1582b96ee 100644 --- a/relayer/chains/cosmos/grpc_query.go +++ b/relayer/chains/cosmos/grpc_query.go @@ -178,7 +178,20 @@ func (cc *CosmosProvider) TxServiceBroadcast(ctx context.Context, req *tx.Broadc wg.Add(1) - if err := cc.broadcastTx(ctx, req.TxBytes, nil, nil, ctx, blockTimeout, []func(*provider.RelayerTxResponse, error){callback}); err != nil { + dynamicFee := cc.DynamicFee(ctx) + + err = cc.broadcastTx( + ctx, + req.TxBytes, + nil, + nil, + ctx, + blockTimeout, + []func(*provider.RelayerTxResponse, error){callback}, + dynamicFee, + ) + + if err != nil { return nil, err } diff --git a/relayer/chains/cosmos/provider.go b/relayer/chains/cosmos/provider.go index ab6f635ff..f34b1125d 100644 --- a/relayer/chains/cosmos/provider.go +++ b/relayer/chains/cosmos/provider.go @@ -32,11 +32,6 @@ var ( _ provider.ProviderConfig = &CosmosProviderConfig{} ) -const ( - cometEncodingThreshold = "v0.37.0-alpha" - cometBlockResultsThreshold = "v0.38.0-alpha" -) - type CosmosProviderConfig struct { KeyDirectory string `json:"key-directory" yaml:"key-directory"` Key string `json:"key" yaml:"key"` @@ -45,6 +40,7 @@ type CosmosProviderConfig struct { RPCAddr string `json:"rpc-addr" yaml:"rpc-addr"` AccountPrefix string `json:"account-prefix" yaml:"account-prefix"` KeyringBackend string `json:"keyring-backend" yaml:"keyring-backend"` + DynamicGasPrice bool `json:"dynamic-gas-price" yaml:"dynamic-gas-price"` GasAdjustment float64 `json:"gas-adjustment" yaml:"gas-adjustment"` GasPrices string `json:"gas-prices" yaml:"gas-prices"` MinGasAmount uint64 `json:"min-gas-amount" yaml:"min-gas-amount"` diff --git a/relayer/chains/cosmos/tx.go b/relayer/chains/cosmos/tx.go index 156301ba0..89d76e033 100644 --- a/relayer/chains/cosmos/tx.go +++ b/relayer/chains/cosmos/tx.go @@ -172,7 +172,19 @@ func (cc *CosmosProvider) SendMessagesToMempool( sequenceGuard.Mu.Lock() defer sequenceGuard.Mu.Unlock() - txBytes, sequence, fees, err := cc.buildMessages(ctx, msgs, memo, 0, txSignerKey, feegranterKeyOrAddr, sequenceGuard) + dynamicFee := cc.DynamicFee(ctx) + + txBytes, sequence, fees, err := cc.buildMessages( + ctx, + msgs, + memo, + 0, + txSignerKey, + feegranterKeyOrAddr, + sequenceGuard, + dynamicFee, + ) + if err != nil { // Account sequence mismatch errors can happen on the simulated transaction also. if strings.Contains(err.Error(), legacyerrors.ErrWrongSequence.Error()) { @@ -182,7 +194,18 @@ func (cc *CosmosProvider) SendMessagesToMempool( return err } - if err := cc.broadcastTx(ctx, txBytes, msgs, fees, asyncCtx, defaultBroadcastWaitTimeout, asyncCallbacks); err != nil { + err = cc.broadcastTx( + ctx, + txBytes, + msgs, + fees, + asyncCtx, + defaultBroadcastWaitTimeout, + asyncCallbacks, + dynamicFee, + ) + + if err != nil { if strings.Contains(err.Error(), legacyerrors.ErrWrongSequence.Error()) { cc.handleAccountSequenceMismatchError(sequenceGuard, err) } @@ -253,7 +276,9 @@ func (cc *CosmosProvider) SendMsgsWith(ctx context.Context, msgs []sdk.Msg, memo rand.Seed(time.Now().UnixNano()) feegrantKeyAcc, _ := cc.GetKeyAddressForKey(feegranterKey) - txf, err := cc.PrepareFactory(cc.TxFactory(), signingKey) + dynamicFee := cc.DynamicFee(ctx) + + txf, err := cc.PrepareFactory(cc.TxFactory(dynamicFee), signingKey) if err != nil { return nil, err } @@ -359,6 +384,7 @@ func (cc *CosmosProvider) broadcastTx( asyncCtx context.Context, // context for async wait for block inclusion after successful tx broadcast asyncTimeout time.Duration, // timeout for waiting for block inclusion asyncCallbacks []func(*provider.RelayerTxResponse, error), // callback for success/fail of the wait for block inclusion + dynamicFee string, ) error { res, err := cc.RPCClient.BroadcastTxSync(ctx, tx) isErr := err != nil @@ -389,7 +415,7 @@ func (cc *CosmosProvider) broadcastTx( return fmt.Errorf("failed to get relayer bech32 wallet address: %w", err) } - cc.UpdateFeesSpent(cc.ChainId(), cc.Key(), address, fees) + cc.UpdateFeesSpent(cc.ChainId(), cc.Key(), address, fees, dynamicFee) // TODO: maybe we need to check if the node has tx indexing enabled? // if not, we need to find a new way to block until inclusion in a block @@ -596,6 +622,7 @@ func (cc *CosmosProvider) buildMessages( txSignerKey string, feegranterKeyOrAddr string, sequenceGuard *WalletState, + dynamicFee string, ) ( txBytes []byte, sequence uint64, @@ -607,7 +634,7 @@ func (cc *CosmosProvider) buildMessages( cMsgs := CosmosMsgs(msgs...) - txf, err := cc.PrepareFactory(cc.TxFactory(), txSignerKey) + txf, err := cc.PrepareFactory(cc.TxFactory(dynamicFee), txSignerKey) if err != nil { return nil, 0, sdk.Coins{}, err } @@ -1594,7 +1621,7 @@ func (cc *CosmosProvider) NewClientState( }, nil } -func (cc *CosmosProvider) UpdateFeesSpent(chain, key, address string, fees sdk.Coins) { +func (cc *CosmosProvider) UpdateFeesSpent(chain, key, address string, fees sdk.Coins, dynamicFee string) { // Don't set the metrics in testing if cc.metrics == nil { return @@ -1604,10 +1631,15 @@ func (cc *CosmosProvider) UpdateFeesSpent(chain, key, address string, fees sdk.C cc.TotalFees = cc.TotalFees.Add(fees...) cc.totalFeesMu.Unlock() + gasPrice := cc.PCfg.GasPrices + if dynamicFee != "" { + gasPrice = dynamicFee + } + for _, fee := range cc.TotalFees { // Convert to a big float to get a float64 for metrics f, _ := big.NewFloat(0.0).SetInt(fee.Amount.BigInt()).Float64() - cc.metrics.SetFeesSpent(chain, cc.PCfg.GasPrices, key, address, fee.GetDenom(), f) + cc.metrics.SetFeesSpent(chain, gasPrice, key, address, fee.GetDenom(), f) } } @@ -1780,13 +1812,19 @@ func (cc *CosmosProvider) CalculateGas(ctx context.Context, txf tx.Factory, sign } // TxFactory instantiates a new tx factory with the appropriate configuration settings for this chain. -func (cc *CosmosProvider) TxFactory() tx.Factory { +func (cc *CosmosProvider) TxFactory(dynamicFee string) tx.Factory { + gasPrice := cc.PCfg.GasPrices + + if dynamicFee != "" { + gasPrice = dynamicFee + } + return tx.Factory{}. WithAccountRetriever(cc). WithChainID(cc.PCfg.ChainID). WithTxConfig(cc.Cdc.TxConfig). WithGasAdjustment(cc.PCfg.GasAdjustment). - WithGasPrices(cc.PCfg.GasPrices). + WithGasPrices(gasPrice). WithKeybase(cc.Keybase). WithSignMode(cc.PCfg.SignMode()) }