Skip to content

feat(state/core_accessor): add fee estimator #4168

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

Merged
merged 10 commits into from
Mar 19, 2025
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 api/docgen/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ func init() {
state.WithKeyName("my_celes_key"),
state.WithSignerAddress("celestia1pjcmwj8w6hyr2c4wehakc5g8cfs36aysgucx66"),
state.WithFeeGranterAddress("celestia1hakc56ax66ypjcmwj8w6hyr2c4g8cfs3wesguc"),
state.WithMaxGasPrice(state.DefaultMaxGasPrice),
state.WithTxPriority(1),
))

add(network.DirUnknown)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b
github.com/benbjohnson/clock v1.3.5
github.com/celestiaorg/celestia-app/v3 v3.4.2-mocha
github.com/celestiaorg/celestia-app/v3 v3.5.0-arabica
github.com/celestiaorg/go-fraud v0.2.1
github.com/celestiaorg/go-header v0.6.4
github.com/celestiaorg/go-libp2p-messenger v0.2.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,8 @@ github.com/celestiaorg/blobstream-contracts/v3 v3.1.0 h1:h1Y4V3EMQ2mFmNtWt2sIhZI
github.com/celestiaorg/blobstream-contracts/v3 v3.1.0/go.mod h1:x4DKyfKOSv1ZJM9NwV+Pw01kH2CD7N5zTFclXIVJ6GQ=
github.com/celestiaorg/boxo v0.29.0-fork h1:gvoKagc3npL+ra8pEtgR+SMGGgeGsirJtzWrYQJ+5cs=
github.com/celestiaorg/boxo v0.29.0-fork/go.mod h1:c3R52nMlgMsN1tADffYcogKoVRsX1RJE1TMYSpJ4uVs=
github.com/celestiaorg/celestia-app/v3 v3.4.2-mocha h1:IJer3Od8/Y4/pF2671jBfE95Az3mRLLISwGVB6DJjPg=
github.com/celestiaorg/celestia-app/v3 v3.4.2-mocha/go.mod h1:HrRLUk9fdgTLaIv16D0JGiVO/ssqmK4lXMmSrsanhkM=
github.com/celestiaorg/celestia-app/v3 v3.5.0-arabica h1:1d1xlx+3tfVxIK2ca2tv4R1IcuPF0JcJdN4gqxnd5Ks=
github.com/celestiaorg/celestia-app/v3 v3.5.0-arabica/go.mod h1:1jX+Xr9VFuIL9oY3hYkJvyY0p6FFNV56PHk1kpGmeqg=
github.com/celestiaorg/celestia-core v1.47.0-tm-v0.34.35 h1:K0kSVRlKfsPwfiA4o8GNUNPfZ+wF1MnYajom4CzJxpQ=
github.com/celestiaorg/celestia-core v1.47.0-tm-v0.34.35/go.mod h1:FSd32MUffdVUYIXW+m/1v5pHptRQF2RJC88fwsgrKG8=
github.com/celestiaorg/cosmos-sdk v1.27.0-sdk-v0.46.16 h1:qxWiGrDEcg4FzVTpIXU/v3wjP7q1Lz4AMhSBBRABInU=
Expand Down
14 changes: 10 additions & 4 deletions libs/utils/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import (

var ErrInvalidIP = errors.New("invalid IP address or hostname given")

// SanitizeAddr trims leading protocol scheme and port from the given
// IP address or hostname if present.
func SanitizeAddr(addr string) (string, error) {
original := addr
// NormalizeAddress extracts the host and port, removing unsupported schemes.
func NormalizeAddress(addr string) string {
addr = strings.TrimPrefix(addr, "http://")
addr = strings.TrimPrefix(addr, "https://")
addr = strings.TrimPrefix(addr, "tcp://")
addr = strings.TrimSuffix(addr, "/")
return addr
}

// SanitizeAddr trims leading protocol scheme and port from the given
// IP address or hostname if present.
func SanitizeAddr(addr string) (string, error) {
original := addr
addr = NormalizeAddress(addr)
addr = strings.Split(addr, ":")[0]
if addr == "" {
return "", fmt.Errorf("%w: %s", ErrInvalidIP, original)
Expand Down
6 changes: 6 additions & 0 deletions nodebuilder/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const (

var MetricsEnabled bool

type EstimatorAddress string

// Config combines all configuration fields for managing the relationship with a Core node.
type Config struct {
IP string
Expand All @@ -24,6 +26,8 @@ type Config struct {
// The JSON file should have a key-value pair where the key is "x-token" and the value is the authentication token.
// If left empty, the client will not include the X-Token in its requests.
XTokenPath string
// FeeEstimatorAddress specifies a third-party endpoint that will be used to calculate the gas price and gas.
FeeEstimatorAddress EstimatorAddress
}

// DefaultConfig returns default configuration for managing the
Expand Down Expand Up @@ -54,6 +58,8 @@ func (cfg *Config) Validate() error {
if err != nil {
return fmt.Errorf("nodebuilder/core: invalid grpc port: %s", err.Error())
}
pasedAddr := utils.NormalizeAddress(string(cfg.FeeEstimatorAddress))
cfg.FeeEstimatorAddress = EstimatorAddress(pasedAddr)
return nil
}

Expand Down
23 changes: 18 additions & 5 deletions nodebuilder/core/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import (
)

var (
coreIPFlag = "core.ip"
corePortFlag = "core.port"
coreGRPCFlag = "core.grpc.port"
coreTLS = "core.tls"
coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
coreIPFlag = "core.ip"
corePortFlag = "core.port"
coreGRPCFlag = "core.grpc.port"
coreTLS = "core.tls"
coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
coreEstimatorAddressFlag = "core.estimator.address"
)

// Flags gives a set of hardcoded Core flags.
Expand Down Expand Up @@ -50,6 +51,13 @@ func Flags() *flag.FlagSet {
"NOTE: the path is parsed only if coreTLS enabled."+
"If left empty, the client will not include the X-Token in its requests.",
)
flags.String(
coreEstimatorAddressFlag,
"",
"specifies the endpoint of the third-party service that should be used to calculate"+
"the gas price and gas. Format: <address>:<port>. Default connection to the consensus node will be used if "+
"left empty.",
)
return flags
}

Expand Down Expand Up @@ -88,5 +96,10 @@ func ParseFlags(
}
}
cfg.IP = coreIP

if cmd.Flag(coreEstimatorAddressFlag).Changed {
addr := cmd.Flag(coreEstimatorAddressFlag).Value.String()
cfg.FeeEstimatorAddress = EstimatorAddress(addr)
}
return cfg.Validate()
}
24 changes: 24 additions & 0 deletions nodebuilder/state/cmd/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"strconv"
"strings"

"cosmossdk.io/math"
"github.com/spf13/cobra"
Expand All @@ -18,6 +19,8 @@ var (
gasPrice float64
feeGranterAddress string
amount uint64
txPriority int
maxGasPrice float64
)

func init() {
Expand Down Expand Up @@ -460,6 +463,25 @@ func ApplyFlags(cmds ...*cobra.Command) {
"Note: The granter should be provided as a Bech32 address.\n"+
"Example: celestiaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
)

// add additional flags for all submit transactions besides submit blobs.
if !strings.Contains(cmd.Name(), "blob") {
cmd.PersistentFlags().Float64Var(
&maxGasPrice,
"max.gas.price",
state.DefaultMaxGasPrice,
"Specifies max gas price for the tx submission.",
)
cmd.PersistentFlags().IntVar(
&txPriority,
"tx.priority",
state.TxPriorityMedium,
"Specifies tx priority. Should be set in range:"+
"1. TxPriorityLow;\n"+
"2. TxPriorityMedium;\n"+
"3. TxPriorityHigh.\nDefault: TxPriorityMedium",
)
}
}
}

Expand All @@ -470,5 +492,7 @@ func GetTxConfig() *state.TxConfig {
state.WithKeyName(keyName),
state.WithSignerAddress(signer),
state.WithFeeGranterAddress(feeGranterAddress),
state.WithMaxGasPrice(maxGasPrice),
state.WithTxPriority(txPriority),
)
}
4 changes: 3 additions & 1 deletion nodebuilder/state/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/celestiaorg/go-header/sync"

"github.com/celestiaorg/celestia-node/header"
"github.com/celestiaorg/celestia-node/nodebuilder/core"
modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud"
"github.com/celestiaorg/celestia-node/nodebuilder/p2p"
"github.com/celestiaorg/celestia-node/share/eds/byzantine"
Expand All @@ -23,13 +24,14 @@ func coreAccessor(
fraudServ libfraud.Service[*header.ExtendedHeader],
network p2p.Network,
client *grpc.ClientConn,
address core.EstimatorAddress,
) (
*state.CoreAccessor,
Module,
*modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader],
error,
) {
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, client, network.String())
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, client, network.String(), string(address))

