From 02f04c93446b66d1eccd449d604531ca528d5ffc Mon Sep 17 00:00:00 2001 From: CHAMI Rachid Date: Thu, 23 Jan 2025 17:33:46 +0100 Subject: [PATCH] feat: implement the gas estimation API (#4257) ## Overview Implements the gas estimation endpoint following: https://github.com/celestiaorg/CIPs/blob/main/cips/cip-18.md Is there an issue for this or I create one? --------- Co-authored-by: Rootul P --- Makefile | 2 +- app/app.go | 3 + app/grpc/gasestimation/gas_estimator.go | 248 ++++ app/grpc/gasestimation/gas_estimator.pb.go | 1058 +++++++++++++++++ app/grpc/gasestimation/gas_estimator_test.go | 138 +++ app/test/gas_estimation_test.go | 191 +++ buf.lock | 11 +- buf.yaml | 1 + ...r-023-gas-used-and-gas-price-estimation.md | 2 +- .../v1/gas_estimation/gas_estimator.proto | 71 ++ 10 files changed, 1719 insertions(+), 6 deletions(-) create mode 100644 app/grpc/gasestimation/gas_estimator.go create mode 100644 app/grpc/gasestimation/gas_estimator.pb.go create mode 100644 app/grpc/gasestimation/gas_estimator_test.go create mode 100644 app/test/gas_estimation_test.go create mode 100644 proto/celestia/core/v1/gas_estimation/gas_estimator.proto diff --git a/Makefile b/Makefile index 09c5938ce2..e9e37956ae 100644 --- a/Makefile +++ b/Makefile @@ -165,7 +165,7 @@ test-race: # TODO: Remove the -skip flag once the following tests no longer contain data races. # https://github.com/celestiaorg/celestia-app/issues/1369 @echo "--> Running tests in race mode" - @go test -timeout 15m ./... -v -race -skip "TestPrepareProposalConsistency|TestIntegrationTestSuite|TestBlobstreamRPCQueries|TestSquareSizeIntegrationTest|TestStandardSDKIntegrationTestSuite|TestTxsimCommandFlags|TestTxsimCommandEnvVar|TestMintIntegrationTestSuite|TestBlobstreamCLI|TestUpgrade|TestMaliciousTestNode|TestBigBlobSuite|TestQGBIntegrationSuite|TestSignerTestSuite|TestPriorityTestSuite|TestTimeInPrepareProposalContext|TestBlobstream|TestCLITestSuite|TestLegacyUpgrade|TestSignerTwins|TestConcurrentTxSubmission|TestTxClientTestSuite|Test_testnode|TestEvictions" + @go test -timeout 15m ./... -v -race -skip "TestPrepareProposalConsistency|TestIntegrationTestSuite|TestBlobstreamRPCQueries|TestSquareSizeIntegrationTest|TestStandardSDKIntegrationTestSuite|TestTxsimCommandFlags|TestTxsimCommandEnvVar|TestMintIntegrationTestSuite|TestBlobstreamCLI|TestUpgrade|TestMaliciousTestNode|TestBigBlobSuite|TestQGBIntegrationSuite|TestSignerTestSuite|TestPriorityTestSuite|TestTimeInPrepareProposalContext|TestBlobstream|TestCLITestSuite|TestLegacyUpgrade|TestSignerTwins|TestConcurrentTxSubmission|TestTxClientTestSuite|Test_testnode|TestEvictions|TestEstimateGasUsed|TestEstimateGasPrice" .PHONY: test-race ## test-bench: Run unit tests in bench mode. diff --git a/app/app.go b/app/app.go index 9ad10d3282..2ff878b118 100644 --- a/app/app.go +++ b/app/app.go @@ -6,6 +6,8 @@ import ( "slices" "time" + "github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation" + "github.com/celestiaorg/celestia-app/v3/app/ante" "github.com/celestiaorg/celestia-app/v3/app/encoding" celestiatx "github.com/celestiaorg/celestia-app/v3/app/grpc/tx" @@ -753,6 +755,7 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, _ config.APIConfig) { func (app *App) RegisterTxService(clientCtx client.Context) { authtx.RegisterTxService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.BaseApp.Simulate, app.interfaceRegistry) celestiatx.RegisterTxService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.interfaceRegistry) + gasestimation.RegisterGasEstimationService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.BaseApp.Simulate) } // RegisterTendermintService implements the Application.RegisterTendermintService method. diff --git a/app/grpc/gasestimation/gas_estimator.go b/app/grpc/gasestimation/gas_estimator.go new file mode 100644 index 0000000000..273e5add53 --- /dev/null +++ b/app/grpc/gasestimation/gas_estimator.go @@ -0,0 +1,248 @@ +package gasestimation + +import ( + "context" + "fmt" + "math" + "strconv" + "strings" + + "github.com/celestiaorg/celestia-app/v3/pkg/appconsts" + "github.com/celestiaorg/celestia-app/v3/x/minfee" + blobtx "github.com/celestiaorg/go-square/v2/tx" + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + gogogrpc "github.com/gogo/protobuf/grpc" + coretypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// EstimationZScore is the z-score corresponding to 10% and 90% of the gas prices distribution. +// More information can be found in: https://en.wikipedia.org/wiki/Standard_normal_table#Cumulative_(less_than_Z) +const EstimationZScore = 1.28 + +// baseAppSimulateFn is the signature of the Baseapp#Simulate function. +type baseAppSimulateFn func(txBytes []byte) (sdk.GasInfo, *sdk.Result, error) + +// RegisterGasEstimationService registers the gas estimation service on the gRPC router. +func RegisterGasEstimationService(qrt gogogrpc.Server, clientCtx client.Context, simulateFn baseAppSimulateFn) { + RegisterGasEstimatorServer( + qrt, + NewGasEstimatorServer(clientCtx, simulateFn), + ) +} + +var _ GasEstimatorServer = &gasEstimatorServer{} + +type gasEstimatorServer struct { + clientCtx client.Context + simulateFn baseAppSimulateFn +} + +func NewGasEstimatorServer(clientCtx client.Context, simulateFn baseAppSimulateFn) GasEstimatorServer { + return &gasEstimatorServer{ + clientCtx: clientCtx, + simulateFn: simulateFn, + } +} + +// lastFiveBlocksTransactionsQuery transaction search query to get all the transactions in the last five blocks. +// the latestHeight param represents the chain's tip height. +func lastFiveBlocksTransactionsQuery(latestHeight int64) string { + startHeight := latestHeight - 5 + if startHeight < 0 { + startHeight = 0 + } + return fmt.Sprintf("tx.height>%d AND tx.height<=%d", startHeight, latestHeight) +} + +// numberOfTransactionsPerPage the number of transactions to return per page in the transaction search +// endpoint. +// Note: the maximum number of transactions per page the endpoint allows is 100. +var numberOfTransactionsPerPage = 100 + +func (s *gasEstimatorServer) EstimateGasPrice(ctx context.Context, request *EstimateGasPriceRequest) (*EstimateGasPriceResponse, error) { + gasPrice, err := s.estimateGasPrice(ctx, request.TxPriority) + if err != nil { + return nil, err + } + return &EstimateGasPriceResponse{EstimatedGasPrice: gasPrice}, nil +} + +// EstimateGasPriceAndUsage takes a transaction priority and a transaction bytes +// and estimates the gas price based on the gas prices of the transactions in the last five blocks. +// If no transaction is found in the last five blocks, return the network +// min gas price. +// It's up to the light client to set the gas price in this case +// to the minimum gas price set by that node. +// The gas used is estimated using the state machine simulation. +func (s *gasEstimatorServer) EstimateGasPriceAndUsage(ctx context.Context, request *EstimateGasPriceAndUsageRequest) (*EstimateGasPriceAndUsageResponse, error) { + // estimate the gas price + gasPrice, err := s.estimateGasPrice(ctx, request.TxPriority) + if err != nil { + return nil, err + } + + // estimate the gas used + btx, isBlob, err := blobtx.UnmarshalBlobTx(request.TxBytes) + if isBlob && err != nil { + return nil, err + } + + var txBytes []byte + if isBlob { + txBytes = btx.Tx + } else { + txBytes = request.TxBytes + } + + gasUsedInfo, _, err := s.simulateFn(txBytes) + if err != nil { + return nil, err + } + return &EstimateGasPriceAndUsageResponse{ + EstimatedGasPrice: gasPrice, + EstimatedGasUsed: gasUsedInfo.GasUsed, + }, nil +} + +// estimateGasPrice takes a transaction priority and estimates the gas price based +// on the gas prices of the transactions in the last five blocks. +// If no transaction is found in the last five blocks, return the network +// min gas price. +// It's up to the light client to set the gas price in this case +// to the minimum gas price set by that node. +func (s *gasEstimatorServer) estimateGasPrice(ctx context.Context, priority TxPriority) (float64, error) { + status, err := s.clientCtx.Client.Status(ctx) + if err != nil { + return 0, err + } + latestHeight := status.SyncInfo.LatestBlockHeight + page := 1 + txSearchResult, err := s.clientCtx.Client.TxSearch( + ctx, + lastFiveBlocksTransactionsQuery(latestHeight), + false, + &page, + &numberOfTransactionsPerPage, + "asc", + ) + if err != nil { + return 0, err + } + + totalNumberOfTransactions := txSearchResult.TotalCount + if totalNumberOfTransactions == 0 { + // return the min gas price if no transaction found in the last 5 blocks + return minfee.DefaultNetworkMinGasPrice.MustFloat64(), nil + } + + gasPrices := make([]float64, 0) + for { + currentPageGasPrices, err := extractGasPriceFromTransactions(txSearchResult.Txs) + if err != nil { + return 0, err + } + gasPrices = append(gasPrices, currentPageGasPrices...) + if len(gasPrices) >= totalNumberOfTransactions { + break + } + page++ + txSearchResult, err = s.clientCtx.Client.TxSearch( + ctx, + lastFiveBlocksTransactionsQuery(latestHeight), + false, + &page, + &numberOfTransactionsPerPage, + "asc", + ) + if err != nil { + return 0, err + } + } + return estimateGasPriceForTransactions(gasPrices, priority) +} + +// estimateGasPriceForTransactions takes a list of transactions and priority +// and returns a gas price estimation. +// The priority sets the estimation as follows: +// - High Priority: The gas price is the price at the start of the top 10% of transactions’ gas prices from the last five blocks. +// - Medium Priority: The gas price is the mean of all gas prices from the last five blocks. +// - Low Priority: The gas price is the value at the end of the lowest 10% of gas prices from the last five blocks. +// - Unspecified Priority (default): This is equivalent to the Medium priority, using the mean of all gas prices from the last five blocks. +// More information can be found in ADR-023. +func estimateGasPriceForTransactions(gasPrices []float64, priority TxPriority) (float64, error) { + meanGasPrice := Mean(gasPrices) + switch priority { + case TxPriority_TX_PRIORITY_UNSPECIFIED: + return meanGasPrice, nil + case TxPriority_TX_PRIORITY_LOW: + stDev := StandardDeviation(meanGasPrice, gasPrices) + return meanGasPrice - EstimationZScore*stDev, nil + case TxPriority_TX_PRIORITY_MEDIUM: + return meanGasPrice, nil + case TxPriority_TX_PRIORITY_HIGH: + stDev := StandardDeviation(meanGasPrice, gasPrices) + return meanGasPrice + EstimationZScore*stDev, nil + default: + return 0, fmt.Errorf("unknown priority: %d", priority) + } +} + +// extractGasPriceFromTransactions takes a list of transaction results +// and returns their corresponding gas prices. +func extractGasPriceFromTransactions(txs []*coretypes.ResultTx) ([]float64, error) { + gasPrices := make([]float64, 0) + for _, tx := range txs { + var feeWithDenom string + for _, event := range tx.TxResult.Events { + if event.GetType() == "tx" { + for _, attr := range event.Attributes { + if string(attr.Key) == "fee" { + feeWithDenom = string(attr.Value) + } + } + } + } + if feeWithDenom == "" { + return nil, fmt.Errorf("couldn't find fee for transaction %s", tx.Hash) + } + feeWithoutDenom, found := strings.CutSuffix(feeWithDenom, appconsts.BondDenom) + if !found { + return nil, fmt.Errorf("couldn't find fee denom for transaction %s: %s", tx.Hash, feeWithDenom) + } + fee, err := strconv.ParseFloat(feeWithoutDenom, 64) + if err != nil { + return nil, fmt.Errorf("couldn't parse fee for transaction %s: %w", tx.Hash, err) + } + if tx.TxResult.GasWanted == 0 { + return nil, fmt.Errorf("zero gas wanted for transaction %s", tx.Hash) + } + gasPrices = append(gasPrices, fee/float64(tx.TxResult.GasWanted)) + } + return gasPrices, nil +} + +// Mean calculates the mean value of the provided gas prices. +func Mean(gasPrices []float64) float64 { + if len(gasPrices) == 0 { + return 0 + } + sum := 0.0 + for _, gasPrice := range gasPrices { + sum += gasPrice + } + return sum / float64(len(gasPrices)) +} + +// StandardDeviation calculates the standard deviation of the provided gas prices. +func StandardDeviation(meanGasPrice float64, gasPrices []float64) float64 { + if len(gasPrices) < 2 { + return 0 + } + var variance float64 + for _, gasPrice := range gasPrices { + variance += math.Pow(gasPrice-meanGasPrice, 2) + } + variance /= float64(len(gasPrices)) + return math.Sqrt(variance) +} diff --git a/app/grpc/gasestimation/gas_estimator.pb.go b/app/grpc/gasestimation/gas_estimator.pb.go new file mode 100644 index 0000000000..362c2f04f2 --- /dev/null +++ b/app/grpc/gasestimation/gas_estimator.pb.go @@ -0,0 +1,1058 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: celestia/core/v1/gas_estimation/gas_estimator.proto + +package gasestimation + +import ( + context "context" + encoding_binary "encoding/binary" + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + grpc1 "github.com/gogo/protobuf/grpc" + proto "github.com/gogo/protobuf/proto" + _ "google.golang.org/genproto/googleapis/api/annotations" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// TxPriority is the priority level of the requested gas price. +// The following priority levels are defined: +// - High Priority: The gas price is the price at the start of the top 10% of transactions’ gas prices from the last 5 blocks. +// - Medium Priority: The gas price is the mean of all gas prices from the last 5 blocks. +// - Low Priority: The gas price is the value at the end of the lowest 10% of gas prices from the last 5 blocks. +// - Unspecified Priority (default): This is equivalent to the Medium priority, using the mean of all gas prices from the last 5 blocks. +type TxPriority int32 + +const ( + // TX_PRIORITY_UNSPECIFIED none priority, the default priority level, which is equivalent to + // the TX_PRIORITY_MEDIUM priority. + TxPriority_TX_PRIORITY_UNSPECIFIED TxPriority = 0 + // TX_PRIORITY_LOW low priority. + TxPriority_TX_PRIORITY_LOW TxPriority = 1 + // TX_PRIORITY_MEDIUM medium priority. + TxPriority_TX_PRIORITY_MEDIUM TxPriority = 2 + // TX_PRIORITY_HIGH high priority. + TxPriority_TX_PRIORITY_HIGH TxPriority = 3 +) + +var TxPriority_name = map[int32]string{ + 0: "TX_PRIORITY_UNSPECIFIED", + 1: "TX_PRIORITY_LOW", + 2: "TX_PRIORITY_MEDIUM", + 3: "TX_PRIORITY_HIGH", +} + +var TxPriority_value = map[string]int32{ + "TX_PRIORITY_UNSPECIFIED": 0, + "TX_PRIORITY_LOW": 1, + "TX_PRIORITY_MEDIUM": 2, + "TX_PRIORITY_HIGH": 3, +} + +func (x TxPriority) String() string { + return proto.EnumName(TxPriority_name, int32(x)) +} + +func (TxPriority) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_67d02876d749b9cc, []int{0} +} + +// EstimateGasPriceRequest the request to estimate the gas price of the network. +// Takes a priority enum to define the priority level. +type EstimateGasPriceRequest struct { + TxPriority TxPriority `protobuf:"varint,1,opt,name=tx_priority,json=txPriority,proto3,enum=celestia.core.v1.gas_estimation.TxPriority" json:"tx_priority,omitempty"` +} + +func (m *EstimateGasPriceRequest) Reset() { *m = EstimateGasPriceRequest{} } +func (m *EstimateGasPriceRequest) String() string { return proto.CompactTextString(m) } +func (*EstimateGasPriceRequest) ProtoMessage() {} +func (*EstimateGasPriceRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_67d02876d749b9cc, []int{0} +} +func (m *EstimateGasPriceRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EstimateGasPriceRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EstimateGasPriceRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EstimateGasPriceRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_EstimateGasPriceRequest.Merge(m, src) +} +func (m *EstimateGasPriceRequest) XXX_Size() int { + return m.Size() +} +func (m *EstimateGasPriceRequest) XXX_DiscardUnknown() { + xxx_messageInfo_EstimateGasPriceRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_EstimateGasPriceRequest proto.InternalMessageInfo + +func (m *EstimateGasPriceRequest) GetTxPriority() TxPriority { + if m != nil { + return m.TxPriority + } + return TxPriority_TX_PRIORITY_UNSPECIFIED +} + +// EstimateGasPriceResponse the response of the gas price estimation. +type EstimateGasPriceResponse struct { + EstimatedGasPrice float64 `protobuf:"fixed64,1,opt,name=estimated_gas_price,json=estimatedGasPrice,proto3" json:"estimated_gas_price,omitempty"` +} + +func (m *EstimateGasPriceResponse) Reset() { *m = EstimateGasPriceResponse{} } +func (m *EstimateGasPriceResponse) String() string { return proto.CompactTextString(m) } +func (*EstimateGasPriceResponse) ProtoMessage() {} +func (*EstimateGasPriceResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_67d02876d749b9cc, []int{1} +} +func (m *EstimateGasPriceResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EstimateGasPriceResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EstimateGasPriceResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EstimateGasPriceResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_EstimateGasPriceResponse.Merge(m, src) +} +func (m *EstimateGasPriceResponse) XXX_Size() int { + return m.Size() +} +func (m *EstimateGasPriceResponse) XXX_DiscardUnknown() { + xxx_messageInfo_EstimateGasPriceResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_EstimateGasPriceResponse proto.InternalMessageInfo + +func (m *EstimateGasPriceResponse) GetEstimatedGasPrice() float64 { + if m != nil { + return m.EstimatedGasPrice + } + return 0 +} + +// EstimateGasPriceAndUsageRequest the request to estimate the gas price of the network +// and also the gas used for the provided transaction. +type EstimateGasPriceAndUsageRequest struct { + TxPriority TxPriority `protobuf:"varint,1,opt,name=tx_priority,json=txPriority,proto3,enum=celestia.core.v1.gas_estimation.TxPriority" json:"tx_priority,omitempty"` + TxBytes []byte `protobuf:"bytes,2,opt,name=tx_bytes,json=txBytes,proto3" json:"tx_bytes,omitempty"` +} + +func (m *EstimateGasPriceAndUsageRequest) Reset() { *m = EstimateGasPriceAndUsageRequest{} } +func (m *EstimateGasPriceAndUsageRequest) String() string { return proto.CompactTextString(m) } +func (*EstimateGasPriceAndUsageRequest) ProtoMessage() {} +func (*EstimateGasPriceAndUsageRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_67d02876d749b9cc, []int{2} +} +func (m *EstimateGasPriceAndUsageRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EstimateGasPriceAndUsageRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EstimateGasPriceAndUsageRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EstimateGasPriceAndUsageRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_EstimateGasPriceAndUsageRequest.Merge(m, src) +} +func (m *EstimateGasPriceAndUsageRequest) XXX_Size() int { + return m.Size() +} +func (m *EstimateGasPriceAndUsageRequest) XXX_DiscardUnknown() { + xxx_messageInfo_EstimateGasPriceAndUsageRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_EstimateGasPriceAndUsageRequest proto.InternalMessageInfo + +func (m *EstimateGasPriceAndUsageRequest) GetTxPriority() TxPriority { + if m != nil { + return m.TxPriority + } + return TxPriority_TX_PRIORITY_UNSPECIFIED +} + +func (m *EstimateGasPriceAndUsageRequest) GetTxBytes() []byte { + if m != nil { + return m.TxBytes + } + return nil +} + +// EstimateGasPriceAndUsageResponse the response of the gas price and used +// estimation. +type EstimateGasPriceAndUsageResponse struct { + EstimatedGasPrice float64 `protobuf:"fixed64,1,opt,name=estimated_gas_price,json=estimatedGasPrice,proto3" json:"estimated_gas_price,omitempty"` + EstimatedGasUsed uint64 `protobuf:"varint,2,opt,name=estimated_gas_used,json=estimatedGasUsed,proto3" json:"estimated_gas_used,omitempty"` +} + +func (m *EstimateGasPriceAndUsageResponse) Reset() { *m = EstimateGasPriceAndUsageResponse{} } +func (m *EstimateGasPriceAndUsageResponse) String() string { return proto.CompactTextString(m) } +func (*EstimateGasPriceAndUsageResponse) ProtoMessage() {} +func (*EstimateGasPriceAndUsageResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_67d02876d749b9cc, []int{3} +} +func (m *EstimateGasPriceAndUsageResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EstimateGasPriceAndUsageResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EstimateGasPriceAndUsageResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EstimateGasPriceAndUsageResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_EstimateGasPriceAndUsageResponse.Merge(m, src) +} +func (m *EstimateGasPriceAndUsageResponse) XXX_Size() int { + return m.Size() +} +func (m *EstimateGasPriceAndUsageResponse) XXX_DiscardUnknown() { + xxx_messageInfo_EstimateGasPriceAndUsageResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_EstimateGasPriceAndUsageResponse proto.InternalMessageInfo + +func (m *EstimateGasPriceAndUsageResponse) GetEstimatedGasPrice() float64 { + if m != nil { + return m.EstimatedGasPrice + } + return 0 +} + +func (m *EstimateGasPriceAndUsageResponse) GetEstimatedGasUsed() uint64 { + if m != nil { + return m.EstimatedGasUsed + } + return 0 +} + +func init() { + proto.RegisterEnum("celestia.core.v1.gas_estimation.TxPriority", TxPriority_name, TxPriority_value) + proto.RegisterType((*EstimateGasPriceRequest)(nil), "celestia.core.v1.gas_estimation.EstimateGasPriceRequest") + proto.RegisterType((*EstimateGasPriceResponse)(nil), "celestia.core.v1.gas_estimation.EstimateGasPriceResponse") + proto.RegisterType((*EstimateGasPriceAndUsageRequest)(nil), "celestia.core.v1.gas_estimation.EstimateGasPriceAndUsageRequest") + proto.RegisterType((*EstimateGasPriceAndUsageResponse)(nil), "celestia.core.v1.gas_estimation.EstimateGasPriceAndUsageResponse") +} + +func init() { + proto.RegisterFile("celestia/core/v1/gas_estimation/gas_estimator.proto", fileDescriptor_67d02876d749b9cc) +} + +var fileDescriptor_67d02876d749b9cc = []byte{ + // 476 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x54, 0xbb, 0x6f, 0xd3, 0x40, + 0x18, 0xf7, 0x05, 0x04, 0xe8, 0xa3, 0x02, 0x73, 0x45, 0x34, 0x0d, 0xc8, 0x8d, 0x3c, 0x55, 0x3c, + 0x6c, 0xb5, 0x5d, 0x80, 0x89, 0x96, 0x9a, 0xd4, 0xa8, 0xa5, 0x91, 0x49, 0xc4, 0x63, 0xb1, 0x2e, + 0xce, 0xe9, 0xb0, 0x94, 0xf8, 0x8e, 0xbb, 0x4b, 0xe5, 0x6e, 0xac, 0x30, 0xf1, 0x2f, 0xf0, 0x8f, + 0x30, 0x33, 0x76, 0x64, 0x44, 0xc9, 0x3f, 0x82, 0x6c, 0xd7, 0xa9, 0x9b, 0xaa, 0xaa, 0x08, 0xea, + 0x60, 0xe9, 0xbe, 0xc7, 0xef, 0xe1, 0xcf, 0x9f, 0x0f, 0x36, 0x22, 0x3a, 0xa0, 0x4a, 0xc7, 0xc4, + 0x8d, 0xb8, 0xa4, 0xee, 0xc1, 0x9a, 0xcb, 0x88, 0x0a, 0xb3, 0xcc, 0x90, 0xe8, 0x98, 0x27, 0xd5, + 0x90, 0x4b, 0x47, 0x48, 0xae, 0x39, 0x5e, 0x29, 0x41, 0x4e, 0x06, 0x72, 0x0e, 0xd6, 0x9c, 0xd3, + 0xa0, 0xc6, 0x03, 0xc6, 0x39, 0x1b, 0x50, 0x97, 0x88, 0xd8, 0x25, 0x49, 0xc2, 0x75, 0x9e, 0x56, + 0x05, 0xbc, 0xb1, 0x1c, 0x71, 0x35, 0xe4, 0x2a, 0xcc, 0x23, 0xb7, 0x08, 0x8a, 0x92, 0xcd, 0x60, + 0xc9, 0x2b, 0x68, 0x68, 0x8b, 0xa8, 0xb6, 0x8c, 0x23, 0x1a, 0xd0, 0xcf, 0x23, 0xaa, 0x34, 0xde, + 0x85, 0x9b, 0x3a, 0x0d, 0x85, 0x8c, 0xb9, 0x8c, 0xf5, 0x61, 0x1d, 0x35, 0xd1, 0xea, 0xad, 0xf5, + 0x47, 0xce, 0x05, 0x56, 0x9c, 0x4e, 0xda, 0x3e, 0x86, 0x04, 0xa0, 0xa7, 0x67, 0xfb, 0x35, 0xd4, + 0xcf, 0x0a, 0x29, 0xc1, 0x13, 0x45, 0xb1, 0x03, 0x8b, 0xc7, 0x04, 0xb4, 0x1f, 0x66, 0x74, 0x22, + 0x2b, 0xe7, 0x8a, 0x28, 0xb8, 0x33, 0x2d, 0x95, 0x38, 0xfb, 0x1b, 0x82, 0x95, 0x59, 0xb2, 0xcd, + 0xa4, 0xdf, 0x55, 0x84, 0x5d, 0x8e, 0x7b, 0xbc, 0x0c, 0x37, 0x74, 0x1a, 0xf6, 0x0e, 0x35, 0x55, + 0xf5, 0x5a, 0x13, 0xad, 0x2e, 0x04, 0xd7, 0x75, 0xba, 0x95, 0x85, 0xf6, 0x17, 0x04, 0xcd, 0xf3, + 0xcd, 0xcc, 0xf7, 0x86, 0xf8, 0x31, 0xe0, 0xd3, 0xfd, 0x23, 0x45, 0xfb, 0xb9, 0xf2, 0xd5, 0xc0, + 0xac, 0xb6, 0x77, 0x15, 0xed, 0x3f, 0x1c, 0x00, 0x9c, 0xf8, 0xc6, 0xf7, 0x61, 0xa9, 0xf3, 0x3e, + 0x6c, 0x07, 0xfe, 0x7e, 0xe0, 0x77, 0x3e, 0x84, 0xdd, 0x37, 0x6f, 0xdb, 0xde, 0x4b, 0xff, 0x95, + 0xef, 0x6d, 0x9b, 0x06, 0x5e, 0x84, 0xdb, 0xd5, 0xe2, 0xee, 0xfe, 0x3b, 0x13, 0xe1, 0x7b, 0x80, + 0xab, 0xc9, 0x3d, 0x6f, 0xdb, 0xef, 0xee, 0x99, 0x35, 0x7c, 0x17, 0xcc, 0x6a, 0x7e, 0xc7, 0x6f, + 0xed, 0x98, 0x57, 0xd6, 0x7f, 0xd6, 0x60, 0xa1, 0x45, 0x94, 0x57, 0xee, 0x28, 0xfe, 0x8a, 0xc0, + 0x9c, 0x9d, 0x00, 0x7e, 0x7a, 0xe1, 0xa8, 0xcf, 0xd9, 0xbb, 0xc6, 0xb3, 0x39, 0x90, 0xc5, 0x98, + 0x6d, 0x03, 0xff, 0x40, 0x67, 0xf7, 0xac, 0xfc, 0x1a, 0xf8, 0xc5, 0x3f, 0x33, 0xcf, 0x6c, 0x55, + 0x63, 0xf3, 0x3f, 0x18, 0x4a, 0x8f, 0x5b, 0x9d, 0x5f, 0x63, 0x0b, 0x1d, 0x8d, 0x2d, 0xf4, 0x67, + 0x6c, 0xa1, 0xef, 0x13, 0xcb, 0x38, 0x9a, 0x58, 0xc6, 0xef, 0x89, 0x65, 0x7c, 0x7c, 0xce, 0x62, + 0xfd, 0x69, 0xd4, 0x73, 0x22, 0x3e, 0x74, 0x4b, 0x21, 0x2e, 0xd9, 0xf4, 0xfc, 0x84, 0x08, 0xe1, + 0x66, 0x0f, 0x93, 0x22, 0xca, 0x6e, 0x8a, 0x13, 0xe1, 0xde, 0xb5, 0xfc, 0x87, 0xde, 0xf8, 0x1b, + 0x00, 0x00, 0xff, 0xff, 0xcb, 0xfa, 0xf9, 0x62, 0x61, 0x04, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// GasEstimatorClient is the client API for GasEstimator service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type GasEstimatorClient interface { + // estimateGasPrice takes a transaction priority and estimates the gas price based + // on the gas prices of the transactions in the last five blocks. + // If no transaction is found in the last five blocks, return the network + // min gas price. + // It's up to the light client to set the gas price in this case + // to the minimum gas price set by that node. + EstimateGasPrice(ctx context.Context, in *EstimateGasPriceRequest, opts ...grpc.CallOption) (*EstimateGasPriceResponse, error) + // EstimateGasPriceAndUsage takes a transaction priority and a transaction bytes + // and estimates the gas price and the gas used for that transaction. + // The gas price estimation is based on the gas prices of the transactions in the last five blocks. + // If no transaction is found in the last five blocks, return the network + // min gas price. + // It's up to the light client to set the gas price in this case + // to the minimum gas price set by that node. + // The gas used is estimated using the state machine simulation. + EstimateGasPriceAndUsage(ctx context.Context, in *EstimateGasPriceAndUsageRequest, opts ...grpc.CallOption) (*EstimateGasPriceAndUsageResponse, error) +} + +type gasEstimatorClient struct { + cc grpc1.ClientConn +} + +func NewGasEstimatorClient(cc grpc1.ClientConn) GasEstimatorClient { + return &gasEstimatorClient{cc} +} + +func (c *gasEstimatorClient) EstimateGasPrice(ctx context.Context, in *EstimateGasPriceRequest, opts ...grpc.CallOption) (*EstimateGasPriceResponse, error) { + out := new(EstimateGasPriceResponse) + err := c.cc.Invoke(ctx, "/celestia.core.v1.gas_estimation.GasEstimator/EstimateGasPrice", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gasEstimatorClient) EstimateGasPriceAndUsage(ctx context.Context, in *EstimateGasPriceAndUsageRequest, opts ...grpc.CallOption) (*EstimateGasPriceAndUsageResponse, error) { + out := new(EstimateGasPriceAndUsageResponse) + err := c.cc.Invoke(ctx, "/celestia.core.v1.gas_estimation.GasEstimator/EstimateGasPriceAndUsage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GasEstimatorServer is the server API for GasEstimator service. +type GasEstimatorServer interface { + // estimateGasPrice takes a transaction priority and estimates the gas price based + // on the gas prices of the transactions in the last five blocks. + // If no transaction is found in the last five blocks, return the network + // min gas price. + // It's up to the light client to set the gas price in this case + // to the minimum gas price set by that node. + EstimateGasPrice(context.Context, *EstimateGasPriceRequest) (*EstimateGasPriceResponse, error) + // EstimateGasPriceAndUsage takes a transaction priority and a transaction bytes + // and estimates the gas price and the gas used for that transaction. + // The gas price estimation is based on the gas prices of the transactions in the last five blocks. + // If no transaction is found in the last five blocks, return the network + // min gas price. + // It's up to the light client to set the gas price in this case + // to the minimum gas price set by that node. + // The gas used is estimated using the state machine simulation. + EstimateGasPriceAndUsage(context.Context, *EstimateGasPriceAndUsageRequest) (*EstimateGasPriceAndUsageResponse, error) +} + +// UnimplementedGasEstimatorServer can be embedded to have forward compatible implementations. +type UnimplementedGasEstimatorServer struct { +} + +func (*UnimplementedGasEstimatorServer) EstimateGasPrice(ctx context.Context, req *EstimateGasPriceRequest) (*EstimateGasPriceResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method EstimateGasPrice not implemented") +} +func (*UnimplementedGasEstimatorServer) EstimateGasPriceAndUsage(ctx context.Context, req *EstimateGasPriceAndUsageRequest) (*EstimateGasPriceAndUsageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method EstimateGasPriceAndUsage not implemented") +} + +func RegisterGasEstimatorServer(s grpc1.Server, srv GasEstimatorServer) { + s.RegisterService(&_GasEstimator_serviceDesc, srv) +} + +func _GasEstimator_EstimateGasPrice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EstimateGasPriceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GasEstimatorServer).EstimateGasPrice(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/celestia.core.v1.gas_estimation.GasEstimator/EstimateGasPrice", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GasEstimatorServer).EstimateGasPrice(ctx, req.(*EstimateGasPriceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GasEstimator_EstimateGasPriceAndUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EstimateGasPriceAndUsageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GasEstimatorServer).EstimateGasPriceAndUsage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/celestia.core.v1.gas_estimation.GasEstimator/EstimateGasPriceAndUsage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GasEstimatorServer).EstimateGasPriceAndUsage(ctx, req.(*EstimateGasPriceAndUsageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _GasEstimator_serviceDesc = grpc.ServiceDesc{ + ServiceName: "celestia.core.v1.gas_estimation.GasEstimator", + HandlerType: (*GasEstimatorServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "EstimateGasPrice", + Handler: _GasEstimator_EstimateGasPrice_Handler, + }, + { + MethodName: "EstimateGasPriceAndUsage", + Handler: _GasEstimator_EstimateGasPriceAndUsage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "celestia/core/v1/gas_estimation/gas_estimator.proto", +} + +func (m *EstimateGasPriceRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EstimateGasPriceRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EstimateGasPriceRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.TxPriority != 0 { + i = encodeVarintGasEstimator(dAtA, i, uint64(m.TxPriority)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *EstimateGasPriceResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EstimateGasPriceResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EstimateGasPriceResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.EstimatedGasPrice != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.EstimatedGasPrice)))) + i-- + dAtA[i] = 0x9 + } + return len(dAtA) - i, nil +} + +func (m *EstimateGasPriceAndUsageRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EstimateGasPriceAndUsageRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EstimateGasPriceAndUsageRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.TxBytes) > 0 { + i -= len(m.TxBytes) + copy(dAtA[i:], m.TxBytes) + i = encodeVarintGasEstimator(dAtA, i, uint64(len(m.TxBytes))) + i-- + dAtA[i] = 0x12 + } + if m.TxPriority != 0 { + i = encodeVarintGasEstimator(dAtA, i, uint64(m.TxPriority)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *EstimateGasPriceAndUsageResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EstimateGasPriceAndUsageResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EstimateGasPriceAndUsageResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.EstimatedGasUsed != 0 { + i = encodeVarintGasEstimator(dAtA, i, uint64(m.EstimatedGasUsed)) + i-- + dAtA[i] = 0x10 + } + if m.EstimatedGasPrice != 0 { + i -= 8 + encoding_binary.LittleEndian.PutUint64(dAtA[i:], uint64(math.Float64bits(float64(m.EstimatedGasPrice)))) + i-- + dAtA[i] = 0x9 + } + return len(dAtA) - i, nil +} + +func encodeVarintGasEstimator(dAtA []byte, offset int, v uint64) int { + offset -= sovGasEstimator(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *EstimateGasPriceRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.TxPriority != 0 { + n += 1 + sovGasEstimator(uint64(m.TxPriority)) + } + return n +} + +func (m *EstimateGasPriceResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.EstimatedGasPrice != 0 { + n += 9 + } + return n +} + +func (m *EstimateGasPriceAndUsageRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.TxPriority != 0 { + n += 1 + sovGasEstimator(uint64(m.TxPriority)) + } + l = len(m.TxBytes) + if l > 0 { + n += 1 + l + sovGasEstimator(uint64(l)) + } + return n +} + +func (m *EstimateGasPriceAndUsageResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.EstimatedGasPrice != 0 { + n += 9 + } + if m.EstimatedGasUsed != 0 { + n += 1 + sovGasEstimator(uint64(m.EstimatedGasUsed)) + } + return n +} + +func sovGasEstimator(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGasEstimator(x uint64) (n int) { + return sovGasEstimator(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *EstimateGasPriceRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EstimateGasPriceRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EstimateGasPriceRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field TxPriority", wireType) + } + m.TxPriority = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.TxPriority |= TxPriority(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipGasEstimator(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGasEstimator + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *EstimateGasPriceResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EstimateGasPriceResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EstimateGasPriceResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field EstimatedGasPrice", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.EstimatedGasPrice = float64(math.Float64frombits(v)) + default: + iNdEx = preIndex + skippy, err := skipGasEstimator(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGasEstimator + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *EstimateGasPriceAndUsageRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EstimateGasPriceAndUsageRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EstimateGasPriceAndUsageRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field TxPriority", wireType) + } + m.TxPriority = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.TxPriority |= TxPriority(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TxBytes", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGasEstimator + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGasEstimator + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.TxBytes = append(m.TxBytes[:0], dAtA[iNdEx:postIndex]...) + if m.TxBytes == nil { + m.TxBytes = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGasEstimator(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGasEstimator + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *EstimateGasPriceAndUsageResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EstimateGasPriceAndUsageResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EstimateGasPriceAndUsageResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 1 { + return fmt.Errorf("proto: wrong wireType = %d for field EstimatedGasPrice", wireType) + } + var v uint64 + if (iNdEx + 8) > l { + return io.ErrUnexpectedEOF + } + v = uint64(encoding_binary.LittleEndian.Uint64(dAtA[iNdEx:])) + iNdEx += 8 + m.EstimatedGasPrice = float64(math.Float64frombits(v)) + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field EstimatedGasUsed", wireType) + } + m.EstimatedGasUsed = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.EstimatedGasUsed |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipGasEstimator(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGasEstimator + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGasEstimator(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGasEstimator + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGasEstimator + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGasEstimator + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGasEstimator + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGasEstimator = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGasEstimator = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGasEstimator = fmt.Errorf("proto: unexpected end of group") +) diff --git a/app/grpc/gasestimation/gas_estimator_test.go b/app/grpc/gasestimation/gas_estimator_test.go new file mode 100644 index 0000000000..f7ebf1e7dd --- /dev/null +++ b/app/grpc/gasestimation/gas_estimator_test.go @@ -0,0 +1,138 @@ +package gasestimation + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMean(t *testing.T) { + tests := []struct { + name string + gasPrices []float64 + want float64 + }{ + { + name: "Empty slice", + gasPrices: []float64{}, + want: 0, + }, + { + name: "Single element", + gasPrices: []float64{10}, + want: 10, + }, + { + name: "Multiple elements", + gasPrices: []float64{1, 2, 3, 4, 5}, + want: 3, + }, + { + name: "Mixed floats", + gasPrices: []float64{1.5, 2.5, 3.5}, + want: 2.5, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Mean(tt.gasPrices) + if got != tt.want { + t.Errorf("mean(%v) = %v, want %v", tt.gasPrices, got, tt.want) + } + }) + } +} + +func TestStandardDeviation(t *testing.T) { + tests := []struct { + name string + gasPrices []float64 + want float64 + }{ + { + name: "Empty slice", + gasPrices: []float64{}, + want: 0, + }, + { + name: "Single element", + gasPrices: []float64{10}, + want: 0, + }, + { + name: "Multiple elements", + gasPrices: []float64{10, 20, 30, 40, 50}, + // Variance = 200, stdev ~ 14.142135... + want: 14.142135623730951, + }, + { + name: "Identical elements", + gasPrices: []float64{5, 5, 5, 5}, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + meanVal := Mean(tt.gasPrices) + got := StandardDeviation(meanVal, tt.gasPrices) + // We'll do a tolerance check for floating-point comparisons. + if math.Abs(got-tt.want) > 1e-9 { + t.Errorf("stdDev(%v) = %v, want %v", tt.gasPrices, got, tt.want) + } + }) + } +} + +func TestEstimateGasPriceForTransactions(t *testing.T) { + gasPrices := []float64{10, 20, 30, 40, 50} + meanGasPrices := Mean(gasPrices) + stDev := StandardDeviation(meanGasPrices, gasPrices) + + tests := []struct { + name string + priority TxPriority + want float64 + wantErr bool + }{ + { + name: "NONE -> same as MEDIUM (mean)", + priority: TxPriority_TX_PRIORITY_UNSPECIFIED, + want: meanGasPrices, + }, + { + name: "LOW -> mean - ZScore * stDev", + priority: TxPriority_TX_PRIORITY_LOW, + want: meanGasPrices - EstimationZScore*stDev, + }, + { + name: "MEDIUM -> mean", + priority: TxPriority_TX_PRIORITY_MEDIUM, + want: meanGasPrices, + }, + { + name: "HIGH -> mean + ZScore * stDev", + priority: TxPriority_TX_PRIORITY_HIGH, + want: meanGasPrices + EstimationZScore*stDev, + }, + { + name: "Unknown -> error", + priority: 999, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := estimateGasPriceForTransactions(gasPrices, tt.priority) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/app/test/gas_estimation_test.go b/app/test/gas_estimation_test.go new file mode 100644 index 0000000000..e467671259 --- /dev/null +++ b/app/test/gas_estimation_test.go @@ -0,0 +1,191 @@ +package app + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/celestiaorg/celestia-app/v3/app" + "github.com/celestiaorg/celestia-app/v3/app/encoding" + "github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation" + "github.com/celestiaorg/celestia-app/v3/pkg/appconsts" + "github.com/celestiaorg/celestia-app/v3/pkg/user" + "github.com/celestiaorg/celestia-app/v3/test/util/blobfactory" + "github.com/celestiaorg/celestia-app/v3/test/util/testfactory" + "github.com/celestiaorg/celestia-app/v3/test/util/testnode" + blobtypes "github.com/celestiaorg/celestia-app/v3/x/blob/types" + "github.com/celestiaorg/go-square/v2/share" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + tmrand "github.com/tendermint/tendermint/libs/rand" +) + +func TestEstimateGasPrice(t *testing.T) { + if testing.Short() { + t.Skip("skipping app/test/gas_estimation gas price and usage in short mode.") + } + + // test setup: create a test chain, submit a few PFBs to it, keep track of their gas + // price, then test the gas estimator API. + accountNames := testfactory.GenerateAccounts(150) // using 150 to have 2 pages of txs + cfg := testnode.DefaultConfig().WithFundedAccounts(accountNames...). + WithTimeoutCommit(10 * time.Second) // to have all the transactions in just a few blocks + cctx, _, _ := testnode.NewNetwork(t, cfg) + require.NoError(t, cctx.WaitForNextBlock()) + + // create the gas estimation client + gasEstimationAPI := gasestimation.NewGasEstimatorClient(cctx.GRPCClient) + + // test getting the gas price for an empty blockchain + resp, err := gasEstimationAPI.EstimateGasPrice(cctx.GoContext(), &gasestimation.EstimateGasPriceRequest{}) + require.NoError(t, err) + assert.Equal(t, appconsts.DefaultNetworkMinGasPrice, resp.EstimatedGasPrice) + + encfg := encoding.MakeConfig(app.ModuleEncodingRegisters...) + rand := tmrand.NewRand() + + txClient, err := user.SetupTxClient(cctx.GoContext(), cctx.Keyring, cctx.GRPCClient, encfg) + require.NoError(t, err) + + gasLimit := blobtypes.DefaultEstimateGas([]uint32{100}) + gasPricesChan := make(chan float64, len(accountNames)) + wg := &sync.WaitGroup{} + for _, accName := range accountNames { + wg.Add(1) + go func() { + defer wg.Done() + // ensure that it is greater than the min gas price + gasPrice := float64(rand.Intn(1000)+1) * appconsts.DefaultMinGasPrice + blobs := blobfactory.ManyBlobs(rand, []share.Namespace{share.RandomBlobNamespace()}, []int{100}) + resp, err := txClient.BroadcastPayForBlobWithAccount( + cctx.GoContext(), + accName, + blobs, + user.SetGasLimitAndGasPrice(gasLimit, gasPrice), + ) + require.NoError(t, err) + require.Equal(t, abci.CodeTypeOK, resp.Code, resp.RawLog) + gasPricesChan <- gasPrice + }() + } + wg.Wait() + err = cctx.WaitForNextBlock() + require.NoError(t, err) + + close(gasPricesChan) + gasPrices := make([]float64, 0, len(accountNames)) + for price := range gasPricesChan { + gasPrices = append(gasPrices, price) + } + + meanGasPrice := gasestimation.Mean(gasPrices) + stDev := gasestimation.StandardDeviation(meanGasPrice, gasPrices) + tests := []struct { + name string + priority gasestimation.TxPriority + expectedGasPrice float64 + }{ + { + name: "NONE -> same as MEDIUM (mean)", + priority: gasestimation.TxPriority_TX_PRIORITY_UNSPECIFIED, + expectedGasPrice: func() float64 { + return meanGasPrice + }(), + }, + { + name: "LOW -> mean - ZScore * stDev", + priority: gasestimation.TxPriority_TX_PRIORITY_LOW, + expectedGasPrice: func() float64 { + return meanGasPrice - gasestimation.EstimationZScore*stDev + }(), + }, + { + name: "MEDIUM -> mean", + priority: gasestimation.TxPriority_TX_PRIORITY_MEDIUM, + expectedGasPrice: func() float64 { + return meanGasPrice + }(), + }, + { + name: "HIGH -> mean + ZScore * stDev", + priority: gasestimation.TxPriority_TX_PRIORITY_HIGH, + expectedGasPrice: func() float64 { + return meanGasPrice + gasestimation.EstimationZScore*stDev + }(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := gasEstimationAPI.EstimateGasPrice(cctx.GoContext(), &gasestimation.EstimateGasPriceRequest{TxPriority: tt.priority}) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("%.2f", tt.expectedGasPrice), fmt.Sprintf("%.2f", resp.EstimatedGasPrice)) + }) + } +} + +func TestEstimateGasUsed(t *testing.T) { + if testing.Short() { + t.Skip("skipping app/test/gas_estimation gas price and usage in short mode.") + } + + cfg := testnode.DefaultConfig().WithFundedAccounts("test") + cctx, _, _ := testnode.NewNetwork(t, cfg) + require.NoError(t, cctx.WaitForNextBlock()) + + encfg := encoding.MakeConfig(app.ModuleEncodingRegisters...) + txClient, err := user.SetupTxClient(cctx.GoContext(), cctx.Keyring, cctx.GRPCClient, encfg) + require.NoError(t, err) + txClient.SetGasMultiplier(1) + addr := testfactory.GetAddress(cctx.Keyring, "test") + + // create a transfer transaction + msg := banktypes.NewMsgSend( + addr, + testnode.RandomAddress().(sdk.AccAddress), + sdk.NewCoins(sdk.NewInt64Coin(appconsts.BondDenom, 10)), + ) + rawTx, err := txClient.Signer().CreateTx( + []sdk.Msg{msg}, + user.SetGasLimit(0), // set to 0 to mimic txClient behavior + user.SetFee(1), + ) + require.NoError(t, err) + + gasEstimationAPI := gasestimation.NewGasEstimatorClient(cctx.GRPCClient) + + // calculate the expected gas used + expectedGasEstimate, err := txClient.EstimateGas(cctx.GoContext(), []sdk.Msg{msg}) + require.NoError(t, err) + // calculate the actual gas used + actualGasEstimate, err := gasEstimationAPI.EstimateGasPriceAndUsage(cctx.GoContext(), &gasestimation.EstimateGasPriceAndUsageRequest{TxBytes: rawTx}) + require.NoError(t, err) + + assert.Equal(t, expectedGasEstimate, actualGasEstimate.EstimatedGasUsed) + + // create a PFB + blobSize := 100 + blobs := blobfactory.ManyRandBlobs(tmrand.NewRand(), blobSize) + pfbTx, _, err := txClient.Signer().CreatePayForBlobs( + "test", + blobs, + user.SetGasLimit(0), // set to 0 to mimic txClient behavior + user.SetFee(1), + ) + require.NoError(t, err) + pfbMsg, err := blobtypes.NewMsgPayForBlobs(addr.String(), appconsts.LatestVersion, blobs...) + require.NoError(t, err) + + // calculate the expected gas used + expectedGasEstimate, err = txClient.EstimateGas(cctx.GoContext(), []sdk.Msg{pfbMsg}) + require.NoError(t, err) + // calculate the actual gas used + actualGasEstimate, err = gasEstimationAPI.EstimateGasPriceAndUsage(cctx.GoContext(), &gasestimation.EstimateGasPriceAndUsageRequest{TxBytes: pfbTx}) + require.NoError(t, err) + + assert.Equal(t, expectedGasEstimate, actualGasEstimate.EstimatedGasUsed) +} diff --git a/buf.lock b/buf.lock index 2794629a37..054da2b8f9 100644 --- a/buf.lock +++ b/buf.lock @@ -1,15 +1,18 @@ # Generated by buf. DO NOT EDIT. -version: v1 +version: v1beta1 deps: - remote: buf.build owner: cosmos repository: cosmos-proto - commit: 1935555c206d4afb9e94615dfd0fad31 + commit: 04467658e59e44bbb22fe568206e1f70 + digest: shake256:73a640bd60e0c523b0f8237ff34eab67c45a38b64bbbde1d80224819d272dbf316ac183526bd245f994af6608b025f5130483d0133c5edd385531326b5990466 - remote: buf.build owner: cosmos repository: gogo-proto - commit: 34d970b699f84aa382f3c29773a60836 + commit: 88ef6483f90f478fb938c37dde52ece3 + digest: shake256:89c45df2aa11e0cff97b0d695436713db3d993d76792e9f8dc1ae90e6ab9a9bec55503d48ceedd6b86069ab07d3041b32001b2bfe0227fa725dd515ff381e5ba - remote: buf.build owner: googleapis repository: googleapis - commit: 463926e7ee924d46ad0a726e1cf4eacd + commit: e93e34f48be043dab55be31b4b47f458 + digest: shake256:93dbe51c27606999eef918360df509485a4d272e79aaed6d0016940379a9b06d316fc5228b7b50cca94bb310f34c5fc5955ce7474f655f0d0a224c4121dda3c1 diff --git a/buf.yaml b/buf.yaml index 4c13e62fc4..b3f77da7d8 100644 --- a/buf.yaml +++ b/buf.yaml @@ -5,6 +5,7 @@ deps: - buf.build/cosmos/cosmos-proto - buf.build/cosmos/gogo-proto - buf.build/googleapis/googleapis + build: roots: - proto diff --git a/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md b/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md index f90171747b..9026d36ebb 100644 --- a/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md +++ b/docs/architecture/adr-023-gas-used-and-gas-price-estimation.md @@ -6,7 +6,7 @@ ## Status -Proposed +Implemented ## Context diff --git a/proto/celestia/core/v1/gas_estimation/gas_estimator.proto b/proto/celestia/core/v1/gas_estimation/gas_estimator.proto new file mode 100644 index 0000000000..c01ffe64b6 --- /dev/null +++ b/proto/celestia/core/v1/gas_estimation/gas_estimator.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; +package celestia.core.v1.gas_estimation; + +import "google/api/annotations.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "github.com/celestiaorg/celestia-app/app/grpc/gasestimation"; + +// GasEstimator estimation service for gas price and gas used. +service GasEstimator { + // estimateGasPrice takes a transaction priority and estimates the gas price based + // on the gas prices of the transactions in the last five blocks. + // If no transaction is found in the last five blocks, return the network + // min gas price. + // It's up to the light client to set the gas price in this case + // to the minimum gas price set by that node. + rpc EstimateGasPrice(EstimateGasPriceRequest) returns (EstimateGasPriceResponse) {} + + // EstimateGasPriceAndUsage takes a transaction priority and a transaction bytes + // and estimates the gas price and the gas used for that transaction. + // The gas price estimation is based on the gas prices of the transactions in the last five blocks. + // If no transaction is found in the last five blocks, return the network + // min gas price. + // It's up to the light client to set the gas price in this case + // to the minimum gas price set by that node. + // The gas used is estimated using the state machine simulation. + rpc EstimateGasPriceAndUsage(EstimateGasPriceAndUsageRequest) returns (EstimateGasPriceAndUsageResponse) {} +} + +// TxPriority is the priority level of the requested gas price. +// The following priority levels are defined: +// - High Priority: The gas price is the price at the start of the top 10% of transactions’ gas prices from the last 5 blocks. +// - Medium Priority: The gas price is the mean of all gas prices from the last 5 blocks. +// - Low Priority: The gas price is the value at the end of the lowest 10% of gas prices from the last 5 blocks. +// - Unspecified Priority (default): This is equivalent to the Medium priority, using the mean of all gas prices from the last 5 blocks. +enum TxPriority { + // TX_PRIORITY_UNSPECIFIED none priority, the default priority level, which is equivalent to + // the TX_PRIORITY_MEDIUM priority. + TX_PRIORITY_UNSPECIFIED = 0; + // TX_PRIORITY_LOW low priority. + TX_PRIORITY_LOW = 1; + // TX_PRIORITY_MEDIUM medium priority. + TX_PRIORITY_MEDIUM = 2; + // TX_PRIORITY_HIGH high priority. + TX_PRIORITY_HIGH = 3; +} + +// EstimateGasPriceRequest the request to estimate the gas price of the network. +// Takes a priority enum to define the priority level. +message EstimateGasPriceRequest { + TxPriority tx_priority = 1; +} + +// EstimateGasPriceResponse the response of the gas price estimation. +message EstimateGasPriceResponse { + double estimated_gas_price = 1; +} + +// EstimateGasPriceAndUsageRequest the request to estimate the gas price of the network +// and also the gas used for the provided transaction. +message EstimateGasPriceAndUsageRequest { + TxPriority tx_priority = 1; + bytes tx_bytes = 2; +} + +// EstimateGasPriceAndUsageResponse the response of the gas price and used +// estimation. +message EstimateGasPriceAndUsageResponse { + double estimated_gas_price = 1; + uint64 estimated_gas_used = 2; +}