Skip to content

Commit 56afde4

Browse files
committed
feat: add fee estimator
1 parent 6bd2670 commit 56afde4

File tree

15 files changed

+394
-66
lines changed

15 files changed

+394
-66
lines changed

api/docgen/examples.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ func init() {
182182
state.WithKeyName("my_celes_key"),
183183
state.WithSignerAddress("celestia1pjcmwj8w6hyr2c4wehakc5g8cfs36aysgucx66"),
184184
state.WithFeeGranterAddress("celestia1hakc56ax66ypjcmwj8w6hyr2c4g8cfs3wesguc"),
185+
state.WithMaxGasPrice(state.DefaultMaxGasPrice),
186+
state.WithTxPriority(1),
185187
))
186188
}
187189

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
88
github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b
99
github.com/benbjohnson/clock v1.3.5
10-
github.com/celestiaorg/celestia-app/v3 v3.3.1
10+
github.com/celestiaorg/celestia-app/v3 v3.3.0
1111
github.com/celestiaorg/go-fraud v0.2.1
1212
github.com/celestiaorg/go-header v0.6.4
1313
github.com/celestiaorg/go-libp2p-messenger v0.2.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,8 @@ github.com/celestiaorg/blobstream-contracts/v3 v3.1.0 h1:h1Y4V3EMQ2mFmNtWt2sIhZI
347347
github.com/celestiaorg/blobstream-contracts/v3 v3.1.0/go.mod h1:x4DKyfKOSv1ZJM9NwV+Pw01kH2CD7N5zTFclXIVJ6GQ=
348348
github.com/celestiaorg/boxo v0.0.0-20241118122411-70a650316c3b h1:M9X7s1WJ/7Ju84ZUbO/6/8XlODkFsj/ln85AE0F6pj8=
349349
github.com/celestiaorg/boxo v0.0.0-20241118122411-70a650316c3b/go.mod h1:OpUrJtGmZZktUqJvPOtmP8wSfEFcdF/55d3PNCcYLwc=
350-
github.com/celestiaorg/celestia-app/v3 v3.3.1 h1:e0iSWbf84mMOGU3aVCDd+I7a7wUQLXurHXhcmG6lyQI=
351-
github.com/celestiaorg/celestia-app/v3 v3.3.1/go.mod h1:FSv7/cIGoZIzcQIQPxTYYDeCO78A4VmC20jxf3Oqn4Y=
350+
github.com/celestiaorg/celestia-app/v3 v3.3.0 h1:NW/Sx5EZiGUrBIqojoqgKYFmVHsb3R7u2hWdOxZV+wI=
351+
github.com/celestiaorg/celestia-app/v3 v3.3.0/go.mod h1:MKhiQgATDdLouzC5KvXDAnDpEgIXyD0MNiq0ChrWFco=
352352
github.com/celestiaorg/celestia-core v1.47.0-tm-v0.34.35 h1:K0kSVRlKfsPwfiA4o8GNUNPfZ+wF1MnYajom4CzJxpQ=
353353
github.com/celestiaorg/celestia-core v1.47.0-tm-v0.34.35/go.mod h1:FSd32MUffdVUYIXW+m/1v5pHptRQF2RJC88fwsgrKG8=
354354
github.com/celestiaorg/cosmos-sdk v1.27.0-sdk-v0.46.16 h1:qxWiGrDEcg4FzVTpIXU/v3wjP7q1Lz4AMhSBBRABInU=

