Skip to content

Commit

Permalink
feat: implement support for Osmosis EIP-1559 (#1412)
Browse files Browse the repository at this point in the history
Add support for querying the dynamic gas price base fee on Osmosis.
  • Loading branch information
jtieri authored Mar 6, 2024
1 parent 250cb55 commit 6c29f16
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 17 deletions.
13 changes: 11 additions & 2 deletions relayer/chains/cosmos/cosmos_chain_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
}
}
79 changes: 79 additions & 0 deletions relayer/chains/cosmos/fee_market.go
Original file line number Diff line number Diff line change
@@ -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
}
61 changes: 61 additions & 0 deletions relayer/chains/cosmos/fee_market_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
15 changes: 14 additions & 1 deletion relayer/chains/cosmos/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 1 addition & 5 deletions relayer/chains/cosmos/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down
56 changes: 47 additions & 9 deletions relayer/chains/cosmos/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -596,6 +622,7 @@ func (cc *CosmosProvider) buildMessages(
txSignerKey string,
feegranterKeyOrAddr string,
sequenceGuard *WalletState,
dynamicFee string,
) (
txBytes []byte,
sequence uint64,
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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())
}
Expand Down

0 comments on commit 6c29f16

Please sign in to comment.