Skip to content

Commit

Permalink
support fee history
Browse files Browse the repository at this point in the history
  • Loading branch information
beer-1 committed Sep 6, 2024
1 parent e076bc5 commit 994d3f4
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 80 deletions.
2 changes: 1 addition & 1 deletion jsonrpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The ETH JSON-RPC (Remote Procedure Call) is a protocol that allows clients to in
| eth | eth_hashrate | 🚫 | Returns the number of hashes per second that the node is mining with. |
| eth | eth_gasPrice || Returns the current price per gas in wei. |
| eth | eth_maxPriorityFeePerGas || Returns the max priority fee per gas. |
| eth | eth_feeHistory | 🚫 | Returns the collection of historical gas information. |
| eth | eth_feeHistory | | Returns the collection of historical gas information. |
| eth | eth_accounts || Returns a list of addresses owned by the client. |
| eth | eth_blockNumber || Returns the number of the most recent block. |
| eth | eth_getBalance || Returns the balance of the account of given address. |
Expand Down
13 changes: 10 additions & 3 deletions jsonrpc/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type JSONRPCBackend struct {
app *app.MinitiaApp
logger log.Logger

queuedTxs *lrucache.Cache[string, []byte]
queuedTxs *lrucache.Cache[string, []byte]
historyCache *lrucache.Cache[cacheKey, processedFees]

mut sync.Mutex // mutex for accMuts
accMuts map[string]*AccMut
Expand All @@ -42,14 +43,20 @@ func NewJSONRPCBackend(
if err != nil {
return nil, err
}
historyCache, err := lrucache.New[cacheKey, processedFees](2048)
if err != nil {
return nil, err
}

ctx := context.Background()
return &JSONRPCBackend{
app: app,
logger: logger,

queuedTxs: queuedTxs,
accMuts: make(map[string]*AccMut),
queuedTxs: queuedTxs,
historyCache: historyCache,

accMuts: make(map[string]*AccMut),

ctx: ctx,
svrCtx: svrCtx,
Expand Down
7 changes: 7 additions & 0 deletions jsonrpc/backend/errors.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package backend

import "errors"

// TxIndexingError is an API error that indicates the transaction indexing is not
// fully finished yet with JSON error code and a binary data blob.
type TxIndexingError struct{}
Expand Down Expand Up @@ -37,3 +39,8 @@ func (e *InternalError) ErrorCode() int {
// Internal JSON-RPC error
return -32603
}

var (
errInvalidPercentile = errors.New("invalid reward percentile")
errRequestBeyondHead = errors.New("request beyond head block")
)
286 changes: 286 additions & 0 deletions jsonrpc/backend/feehistory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package backend

import (
"encoding/binary"
"fmt"
"math"
"math/big"
"slices"
"sync/atomic"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
coretypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"

rpctypes "github.com/initia-labs/minievm/jsonrpc/types"
evmtypes "github.com/initia-labs/minievm/x/evm/types"
)

const (
// maxBlockFetchers is the max number of goroutines to spin up to pull blocks
// for the fee history calculation (mostly relevant for LES).
maxBlockFetchers = 4
// maxQueryLimit is the max number of requested percentiles.
maxQueryLimit = 100
)

// blockFees represents a single block for processing
type blockFees struct {
// set by the caller
blockNumber uint64
header *coretypes.Header
txs coretypes.Transactions
receipts coretypes.Receipts
// filled by processBlock
results processedFees
err error
}

type cacheKey struct {
number uint64
percentiles string
}

// processedFees contains the results of a processed block.
type processedFees struct {
reward []*big.Int
baseFee, nextBaseFee *big.Int
gasUsedRatio float64
blobGasUsedRatio float64
blobBaseFee, nextBlobBaseFee *big.Int
}

// txGasAndReward is sorted in ascending order based on reward
type txGasAndReward struct {
gasUsed uint64
reward *big.Int
}

// FeeHistory returns data relevant for fee estimation based on the specified range of blocks.
// The range can be specified either with absolute block numbers or ending with the latest
// or pending block. Backends may or may not support gathering data from the pending block
// or blocks older than a certain age (specified in maxHistory). The first block of the
// actually processed range is returned to avoid ambiguity when parts of the requested range
// are not available or when the head has changed during processing this request.
// Five arrays are returned based on the processed blocks:
// - reward: the requested percentiles of effective priority fees per gas of transactions in each
// block, sorted in ascending order and weighted by gas used.
// - baseFee: base fee per gas in the given block
// - gasUsedRatio: gasUsed/gasLimit in the given block
// - blobBaseFee: the blob base fee per gas in the given block
// - blobGasUsedRatio: blobGasUsed/blobGasLimit in the given block
//
// Note: baseFee and blobBaseFee both include the next block after the newest of the returned range,
// because this value can be derived from the newest block.
func (b *JSONRPCBackend) FeeHistory(blocks uint64, unresolvedLastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) {
if blocks < 1 {
return common.Big0, nil, nil, nil, nil, nil, nil
}
maxFeeHistory := uint64(b.cfg.FeeHistoryMaxHeaders)
if len(rewardPercentiles) != 0 {
maxFeeHistory = uint64(b.cfg.FeeHistoryMaxBlocks)
}
if len(rewardPercentiles) > maxQueryLimit {
return common.Big0, nil, nil, nil, nil, nil, fmt.Errorf("%w: over the query limit %d", errInvalidPercentile, maxQueryLimit)
}
if blocks > maxFeeHistory {
b.logger.Warn("Sanitizing fee history length", "requested", blocks, "truncated", maxFeeHistory)
blocks = maxFeeHistory
}
for i, p := range rewardPercentiles {
if p < 0 || p > 100 {
return common.Big0, nil, nil, nil, nil, nil, fmt.Errorf("%w: %f", errInvalidPercentile, p)
}
if i > 0 && p <= rewardPercentiles[i-1] {
return common.Big0, nil, nil, nil, nil, nil, fmt.Errorf("%w: #%d:%f >= #%d:%f", errInvalidPercentile, i-1, rewardPercentiles[i-1], i, p)
}
}
lastBlock, blocks, err := b.resolveBlockRange(unresolvedLastBlock, blocks)
if err != nil || blocks == 0 {
return common.Big0, nil, nil, nil, nil, nil, err
}
oldestBlock := lastBlock + 1 - blocks

var next atomic.Uint64
next.Store(oldestBlock)
results := make(chan *blockFees, blocks)

percentileKey := make([]byte, 8*len(rewardPercentiles))
for i, p := range rewardPercentiles {
binary.LittleEndian.PutUint64(percentileKey[i*8:(i+1)*8], math.Float64bits(p))
}
for i := 0; i < maxBlockFetchers && i < int(blocks); i++ {
go func() {
for {
// Retrieve the next block number to fetch with this goroutine
blockNumber := next.Add(1) - 1
if blockNumber > lastBlock {
return
}

fees := &blockFees{blockNumber: blockNumber}
cacheKey := cacheKey{number: blockNumber, percentiles: string(percentileKey)}

if p, ok := b.historyCache.Get(cacheKey); ok {
fees.results = p
results <- fees
} else {
fees.header, fees.err = b.GetHeaderByNumber(rpc.BlockNumber(blockNumber))
if len(rewardPercentiles) != 0 && fees.err == nil {
fees.receipts, fees.err = b.getBLockReceipts(blockNumber)
if fees.err == nil {
var txs []*rpctypes.RPCTransaction
txs, fees.err = b.getBlockTransactions(blockNumber)
if fees.err == nil {
for _, tx := range txs {
fees.txs = append(fees.txs, tx.ToTransaction())
}
}
}
}
if fees.header != nil && fees.err == nil {
b.processBlock(fees, rewardPercentiles)
if fees.err == nil {
b.historyCache.Add(cacheKey, fees.results)
}
}
// send to results even if empty to guarantee that blocks items are sent in total
results <- fees
}

}
}()
}
var (
reward = make([][]*big.Int, blocks)
baseFee = make([]*big.Int, blocks+1)
gasUsedRatio = make([]float64, blocks)
blobGasUsedRatio = make([]float64, blocks)
blobBaseFee = make([]*big.Int, blocks+1)
firstMissing = blocks
)
for ; blocks > 0; blocks-- {
fees := <-results
if fees.err != nil {
return common.Big0, nil, nil, nil, nil, nil, fees.err
}
i := fees.blockNumber - oldestBlock
if fees.results.baseFee != nil {
reward[i], baseFee[i], baseFee[i+1], gasUsedRatio[i] = fees.results.reward, fees.results.baseFee, fees.results.nextBaseFee, fees.results.gasUsedRatio
blobGasUsedRatio[i], blobBaseFee[i], blobBaseFee[i+1] = fees.results.blobGasUsedRatio, fees.results.blobBaseFee, fees.results.nextBlobBaseFee
} else {
// getting no block and no error means we are requesting into the future (might happen because of a reorg)
if i < firstMissing {
firstMissing = i
}
}
}
if firstMissing == 0 {
return common.Big0, nil, nil, nil, nil, nil, nil
}
if len(rewardPercentiles) != 0 {
reward = reward[:firstMissing]
} else {
reward = nil
}
baseFee, gasUsedRatio = baseFee[:firstMissing+1], gasUsedRatio[:firstMissing]
blobBaseFee, blobGasUsedRatio = blobBaseFee[:firstMissing+1], blobGasUsedRatio[:firstMissing]
return new(big.Int).SetUint64(oldestBlock), reward, baseFee, gasUsedRatio, blobBaseFee, blobGasUsedRatio, nil
}

func (b *JSONRPCBackend) resolveBlockRange(reqEnd rpc.BlockNumber, blocks uint64) (uint64, uint64, error) {
var (
headBlock *coretypes.Header
err error
)

// Get the chain's current head.
if headBlock, err = b.GetHeaderByNumber(rpc.LatestBlockNumber); err != nil {
return 0, 0, err
}
head := rpc.BlockNumber(headBlock.Number.Uint64())

// Fail if request block is beyond the chain's current head.
if head < reqEnd {
return 0, 0, fmt.Errorf("%w: requested %d, head %d", errRequestBeyondHead, reqEnd, head)
}

// return latest block if requested block is special
if reqEnd < 0 {
reqEnd = rpc.BlockNumber(headBlock.Number.Uint64())
}

// If there are no blocks to return, short circuit.
if blocks == 0 {
return 0, 0, nil
}
// Ensure not trying to retrieve before genesis.
if uint64(reqEnd+1) < blocks {
blocks = uint64(reqEnd + 1)
}
return uint64(reqEnd), blocks, nil
}

// processBlock takes a blockFees structure with the blockNumber, the header and optionally
// the block field filled in, retrieves the block from the backend if not present yet and
// fills in the rest of the fields.
func (b *JSONRPCBackend) processBlock(bf *blockFees, percentiles []float64) {
ctx, err := b.app.CreateQueryContext(0, false)
if err != nil {
bf.err = err
return
}

config := evmtypes.DefaultChainConfig(ctx)

// Fill in base fee and next base fee.
if bf.results.baseFee = bf.header.BaseFee; bf.results.baseFee == nil {
bf.results.baseFee = new(big.Int)
}
bf.results.nextBaseFee = eip1559.CalcBaseFee(config, bf.header)
bf.results.blobBaseFee = new(big.Int)
bf.results.nextBlobBaseFee = new(big.Int)

// Compute gas used ratio for normal and blob gas.
bf.results.gasUsedRatio = float64(bf.header.GasUsed) / float64(bf.header.GasLimit)

if len(percentiles) == 0 {
// rewards were not requested, return null
return
}
if bf.receipts == nil && len(bf.txs) == 0 {
b.logger.Error("Block or receipts are missing while reward percentiles are requested")
return
}

bf.results.reward = make([]*big.Int, len(percentiles))
if len(bf.txs) == 0 {
// return an all zero row if there are no transactions to gather data from
for i := range bf.results.reward {
bf.results.reward[i] = new(big.Int)
}
return
}

sorter := make([]txGasAndReward, len(bf.txs))
for i, tx := range bf.txs {
reward, _ := tx.EffectiveGasTip(bf.header.BaseFee)
sorter[i] = txGasAndReward{gasUsed: bf.receipts[i].GasUsed, reward: reward}
}
slices.SortStableFunc(sorter, func(a, b txGasAndReward) int {
return a.reward.Cmp(b.reward)
})

var txIndex int
sumGasUsed := sorter[0].gasUsed

for i, p := range percentiles {
thresholdGasUsed := uint64(float64(bf.header.GasUsed) * p / 100)
for sumGasUsed < thresholdGasUsed && txIndex < len(bf.txs)-1 {
txIndex++
sumGasUsed += sorter[txIndex].gasUsed
}
bf.results.reward[i] = sorter[txIndex].reward
}
}
4 changes: 2 additions & 2 deletions jsonrpc/backend/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ func (b *JSONRPCBackend) getBlockTransactions(blockNumber uint64) ([]*rpctypes.R
return txs, nil
}

func (b *JSONRPCBackend) getBlockRecepts(blockNumber uint64) ([]*coretypes.Receipt, error) {
func (b *JSONRPCBackend) getBLockReceipts(blockNumber uint64) ([]*coretypes.Receipt, error) {
queryCtx, err := b.getQueryCtx()
if err != nil {
return nil, err
Expand Down Expand Up @@ -405,7 +405,7 @@ func (b *JSONRPCBackend) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc
return nil, err
}

receipts, err := b.getBlockRecepts(blockNumber)
receipts, err := b.getBLockReceipts(blockNumber)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 994d3f4

Please sign in to comment.