libs/utils/address.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ import (
1010

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

13-
// SanitizeAddr trims leading protocol scheme and port from the given
14-
// IP address or hostname if present.
15-
func SanitizeAddr(addr string) (string, error) {
16-
original := addr
13+
// NormalizeAddress extracts the host and port, removing unsupported schemes.
14+
func NormalizeAddress(addr string) string {
1715
addr = strings.TrimPrefix(addr, "http://")
1816
addr = strings.TrimPrefix(addr, "https://")
1917
addr = strings.TrimPrefix(addr, "tcp://")
2018
addr = strings.TrimSuffix(addr, "/")
19+
return addr
20+
}
21+
22+
// SanitizeAddr trims leading protocol scheme and port from the given
23+
// IP address or hostname if present.
24+
func SanitizeAddr(addr string) (string, error) {
25+
original := addr
26+
addr = NormalizeAddress(addr)
2127
addr = strings.Split(addr, ":")[0]
2228
if addr == "" {
2329
return "", fmt.Errorf("%w: %s", ErrInvalidIP, original)

nodebuilder/core/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const (
1313

1414
var MetricsEnabled bool
1515

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

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

nodebuilder/core/flags.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import (
88
)
99

1010
var (
11-
coreIPFlag = "core.ip"
12-
corePortFlag = "core.port"
13-
coreGRPCFlag = "core.grpc.port"
14-
coreTLS = "core.tls"
15-
coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
11+
coreIPFlag = "core.ip"
12+
corePortFlag = "core.port"
13+
coreGRPCFlag = "core.grpc.port"
14+
coreTLS = "core.tls"
15+
coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
16+
coreEstimatorAddressFlag = "core.estimator.address"
1617
)
1718

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

@@ -88,5 +96,10 @@ func ParseFlags(
8896
}
8997
}
9098
cfg.IP = coreIP
99+
100+
if cmd.Flag(coreEstimatorAddressFlag).Changed {
101+
addr := cmd.Flag(coreEstimatorAddressFlag).Value.String()
102+
cfg.FeeEstimatorAddress = EstimatorAddress(addr)
103+
}
91104
return cfg.Validate()
92105
}

nodebuilder/state/cmd/state.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"strconv"
6+
"strings"
67

78
"cosmossdk.io/math"
89
"github.com/spf13/cobra"
@@ -18,6 +19,8 @@ var (
1819
gasPrice float64
1920
feeGranterAddress string
2021
amount uint64
22+
txPriority int
23+
maxGasPrice float64
2124
)
2225

2326
func init() {
@@ -460,6 +463,25 @@ func ApplyFlags(cmds ...*cobra.Command) {
460463
"Note: The granter should be provided as a Bech32 address.\n"+
461464
"Example: celestiaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
462465
)
466+
467+
// add additional flags for all submit transactions besides submit blobs.
468+
if !strings.Contains(cmd.Name(), "blob") {
469+
cmd.PersistentFlags().Float64Var(
470+
&maxGasPrice,
471+
"max.gas.price",
472+
state.DefaultMaxGasPrice,
473+
"Specifies max gas price for the tx submission.",
474+
)
475+
cmd.PersistentFlags().IntVar(
476+
&txPriority,
477+
"tx.priority",
478+
state.TxPriorityMedium,
479+
"Specifies tx priority. Should be set in range:"+
480+
"1. TxPriorityLow;\n"+
481+
"2. TxPriorityMedium;\n"+
482+
"3. TxPriorityHigh.\nDefault: TxPriorityMedium",
483+
)
484+
}
463485
}
464486
}
465487

@@ -470,5 +492,7 @@ func GetTxConfig() *state.TxConfig {
470492
state.WithKeyName(keyName),
471493
state.WithSignerAddress(signer),
472494
state.WithFeeGranterAddress(feeGranterAddress),
495+
state.WithMaxGasPrice(maxGasPrice),
496+
state.WithTxPriority(txPriority),
473497
)
474498
}

nodebuilder/state/core.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/celestiaorg/go-header/sync"
99

1010
"github.com/celestiaorg/celestia-node/header"
11+
"github.com/celestiaorg/celestia-node/nodebuilder/core"
1112
modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud"
1213
"github.com/celestiaorg/celestia-node/nodebuilder/p2p"
1314
"github.com/celestiaorg/celestia-node/share/eds/byzantine"
@@ -23,13 +24,14 @@ func coreAccessor(
2324
fraudServ libfraud.Service[*header.ExtendedHeader],
2425
network p2p.Network,
2526
client *grpc.ClientConn,
27+
address core.EstimatorAddress,
2628
) (
2729
*state.CoreAccessor,
2830
Module,
2931
*modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader],
3032
error,
3133
) {
32-
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, client, network.String())
34+
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, client, network.String(), string(address))
3335