sBreaker := &modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]{
Service: ca,
Expand Down
1 change: 1 addition & 0 deletions nodebuilder/state/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func ConstructModule(tp node.Type, cfg *Config, coreCfg *core.Config) fx.Option
cfgErr := cfg.Validate()
baseComponents := fx.Options(
fx.Supply(*cfg),
fx.Supply(coreCfg.FeeEstimatorAddress),
fx.Error(cfgErr),
fx.Provide(func(ks keystore.Keystore) (keyring.Keyring, AccountName, error) {
return Keyring(*cfg, ks)
Expand Down
47 changes: 26 additions & 21 deletions state/core_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ const (

var (
ErrInvalidAmount = errors.New("state: amount must be greater than zero")

log = logging.Logger("state")
log = logging.Logger("state")
)

// CoreAccessor implements service over a gRPC connection
Expand All @@ -63,6 +62,7 @@ type CoreAccessor struct {
coreConn *grpc.ClientConn
network string

estimator *estimator
// these fields are mutatable and thus need to be protected by a mutex
lock sync.Mutex
lastPayForBlob int64
Expand All @@ -83,6 +83,7 @@ func NewCoreAccessor(
getter libhead.Head[*header.ExtendedHeader],
conn *grpc.ClientConn,
network string,
estimatorAddress string,
) (*CoreAccessor, error) {
// create verifier
prt := merkle.DefaultProofRuntime()
Expand All @@ -106,6 +107,7 @@ func NewCoreAccessor(
prt: prt,
coreConn: conn,
network: network,
estimator: &estimator{estimatorAddress: estimatorAddress, defaultClientConn: conn},
}
return ca, nil
}
Expand Down Expand Up @@ -136,12 +138,18 @@ func (ca *CoreAccessor) Start(ctx context.Context) error {
if err != nil {
return fmt.Errorf("querying minimum gas price: %w", err)
}

err = ca.estimator.Start(ctx)
if err != nil {
log.Warn("state: failed to connect to estimator endpoint", "err", err)
return fmt.Errorf("state: failed to connect to estimator endpoint: %w", err)
}
return nil
}

func (ca *CoreAccessor) Stop(context.Context) error {
func (ca *CoreAccessor) Stop(ctx context.Context) error {
ca.cancel()
return nil
return ca.estimator.Stop(ctx)
}

// SubmitPayForBlob builds, signs, and synchronously submits a MsgPayForBlob with additional
Expand Down Expand Up @@ -176,7 +184,7 @@ func (ca *CoreAccessor) SubmitPayForBlob(
for i, blob := range libBlobs {
blobSizes[i] = uint32(len(blob.Data()))
}
gas = estimateGasForBlobs(blobSizes)
gas = ca.estimator.estimateGasForBlobs(blobSizes)
}

gasPrice := cfg.GasPrice()
Expand Down Expand Up @@ -574,22 +582,6 @@ func (ca *CoreAccessor) submitMsg(
}

txConfig := make([]user.TxOption, 0)
gas := cfg.GasLimit()

if gas == 0 {
gas, err = estimateGas(ctx, client, msg)
if err != nil {
return nil, fmt.Errorf("estimating gas: %w", err)
}
}

gasPrice := cfg.GasPrice()
if gasPrice == DefaultGasPrice {
gasPrice = ca.minGasPrice
}

txConfig = append(txConfig, user.SetGasLimitAndGasPrice(gas, gasPrice))

if cfg.FeeGranterAddress() != "" {
granter, err := parseAccAddressFromString(cfg.FeeGranterAddress())
if err != nil {
Expand All @@ -598,7 +590,20 @@ func (ca *CoreAccessor) submitMsg(
txConfig = append(txConfig, user.SetFeeGranter(granter))
}

gasPrice, gas, err := ca.estimator.estimate(ctx, cfg, client, msg)
if err != nil {
return nil, err
}

if gasPrice < ca.getMinGasPrice() {
gasPrice = ca.getMinGasPrice()
}
txConfig = append(txConfig, user.SetGasLimitAndGasPrice(gas, gasPrice))

resp, err := client.SubmitTx(ctx, []sdktypes.Msg{msg}, txConfig...)
if err != nil {
return nil, err
}
return convertToSdkTxResponse(resp), err
}

Expand Down
17 changes: 15 additions & 2 deletions state/core_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,19 @@ func TestTransfer(t *testing.T) {
expErr: nil,
},
{
name: "transfer with options",
name: "transfer with gasPrice set",
gasPrice: 0.005,
gasLim: 0,
account: accounts[2],
expErr: nil,
},
{
name: "transfer with gas set",
gasPrice: DefaultGasPrice,
gasLim: 84617,
account: accounts[2],
expErr: nil,
},
}

for _, tc := range testcases {
Expand Down Expand Up @@ -213,6 +220,12 @@ func TestDelegate(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, 0, resp.Code)

opts = NewTxConfig(
WithGas(tc.gasLim),
WithGasPrice(tc.gasPrice),
WithKeyName(accounts[2]),
)

resp, err = ca.Undelegate(ctx, ValAddress(valAddr), sdktypes.NewInt(100_000), opts)
require.NoError(t, err)
require.EqualValues(t, 0, resp.Code)
Expand Down Expand Up @@ -265,7 +278,7 @@ func buildAccessor(t *testing.T) (*CoreAccessor, []string) {

conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
ca, err := NewCoreAccessor(cctx.Keyring, accounts[0].Name, nil, conn, chainID)
ca, err := NewCoreAccessor(cctx.Keyring, accounts[0].Name, nil, conn, chainID, "")
require.NoError(t, err)
return ca, getNames(accounts)
}
Expand Down
Loading
Loading