Skip to content

Commit

Permalink
feat: introduce cache layer for fee denom and decimals (#110)
Browse files Browse the repository at this point in the history
* loading fee requires executing evm call, so introduce cache layer and periodically fetch it

* decrease update interval

* introduce lock

* decrease fetch interval
  • Loading branch information
beer-1 authored Nov 13, 2024
1 parent 63aa650 commit 8be43a3
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 76 deletions.
75 changes: 72 additions & 3 deletions jsonrpc/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backend

import (
"context"
"fmt"
"sync"
"time"

Expand Down Expand Up @@ -39,6 +40,11 @@ type JSONRPCBackend struct {
txLookupCache *lru.Cache[common.Hash, *rpctypes.RPCTransaction]
receiptCache *lru.Cache[common.Hash, *coretypes.Receipt]

// fee cache
feeDenom string
feeDecimals uint8
feeMutex sync.RWMutex

mut sync.Mutex // mutex for accMuts
accMuts map[string]*AccMut

Expand All @@ -65,6 +71,7 @@ const (

// NewJSONRPCBackend creates a new JSONRPCBackend instance
func NewJSONRPCBackend(
ctx context.Context,
app *app.MinitiaApp,
logger log.Logger,
svrCtx *server.Context,
Expand All @@ -86,8 +93,7 @@ func NewJSONRPCBackend(
return nil, err
}

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

Expand All @@ -113,7 +119,70 @@ func NewJSONRPCBackend(
svrCtx: svrCtx,
clientCtx: clientCtx,
cfg: cfg,
}, nil
}

// start fee fetcher
go b.feeFetcher()

return b, nil
}

func (b *JSONRPCBackend) feeInfo() (string, uint8, error) {
b.feeMutex.RLock()
defer b.feeMutex.RUnlock()

if b.feeDenom == "" {
return "", 0, NewInternalError("jsonrpc is not ready")
}

return b.feeDenom, b.feeDecimals, nil
}

func (b *JSONRPCBackend) feeFetcher() {
fetcher := func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("feeFetcher panic: %v", r)
}
}()

queryCtx, err := b.getQueryCtx()
if err != nil {
return err
}

params, err := b.app.EVMKeeper.Params.Get(queryCtx)
if err != nil {
return err
}

feeDenom := params.FeeDenom
decimals, err := b.app.EVMKeeper.ERC20Keeper().GetDecimals(queryCtx, feeDenom)
if err != nil {
return err
}

b.feeMutex.Lock()
b.feeDenom = feeDenom
b.feeDecimals = decimals
b.feeMutex.Unlock()

return nil
}

ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
if err := fetcher(); err != nil {
b.logger.Error("failed to fetch fee", "err", err)
}
case <-b.ctx.Done():
return
}
}
}

type AccMut struct {
Expand Down
4 changes: 2 additions & 2 deletions jsonrpc/backend/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (b *JSONRPCBackend) GetBalance(address common.Address, blockNrOrHash rpc.Bl
return nil, err
}

feeDenom, decimals, err := b.feeDenomWithDecimals()
feeDenom, feeDecimals, err := b.feeInfo()
if err != nil {
return nil, err
}
Expand All @@ -35,7 +35,7 @@ func (b *JSONRPCBackend) GetBalance(address common.Address, blockNrOrHash rpc.Bl
return nil, err
}

return (*hexutil.Big)(types.ToEthersUint(decimals, balance.BigInt())), nil
return (*hexutil.Big)(types.ToEthersUint(feeDecimals, balance.BigInt())), nil
}

