Skip to content

Commit

Permalink
feat: backport features to allow node to handle gas price estimation (#…
Browse files Browse the repository at this point in the history
…2134)

Backports #2122

---------

Co-authored-by: Rootul P <[email protected]>
  • Loading branch information
cmwaters and rootulp authored Jul 24, 2023
1 parent 3e50374 commit 7807722
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 37 deletions.
8 changes: 8 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
minttypes "github.com/celestiaorg/celestia-app/x/mint/types"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
nodeservice "github.com/cosmos/cosmos-sdk/client/grpc/node"
"github.com/cosmos/cosmos-sdk/client/grpc/tmservice"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/types"
Expand Down Expand Up @@ -676,6 +677,9 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, _ config.APIConfig) {
// Register new tendermint queries routes from grpc-gateway.
tmservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter)

// Register node gRPC service for grpc-gateway.
nodeservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter)

// Register the
ModuleBasics.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter)
}
Expand All @@ -690,6 +694,10 @@ func (app *App) RegisterTendermintService(clientCtx client.Context) {
tmservice.RegisterTendermintService(clientCtx, app.BaseApp.GRPCQueryRouter(), app.interfaceRegistry, nil)
}

func (app *App) RegisterNodeService(clientCtx client.Context) {
nodeservice.RegisterNodeService(clientCtx, app.GRPCQueryRouter())
}

func (app *App) setPostHanders() {
postHandler, err := posthandler.NewPostHandler(
posthandler.HandlerOptions{},
Expand Down
80 changes: 80 additions & 0 deletions app/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package errors

import (
"errors"
"fmt"
"regexp"
"strconv"

sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

var (
// This is relatively brittle. It would be better if going below the min gas price
// had a specific error type.
regexpMinGasPrice = regexp.MustCompile(`insufficient fees; got: \d+utia required: \d+utia`)
regexpInt = regexp.MustCompile(`[0-9]+`)
)

// ParseInsufficientMinGasPrice checks if the error is due to the gas price being too low.
// Given the previous gas price and gas limit, it returns the new minimum gas price that
// the node should accept.
// If the error is not due to the gas price being too low, it returns 0, nil
func ParseInsufficientMinGasPrice(err error, gasPrice float64, gasLimit uint64) (float64, error) {
// first work out if the error is ErrInsufficientFunds
if err == nil || !sdkerrors.ErrInsufficientFee.Is(err) {
return 0, nil
}

// As there are multiple cases of ErrInsufficientFunds, we need to check the error message
// matches the regexp
substr := regexpMinGasPrice.FindAllString(err.Error(), -1)
if len(substr) != 1 {
return 0, nil
}

// extract the first and second numbers from the error message (got and required)
numbers := regexpInt.FindAllString(substr[0], -1)
if len(numbers) != 2 {
return 0, fmt.Errorf("expected two numbers in error message got %d", len(numbers))
}

// attempt to parse them into float64 values
got, err := strconv.ParseFloat(numbers[0], 64)
if err != nil {
return 0, err
}
required, err := strconv.ParseFloat(numbers[1], 64)
if err != nil {
return 0, err
}

// catch rare condition that required is zero. This should theoretically
// never happen as a min gas price of zero should always be accepted.
if required == 0 {
return 0, errors.New("unexpected case: required gas price is zero (why was an error returned)")
}

// calculate the actual min gas price of the node based on the difference
// between the got and required values. If gas price was zero, we need to use
// the gasLimit to infer this.
if gasPrice == 0 || got == 0 {
if gasLimit == 0 {
return 0, fmt.Errorf("gas limit and gas price cannot be zero")
}
return required / float64(gasLimit), nil
}
return required / got * gasPrice, nil
}

// IsInsufficientMinGasPrice checks if the error is due to the gas price being too low.
func IsInsufficientMinGasPrice(err error) bool {
// first work out if the error is ErrInsufficientFunds
if err == nil || !sdkerrors.ErrInsufficientFee.Is(err) {
return false
}

// As there are multiple cases of ErrInsufficientFunds, we need to check the error message
// matches the regexp
return regexpMinGasPrice.MatchString(err.Error())
}
145 changes: 145 additions & 0 deletions app/errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package errors_test

import (
"fmt"
"testing"

"cosmossdk.io/errors"
"github.com/celestiaorg/celestia-app/app"
apperr "github.com/celestiaorg/celestia-app/app/errors"
"github.com/celestiaorg/celestia-app/pkg/appconsts"
"github.com/celestiaorg/celestia-app/pkg/namespace"
testutil "github.com/celestiaorg/celestia-app/test/util"
blob "github.com/celestiaorg/celestia-app/x/blob/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/stretchr/testify/require"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
)

// This will detect any changes to the DeductFeeDecorator which may cause a
// different error message that does not match the regexp.
func TestInsufficientMinGasPriceIntegration(t *testing.T) {
var (
gasLimit uint64 = 1_000_000
feeAmount int64 = 10
gasPrice = float64(feeAmount) / float64(gasLimit)
)
account := "test"
testApp, kr := testutil.SetupTestAppWithGenesisValSet(app.DefaultConsensusParams(), account)
minGasPrice, err := sdk.ParseDecCoins(fmt.Sprintf("%v%s", appconsts.DefaultMinGasPrice, app.BondDenom))
require.NoError(t, err)
ctx := testApp.NewContext(true, tmproto.Header{}).WithMinGasPrices(minGasPrice)
signer := blob.NewKeyringSigner(kr, account, testutil.ChainID)
builder := signer.NewTxBuilder()
builder.SetGasLimit(gasLimit)
builder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(feeAmount))))

address, err := signer.GetSignerInfo().GetAddress()
require.NoError(t, err)

_, err = sdk.AccAddressFromBech32(address.String())
require.NoError(t, err, address)

b, err := blob.NewBlob(namespace.RandomNamespace(), []byte("hello world"), 0)
require.NoError(t, err)

pfb, err := blob.NewMsgPayForBlobs(address.String(), b)
require.NoError(t, err, address)

tx, err := signer.BuildSignedTx(builder, pfb)
require.NoError(t, err)

decorator := ante.NewDeductFeeDecorator(testApp.AccountKeeper, testApp.BankKeeper, testApp.FeeGrantKeeper, nil)
anteHandler := sdk.ChainAnteDecorators(decorator)

_, err = anteHandler(ctx, tx, false)
require.True(t, apperr.IsInsufficientMinGasPrice(err))
actualGasPrice, err := apperr.ParseInsufficientMinGasPrice(err, gasPrice, gasLimit)
require.NoError(t, err)
require.Equal(t, appconsts.DefaultMinGasPrice, actualGasPrice, err)
}

