diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml index 24bdf27ed..77713c855 100644 --- a/api/doc/thor.yaml +++ b/api/doc/thor.yaml @@ -763,7 +763,7 @@ paths: description: | Retrieve historical fee data, including the gas used ratio per block. Will return only fees for accessible blocks. - Max accessible blocks = BestBlock - api-backtrace-limit-flag . + Max accessible blocks = BestBlock - api-backtrace-limit-flag. api-backtrace-limit defaults to 1000 blocks. parameters: - $ref: '#/components/parameters/BlockCountInQuery' @@ -782,6 +782,21 @@ paths: schema: type: string example: 'Invalid revision' + + /fees/priority: + get: + tags: + - Fees + summary: Suggest a priority fee for a transaction to be included in a block + description: | + This endpoint allows you to estimate the priority fee for a transaction to be included in a block. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetFeesPriorityResponse' components: schemas: @@ -1280,6 +1295,18 @@ components: - 0.75 - 0.22 + GetFeesPriorityResponse: + type: object + title: GetFeesPriorityResponse + allOf: + - properties: + maxPriorityFeePerGas: + type: string + format: hex + description: | + The suggested maximum priority fee per gas as an hexadecimal string. + example: '0x98' + TxMeta: title: TxMeta type: object diff --git a/api/fees/data.go b/api/fees/data.go index e37151c86..06bc523e8 100644 --- a/api/fees/data.go +++ b/api/fees/data.go @@ -6,19 +6,47 @@ package fees import ( + "container/heap" "math/big" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/vechain/thor/v2/cache" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/thor" + "github.com/vechain/thor/v2/tx" ) +const ( + priorityNumberOfTxsPerBlock = 3 + priorityPercentile = 60 +) + +// minPriorityHeap is a min-heap of priority fee values. +type minPriorityHeap []*big.Int + +func (h minPriorityHeap) Len() int { return len(h) } +func (h minPriorityHeap) Less(i, j int) bool { return h[i].Cmp(h[j]) < 0 } +func (h minPriorityHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *minPriorityHeap) Push(x interface{}) { + *h = append(*h, x.(*big.Int)) +} + +func (h *minPriorityHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} + type FeeCacheEntry struct { + parentBlockID thor.Bytes32 baseFee *hexutil.Big gasUsedRatio float64 - parentBlockID thor.Bytes32 + priorityFees *minPriorityHeap } + type FeesData struct { repo *chain.Repository cache *cache.PrioCache @@ -38,38 +66,85 @@ func getBaseFee(baseFee *big.Int) *hexutil.Big { return (*hexutil.Big)(big.NewInt(0)) } -// resolveRange resolves the base fees and gas used ratios for the given block range. +// resolveRange resolves the base fees, gas used ratios and priority fees for the given block range. // Assumes that the boundaries (newest block - block count) are correct and validated beforehand. -func (fd *FeesData) resolveRange(newestBlockSummary *chain.BlockSummary, blockCount uint32) (thor.Bytes32, []*hexutil.Big, []float64, error) { +func (fd *FeesData) resolveRange(newestBlockSummary *chain.BlockSummary, blockCount uint32) (thor.Bytes32, []*hexutil.Big, []float64, *minPriorityHeap, error) { newestBlockID := newestBlockSummary.Header.ID() baseFees := make([]*hexutil.Big, blockCount) gasUsedRatios := make([]float64, blockCount) + priorityFees := &minPriorityHeap{} + heap.Init(priorityFees) var oldestBlockID thor.Bytes32 for i := blockCount; i > 0; i-- { oldestBlockID = newestBlockID - fees, _, found := fd.cache.Get(newestBlockID) - if !found { - // retrieve from db + retro-populate cache - blockSummary, err := fd.repo.GetBlockSummary(newestBlockID) - if err != nil { - return thor.Bytes32{}, nil, nil, err - } - - header := blockSummary.Header - fees = &FeeCacheEntry{ - baseFee: getBaseFee(header.BaseFee()), - gasUsedRatio: float64(header.GasUsed()) / float64(header.GasLimit()), - parentBlockID: header.ParentID(), - } - fd.cache.Set(header.ID(), fees, float64(header.Number())) + fees, err := fd.getOrLoadFees(newestBlockID) + if err != nil { + return thor.Bytes32{}, nil, nil, nil, err } - baseFees[i-1] = fees.(*FeeCacheEntry).baseFee - gasUsedRatios[i-1] = fees.(*FeeCacheEntry).gasUsedRatio + baseFees[i-1] = fees.baseFee + gasUsedRatios[i-1] = fees.gasUsedRatio + fd.updatePriorityFees(priorityFees, fees.priorityFees, priorityNumberOfTxsPerBlock*int(blockCount)) + + newestBlockID = fees.parentBlockID + } + + return oldestBlockID, baseFees, gasUsedRatios, priorityFees, nil +} + +func (fd *FeesData) getOrLoadFees(blockID thor.Bytes32) (*FeeCacheEntry, error) { + fees, _, found := fd.cache.Get(blockID) + if found { + return fees.(*FeeCacheEntry), nil + } + + block, err := fd.repo.GetBlock(blockID) + if err != nil { + return nil, err + } + + header := block.Header() + transactions := block.Transactions() + + blockPriorityFees := &minPriorityHeap{} + heap.Init(blockPriorityFees) + + for _, tx := range transactions { + maxPriorityFeePerGas := fd.effectiveMaxPriorityFeePerGas(tx, header.BaseFee()) + fd.updatePriorityFees(blockPriorityFees, &minPriorityHeap{maxPriorityFeePerGas}, priorityNumberOfTxsPerBlock) + } + + fees = &FeeCacheEntry{ + baseFee: getBaseFee(header.BaseFee()), + gasUsedRatio: float64(header.GasUsed()) / float64(header.GasLimit()), + parentBlockID: header.ParentID(), + priorityFees: blockPriorityFees, + } + fd.cache.Set(header.ID(), fees, float64(header.Number())) + + return fees.(*FeeCacheEntry), nil +} + +func (fd *FeesData) effectiveMaxPriorityFeePerGas(tx *tx.Transaction, baseFee *big.Int) *big.Int { + if baseFee == nil { + return tx.MaxPriorityFeePerGas() + } + maxFeePerGas := tx.MaxFeePerGas() + maxFeePerGas.Sub(maxFeePerGas, baseFee) - newestBlockID = fees.(*FeeCacheEntry).parentBlockID + maxPriorityFeePerGas := tx.MaxPriorityFeePerGas() + if maxPriorityFeePerGas.Cmp(maxFeePerGas) < 0 { + return maxPriorityFeePerGas } + return maxPriorityFeePerGas +} - return oldestBlockID, baseFees, gasUsedRatios, nil +func (fd *FeesData) updatePriorityFees(priorityFees, newFees *minPriorityHeap, maxLen int) { + for _, fee := range *newFees { + heap.Push(priorityFees, fee) + if priorityFees.Len() > maxLen { + heap.Pop(priorityFees) + } + } } diff --git a/api/fees/fees.go b/api/fees/fees.go index f3834addd..217e0d291 100644 --- a/api/fees/fees.go +++ b/api/fees/fees.go @@ -7,9 +7,11 @@ package fees import ( "math" + "math/big" "net/http" "strconv" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/vechain/thor/v2/api/utils" @@ -17,6 +19,12 @@ import ( "github.com/vechain/thor/v2/chain" ) +const priorityNumberOfBlocks = 20 + +var ( + priorityMinPriorityFee = big.NewInt(2) +) + type Fees struct { data *FeesData bft bft.Committer @@ -86,7 +94,7 @@ func (f *Fees) handleGetFeesHistory(w http.ResponseWriter, req *http.Request) er return err } - oldestBlockRevision, baseFees, gasUsedRatios, err := f.data.resolveRange(newestBlockSummary, blockCount) + oldestBlockRevision, baseFees, gasUsedRatios, _, err := f.data.resolveRange(newestBlockSummary, blockCount) if err != nil { return err } @@ -98,10 +106,37 @@ func (f *Fees) handleGetFeesHistory(w http.ResponseWriter, req *http.Request) er }) } +func (f *Fees) handleGetPriority(w http.ResponseWriter, _ *http.Request) error { + bestBlockSummary := f.data.repo.BestBlockSummary() + blockCount := uint32(math.Min(float64(priorityNumberOfBlocks), float64(f.backtraceLimit))) + blockCount = uint32(math.Min(float64(blockCount), float64(bestBlockSummary.Header.Number()+1))) + + _, _, _, priorityFees, err := f.data.resolveRange(bestBlockSummary, blockCount) + if err != nil { + return err + } + + priorityFee := (*hexutil.Big)(priorityMinPriorityFee) + if priorityFees.Len() > 0 { + priorityFeeEntry := (*priorityFees)[(priorityFees.Len()-1)*priorityPercentile/100] + if priorityFeeEntry.Cmp(priorityMinPriorityFee) > 0 { + priorityFee = (*hexutil.Big)(priorityFeeEntry) + } + } + + return utils.WriteJSON(w, &FeesPriority{ + MaxPriorityFeePerGas: priorityFee, + }) +} + func (f *Fees) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() sub.Path("/history"). Methods(http.MethodGet). Name("GET /fees/history"). HandlerFunc(utils.WrapHandlerFunc(f.handleGetFeesHistory)) + sub.Path("/priority"). + Methods(http.MethodGet). + Name("GET /fees/priority"). + HandlerFunc(utils.WrapHandlerFunc(f.handleGetPriority)) } diff --git a/api/fees/fees_test.go b/api/fees/fees_test.go index a54de5d7f..cc7a36bd9 100644 --- a/api/fees/fees_test.go +++ b/api/fees/fees_test.go @@ -73,6 +73,7 @@ func TestFeesFixedSizeSameAsBacktrace(t *testing.T) { "getFeeHistoryMoreBlocksRequestedThanAvailable": getFeeHistoryMoreBlocksRequestedThanAvailable, "getFeeHistoryBlock0": getFeeHistoryBlock0, "getFeeHistoryBlockCount0": getFeeHistoryBlockCount0, + "getFeePriority": getFeePriority, } { t.Run(name, func(t *testing.T) { tt(t, tclient, bestchain) @@ -339,3 +340,20 @@ func getFeeHistoryBlockCount0(t *testing.T, tclient *thorclient.Client, bestchai require.NotNil(t, res) assert.Equal(t, "invalid blockCount, it should not be 0\n", string(res)) } + +func getFeePriority(t *testing.T, tclient *thorclient.Client, bestchain *chain.Chain) { + res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/fees/priority") + require.NoError(t, err) + require.Equal(t, 200, statusCode) + require.NotNil(t, res) + var feesPriority fees.FeesPriority + if err := json.Unmarshal(res, &feesPriority); err != nil { + t.Fatal(err) + } + + expectedFeesPriority := fees.FeesPriority{ + MaxPriorityFeePerGas: (*hexutil.Big)(big.NewInt(100)), + } + + assert.Equal(t, expectedFeesPriority, feesPriority) +} diff --git a/api/fees/types.go b/api/fees/types.go index e034535ec..55eecded9 100644 --- a/api/fees/types.go +++ b/api/fees/types.go @@ -15,3 +15,7 @@ type FeesHistory struct { BaseFees []*hexutil.Big `json:"baseFees"` GasUsedRatios []float64 `json:"gasUsedRatios"` } + +type FeesPriority struct { + MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` +}