3436
sBreaker := &modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]{
3537
Service: ca,

nodebuilder/state/module.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func ConstructModule(tp node.Type, cfg *Config, coreCfg *core.Config) fx.Option
2525
cfgErr := cfg.Validate()
2626
baseComponents := fx.Options(
2727
fx.Supply(*cfg),
28+
fx.Supply(coreCfg.FeeEstimatorAddress),
2829
fx.Error(cfgErr),
2930
fx.Provide(func(ks keystore.Keystore) (keyring.Keyring, AccountName, error) {
3031
return Keyring(*cfg, ks)

state/core_access.go

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ const (
3535
)
3636

3737
var (
38-
ErrInvalidAmount = errors.New("state: amount must be greater than zero")
39-
40-
log = logging.Logger("state")
38+
ErrInvalidAmount = errors.New("state: amount must be greater than zero")
39+
errGasPriceExceedsLimit = errors.New("state: estimated gasPrice exceeds max gasPrice")
40+
log = logging.Logger("state")
4141
)
4242

4343
// CoreAccessor implements service over a gRPC connection
@@ -63,6 +63,7 @@ type CoreAccessor struct {
6363
coreConn *grpc.ClientConn
6464
network string
6565

66+
estimator *estimator
6667
// these fields are mutatable and thus need to be protected by a mutex
6768
lock sync.Mutex
6869
lastPayForBlob int64
@@ -83,6 +84,7 @@ func NewCoreAccessor(
8384
getter libhead.Head[*header.ExtendedHeader],
8485
conn *grpc.ClientConn,
8586
network string,
87+
estimatorAddress string,
8688
) (*CoreAccessor, error) {
8789
// create verifier
8890
prt := merkle.DefaultProofRuntime()
@@ -106,6 +108,7 @@ func NewCoreAccessor(
106108
prt: prt,
107109
coreConn: conn,
108110
network: network,
111+
estimator: &estimator{estimatorAddress: estimatorAddress, defaultClientConn: conn},
109112
}
110113
return ca, nil
111114
}
@@ -136,6 +139,7 @@ func (ca *CoreAccessor) Start(ctx context.Context) error {
136139
if err != nil {
137140
return fmt.Errorf("querying minimum gas price: %w", err)
138141
}
142+
ca.estimator.connect()
139143
return nil
140144
}
141145

@@ -176,7 +180,7 @@ func (ca *CoreAccessor) SubmitPayForBlob(
176180
for i, blob := range libBlobs {
177181
blobSizes[i] = uint32(len(blob.Data()))
178182
}
179-
gas = estimateGasForBlobs(blobSizes)
183+
gas = ca.estimator.estimateGasForBlobs(blobSizes)
180184
}
181185

182186
gasPrice := cfg.GasPrice()
@@ -574,22 +578,6 @@ func (ca *CoreAccessor) submitMsg(
574578
}
575579

576580
txConfig := make([]user.TxOption, 0)
577-
gas := cfg.GasLimit()
578-
579-
if gas == 0 {
580-
gas, err = estimateGas(ctx, client, msg)
581-
if err != nil {
582-
return nil, fmt.Errorf("estimating gas: %w", err)
583-
}
584-
}
585-
586-
gasPrice := cfg.GasPrice()
587-
if gasPrice == DefaultGasPrice {
588-
gasPrice = ca.minGasPrice
589-
}
590-
591-
txConfig = append(txConfig, user.SetGasLimitAndGasPrice(gas, gasPrice))
592-
593581
if cfg.FeeGranterAddress() != "" {
594582
granter, err := parseAccAddressFromString(cfg.FeeGranterAddress())
595583
if err != nil {
@@ -598,7 +586,33 @@ func (ca *CoreAccessor) submitMsg(
598586
txConfig = append(txConfig, user.SetFeeGranter(granter))
599587
}
600588

589+
if cfg.GasLimit() == 0 || cfg.GasPrice() == DefaultGasPrice {
590+
gasPrice, gas, err := ca.estimator.estimateGas(ctx, client, cfg.priority, msg)
591+
if err != nil {
592+
return nil, err
593+
}
594+
595+
if cfg.GasLimit() == 0 {
596+
cfg.gas = gas
597+
}
598+
if cfg.GasPrice() == DefaultGasPrice {
599+
migGasPrice := ca.getMinGasPrice()
600+
if gasPrice < migGasPrice {
601+
gasPrice = migGasPrice
602+
}
603+
cfg.gasPrice = gasPrice
604+
cfg.isGasPriceSet = true
605+
}
606+
}
607+
608+
if cfg.maxGasPrice < cfg.GasPrice() {
609+
return nil, errGasPriceExceedsLimit
610+
}
611+
txConfig = append(txConfig, user.SetGasLimitAndGasPrice(cfg.GasLimit(), cfg.GasPrice()))
601612
resp, err := client.SubmitTx(ctx, []sdktypes.Msg{msg}, txConfig...)
613+
if err != nil {
614+
return nil, err
615+
}
602616
return convertToSdkTxResponse(resp), err
603617
}
604618

state/core_access_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ func TestTransfer(t *testing.T) {
131131
account: accounts[2],
132132
expErr: nil,
133133
},
134+
{
135+
name: "gas price limit exceeded",
136+
gasPrice: DefaultMaxGasPrice * 100,
137+
gasLim: 1000,
138+
account: accounts[2],
139+
expErr: errGasPriceExceedsLimit,
140+
},
134141
}
135142

136143
for _, tc := range testcases {
@@ -265,7 +272,7 @@ func buildAccessor(t *testing.T) (*CoreAccessor, []string) {
265272

266273
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
267274
require.NoError(t, err)
268-
ca, err := NewCoreAccessor(cctx.Keyring, accounts[0].Name, nil, conn, chainID)
275+
ca, err := NewCoreAccessor(cctx.Keyring, accounts[0].Name, nil, conn, chainID, "")
269276
require.NoError(t, err)
270277
return ca, getNames(accounts)
271278
}

0 commit comments

Comments
 (0)