func TestInsufficientMinGasPriceTable(t *testing.T) {
testCases := []struct {
name string
err error
inputGasPrice float64
inputGasLimit uint64
isInsufficientMinGasPriceErr bool
expectParsingError bool
expectedGasPrice float64
}{
{
name: "nil error",
err: nil,
isInsufficientMinGasPriceErr: false,
},
{
name: "not insufficient fee error",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "not enough gas to pay for blobs (minimum: 1000000, got: 100000)"),
isInsufficientMinGasPriceErr: false,
},
{
name: "not insufficient fee error 2",
err: errors.Wrap(sdkerrors.ErrInsufficientFunds, "not enough gas to pay for blobs (got: 1000000, required: 100000)"),
isInsufficientMinGasPriceErr: false,
},
{
name: "insufficient fee error",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 10utia required: 100utia"),
inputGasPrice: 0.01,
expectedGasPrice: 0.1,
isInsufficientMinGasPriceErr: true,
},
{
name: "insufficient fee error with zero gas price",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 0utia required: 100utia"),
inputGasPrice: 0,
inputGasLimit: 100,
expectedGasPrice: 1,
isInsufficientMinGasPriceErr: true,
},
{
name: "insufficient fee error with zero gas price and zero gas limit",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 0utia required: 100utia"),
inputGasPrice: 0,
inputGasLimit: 0,
isInsufficientMinGasPriceErr: true,
expectParsingError: true,
},
{
name: "incorrectly formatted error",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 0uatom required: 100uatom"),
isInsufficientMinGasPriceErr: false,
},
{
name: "error with zero required gas price",
err: errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 10utia required: 0utia"),
isInsufficientMinGasPriceErr: true,
expectParsingError: true,
},
{
name: "error with extra wrapping",
err: errors.Wrap(errors.Wrap(sdkerrors.ErrInsufficientFee, "insufficient fees; got: 10utia required: 100utia"), "extra wrapping"),
inputGasPrice: 0.01,
expectedGasPrice: 0.1,
isInsufficientMinGasPriceErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.isInsufficientMinGasPriceErr, apperr.IsInsufficientMinGasPrice(tc.err))
actualGasPrice, err := apperr.ParseInsufficientMinGasPrice(tc.err, tc.inputGasPrice, tc.inputGasLimit)
if tc.expectParsingError {
require.Error(t, err)
require.Zero(t, actualGasPrice)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectedGasPrice, actualGasPrice)
}
})
}
}
21 changes: 7 additions & 14 deletions test/txsim/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"math/rand"