func (b *JSONRPCBackend) Call(args rpctypes.TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, overrides *rpctypes.StateOverride, blockOverrides *rpctypes.BlockOverrides) (hexutil.Bytes, error) {
Expand Down
54 changes: 15 additions & 39 deletions jsonrpc/backend/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (b *JSONRPCBackend) EstimateGas(args rpctypes.TransactionArgs, blockNrOrHas
return hexutil.Uint64(0), err
}

_, decimals, err := b.feeDenomWithDecimals()
_, feeDecimals, err := b.feeInfo()
if err != nil {
return hexutil.Uint64(0), err
}
Expand All @@ -49,14 +49,14 @@ func (b *JSONRPCBackend) EstimateGas(args rpctypes.TransactionArgs, blockNrOrHas
sdkMsgs = append(sdkMsgs, &types.MsgCreate{
Sender: sender,
Code: hexutil.Encode(args.GetData()),
Value: math.NewIntFromBigInt(types.FromEthersUnit(decimals, args.Value.ToInt())),
Value: math.NewIntFromBigInt(types.FromEthersUnit(feeDecimals, args.Value.ToInt())),
})
} else {
sdkMsgs = append(sdkMsgs, &types.MsgCall{
Sender: sender,
ContractAddr: args.To.Hex(),
Input: hexutil.Encode(args.GetData()),
Value: math.NewIntFromBigInt(types.FromEthersUnit(decimals, args.Value.ToInt())),
Value: math.NewIntFromBigInt(types.FromEthersUnit(feeDecimals, args.Value.ToInt())),
})
}

Expand Down Expand Up @@ -90,39 +90,6 @@ func (b *JSONRPCBackend) EstimateGas(args rpctypes.TransactionArgs, blockNrOrHas
return hexutil.Uint64(gasInfo.GasUsed), nil
}

func (b *JSONRPCBackend) feeDenom() (string, error) {
queryCtx, err := b.getQueryCtx()
if err != nil {
return "", err
}

params, err := b.app.EVMKeeper.Params.Get(queryCtx)
if err != nil {
return "", err
}

return params.FeeDenom, nil
}

func (b *JSONRPCBackend) feeDenomWithDecimals() (string, uint8, error) {
feeDenom, err := b.feeDenom()
if err != nil {
return "", 0, err
}

queryCtx, err := b.getQueryCtx()
if err != nil {
return "", 0, err
}

decimals, err := b.app.EVMKeeper.ERC20Keeper().GetDecimals(queryCtx, feeDenom)
if err != nil {
return "", 0, err
}

return feeDenom, decimals, nil
}

func (b *JSONRPCBackend) GasPrice() (*hexutil.Big, error) {
queryCtx, err := b.getQueryCtx()
if err != nil {
Expand All @@ -134,17 +101,26 @@ func (b *JSONRPCBackend) GasPrice() (*hexutil.Big, error) {
return nil, err
}

feeDenom, decimals, err := b.feeDenomWithDecimals()
feeDenom, feeDecimals, err := b.feeInfo()
if err != nil {
return nil, err
}

// Multiply by 1e9 to maintain precision during conversion
// This adds 9 decimal places to prevent truncation errors
const precisionMultiplier = 1e9

// multiply by 1e9 to prevent decimal drops
gasPrice := params.MinGasPrices.AmountOf(feeDenom).
MulTruncate(math.LegacyNewDec(1e9)).
MulTruncate(math.LegacyNewDec(precisionMultiplier)).
TruncateInt().BigInt()

return (*hexutil.Big)(types.ToEthersUint(decimals+9, gasPrice)), nil
// Verify the result is within safe bounds
if gasPrice.BitLen() > 256 {
return nil, NewInternalError("gas price overflow")
}

return (*hexutil.Big)(types.ToEthersUint(feeDecimals+9, gasPrice)), nil
}

func (b *JSONRPCBackend) MaxPriorityFeePerGas() (*hexutil.Big, error) {
Expand Down
14 changes: 7 additions & 7 deletions jsonrpc/jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func StartJSONRPC(
rpcServer := rpc.NewServer()
rpcServer.SetBatchLimits(jsonRPCConfig.BatchRequestLimit, jsonRPCConfig.BatchResponseMaxSize)

bkd, err := backend.NewJSONRPCBackend(app, logger, svrCtx, clientCtx, jsonRPCConfig)
bkd, err := backend.NewJSONRPCBackend(ctx, app, logger, svrCtx, clientCtx, jsonRPCConfig)
if err != nil {
return err
}
Expand All @@ -73,37 +73,37 @@ func StartJSONRPC(
{
Namespace: EthNamespace,
Version: apiVersion,
Service: ethns.NewEthAPI(logger, bkd),
Service: ethns.NewEthAPI(ctx, logger, bkd),
Public: true,
},
{
Namespace: EthNamespace,
Version: apiVersion,
Service: filters.NewFilterAPI(app, bkd, logger),
Service: filters.NewFilterAPI(ctx, app, bkd, logger),
Public: true,
},
{
Namespace: NetNamespace,
Version: apiVersion,
Service: netns.NewNetAPI(logger, bkd),
Service: netns.NewNetAPI(ctx, logger, bkd),
Public: true,
},
{
Namespace: Web3Namespace,
Version: apiVersion,
Service: web3ns.NewWeb3API(logger, bkd),
Service: web3ns.NewWeb3API(ctx, logger, bkd),
Public: true,
},
{
Namespace: TxPoolNamespace,
Version: apiVersion,
Service: txpoolns.NewTxPoolAPI(logger, bkd),
Service: txpoolns.NewTxPoolAPI(ctx, logger, bkd),
Public: true,
},
{
Namespace: CosmosNamespace,
Version: apiVersion,
Service: cosmosns.NewCosmosAPI(logger, bkd),
Service: cosmosns.NewCosmosAPI(ctx, logger, bkd),
Public: true,
},
}
Expand Down
4 changes: 2 additions & 2 deletions jsonrpc/namespaces/cosmos/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ type CosmosAPI struct {
}

// NewCosmosAPI creates an instance of the public ETH Web3 API.
func NewCosmosAPI(logger log.Logger, backend *backend.JSONRPCBackend) *CosmosAPI {
func NewCosmosAPI(ctx context.Context, logger log.Logger, backend *backend.JSONRPCBackend) *CosmosAPI {
api := &CosmosAPI{
ctx: context.TODO(),
ctx: ctx,
logger: logger.With("client", "json-rpc"),
backend: backend,
}
Expand Down
4 changes: 2 additions & 2 deletions jsonrpc/namespaces/eth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ type EthAPI struct {
}

// NewEthAPI creates an instance of the public ETH Web3 API.
func NewEthAPI(logger log.Logger, backend *backend.JSONRPCBackend) *EthAPI {
func NewEthAPI(ctx context.Context, logger log.Logger, backend *backend.JSONRPCBackend) *EthAPI {
api := &EthAPI{
ctx: context.TODO(),
ctx: ctx,
logger: logger.With("client", "json-rpc"),
backend: backend,
}
Expand Down
40 changes: 25 additions & 15 deletions jsonrpc/namespaces/eth/filters/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type filter struct {

// FilterAPI is the eth_ filter namespace API
type FilterAPI struct {
ctx context.Context

app *app.MinitiaApp
backend *backend.JSONRPCBackend

Expand All @@ -63,9 +65,11 @@ type FilterAPI struct {
}

// NewFiltersAPI returns a new instance
func NewFilterAPI(app *app.MinitiaApp, backend *backend.JSONRPCBackend, logger log.Logger) *FilterAPI {
func NewFilterAPI(ctx context.Context, app *app.MinitiaApp, backend *backend.JSONRPCBackend, logger log.Logger) *FilterAPI {
logger = logger.With("api", "filter")
api := &FilterAPI{
ctx: ctx,

app: app,
backend: backend,

Expand Down Expand Up @@ -98,23 +102,27 @@ func (api *FilterAPI) clearUnusedFilters() {

var toUninstall []*subscription
for {
<-ticker.C
api.filtersMut.Lock()
for id, f := range api.filters {
if time.Since(f.lastUsed) > timeout {
toUninstall = append(toUninstall, f.s)
delete(api.filters, id)
select {
case <-ticker.C:
api.filtersMut.Lock()
for id, f := range api.filters {
if time.Since(f.lastUsed) > timeout {
toUninstall = append(toUninstall, f.s)
delete(api.filters, id)
}
}
}
api.filtersMut.Unlock()
api.filtersMut.Unlock()

// Unsubscribes are processed outside the lock to avoid the following scenario:
// event loop attempts broadcasting events to still active filters while
// Unsubscribe is waiting for it to process the uninstall request.
for _, s := range toUninstall {
api.uninstallSubscription(s)
// Unsubscribes are processed outside the lock to avoid the following scenario:
// event loop attempts broadcasting events to still active filters while
// Unsubscribe is waiting for it to process the uninstall request.
for _, s := range toUninstall {
api.uninstallSubscription(s)
}
toUninstall = nil
case <-api.ctx.Done():
return
}
toUninstall = nil
}
}

Expand Down Expand Up @@ -155,6 +163,8 @@ func (api *FilterAPI) eventLoop() {
case s := <-api.uninstall:
delete(api.subscriptions, s.id)
close(s.err)
case <-api.ctx.Done():
return
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions jsonrpc/namespaces/net/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ type NetAPI struct {
}

// NewNetAPI creates a new net API instance
func NewNetAPI(logger log.Logger, backend *backend.JSONRPCBackend) *NetAPI {
func NewNetAPI(ctx context.Context, logger log.Logger, backend *backend.JSONRPCBackend) *NetAPI {
return &NetAPI{
ctx: context.TODO(),
ctx: ctx,
logger: logger,
backend: backend,
}
Expand Down
4 changes: 2 additions & 2 deletions jsonrpc/namespaces/txpool/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ type TxPoolAPI struct {
}

// NewTxPoolAPI creates a new txpool API instance.
func NewTxPoolAPI(logger log.Logger, backend *backend.JSONRPCBackend) *TxPoolAPI {
func NewTxPoolAPI(ctx context.Context, logger log.Logger, backend *backend.JSONRPCBackend) *TxPoolAPI {
return &TxPoolAPI{
ctx: context.TODO(),
ctx: ctx,
logger: logger,
backend: backend,
}
Expand Down
Loading

0 comments on commit 8be43a3

Please sign in to comment.