Skip to content
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

feat: fees/priority endpoint #984

Merged
merged 19 commits into from
Feb 25, 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
29 changes: 28 additions & 1 deletion api/doc/thor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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:
Expand Down Expand Up @@ -1276,6 +1291,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
Expand Down
119 changes: 97 additions & 22 deletions api/fees/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
}
37 changes: 36 additions & 1 deletion api/fees/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,24 @@ 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"
"github.com/vechain/thor/v2/bft"
"github.com/vechain/thor/v2/chain"
)

const priorityNumberOfBlocks = 20

var (
priorityMinPriorityFee = big.NewInt(2)
)

type Fees struct {
data *FeesData
bft bft.Committer
Expand Down Expand Up @@ -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
}
Expand All @@ -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))
}
18 changes: 18 additions & 0 deletions api/fees/fees_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
4 changes: 4 additions & 0 deletions api/fees/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ type FeesHistory struct {
BaseFees []*hexutil.Big `json:"baseFees"`
GasUsedRatios []float64 `json:"gasUsedRatios"`
}

type FeesPriority struct {
MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"`
}
Loading