"github.com/celestiaorg/celestia-app/pkg/appconsts"
ns "github.com/celestiaorg/celestia-app/pkg/namespace"
"github.com/celestiaorg/celestia-app/test/util/blobfactory"
blob "github.com/celestiaorg/celestia-app/x/blob/types"
Expand Down Expand Up @@ -86,7 +85,7 @@ func (s *BlobSequence) Next(_ context.Context, _ grpc.ClientConn, rand *rand.Ran
return Operation{
Msgs: []types.Msg{msg},
Blobs: blobs,
GasLimit: EstimateGas(sizes),
GasLimit: estimateGas(sizes),
}, nil
}

Expand All @@ -107,18 +106,12 @@ func (r Range) Rand(rand *rand.Rand) int {
return rand.Intn(r.Max-r.Min) + r.Min
}

const (
perByteGasTolerance = 2
pfbGasFixedCost = 80000
)

// EstimateGas estimates the gas required to pay for a set of blobs in a PFB.
func EstimateGas(blobSizes []int) uint64 {
totalByteCount := 0
for _, size := range blobSizes {
totalByteCount += size
// estimateGas estimates the gas required to pay for a set of blobs in a PFB.
func estimateGas(blobSizes []int) uint64 {
size := make([]uint32, len(blobSizes))
for i, s := range blobSizes {
size[i] = uint32(s)
}
variableGasAmount := (appconsts.DefaultGasPerBlobByte + perByteGasTolerance) * totalByteCount

return uint64(variableGasAmount + pfbGasFixedCost)
return blob.DefaultEstimateGas(size)
}
6 changes: 3 additions & 3 deletions test/txsim/sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ type Operation struct {
}

const (
// set default gas limit to cover the costs of most transactions
// At 0.001 utia per gas, this equates to 1000utia per transaction
DefaultGasLimit = 1000000
// Set the default gas limit to cover the costs of most transactions.
// At 0.1 utia per gas, this equates to 20_000utia per transaction.
DefaultGasLimit = 200_000
)

// ErrEndOfSequence is a special error which indicates that the sequence has been terminated
Expand Down
13 changes: 1 addition & 12 deletions x/blob/ante/ante.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package ante

import (
"github.com/celestiaorg/celestia-app/pkg/appconsts"
"github.com/celestiaorg/celestia-app/pkg/shares"
"github.com/celestiaorg/celestia-app/x/blob/types"

"cosmossdk.io/errors"
Expand Down Expand Up @@ -38,7 +36,7 @@ func (d MinGasPFBDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool
// lazily fetch the gas per byte param
gasPerByte = d.k.GasPerBlobByte(ctx)
}
gasToConsume := gasToConsume(pfb, gasPerByte)
gasToConsume := pfb.Gas(gasPerByte)
if gasToConsume > txGas {
return ctx, errors.Wrapf(sdkerrors.ErrInsufficientFee, "not enough gas to pay for blobs (minimum: %d, got: %d)", gasToConsume, txGas)
}
Expand All @@ -48,15 +46,6 @@ func (d MinGasPFBDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool
return next(ctx, tx, simulate)
}

func gasToConsume(pfb *types.MsgPayForBlobs, gasPerByte uint32) uint64 {
var totalSharesUsed uint64
for _, size := range pfb.BlobSizes {
totalSharesUsed += uint64(shares.SparseSharesNeeded(size))
}

return totalSharesUsed * appconsts.ShareSize * uint64(gasPerByte)
}

type BlobKeeper interface {
GasPerBlobByte(ctx sdk.Context) uint32
}
9 changes: 1 addition & 8 deletions x/blob/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"context"
"fmt"

"github.com/celestiaorg/celestia-app/pkg/appconsts"
"github.com/celestiaorg/celestia-app/pkg/shares"
"github.com/celestiaorg/celestia-app/x/blob/types"
"github.com/cosmos/cosmos-sdk/codec"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
Expand Down Expand Up @@ -52,12 +50,7 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger {
func (k Keeper) PayForBlobs(goCtx context.Context, msg *types.MsgPayForBlobs) (*types.MsgPayForBlobsResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

var totalSharesUsed uint64
for _, size := range msg.BlobSizes {
totalSharesUsed += uint64(shares.SparseSharesNeeded(size))
}

gasToConsume := totalSharesUsed * appconsts.ShareSize * uint64(k.GasPerBlobByte(ctx))
gasToConsume := types.GasToConsume(msg.BlobSizes, k.GasPerBlobByte(ctx))
ctx.GasMeter().ConsumeGas(gasToConsume, payForBlobGasDescriptor)

err := ctx.EventManager().EmitTypedEvent(
Expand Down
Loading

0 comments on commit 7807722

Please sign in to comment.