Commit c2f92c2f authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2029 from ethereum-optimism/inphi/proxyd-blockcache

go/proxyd: Cache block-dependent RPCs
parents e8ea9654 19f5659f
---
'@eth-optimism/proxyd': minor
---
proxyd: Cache block-dependent RPCs
...@@ -2,7 +2,6 @@ package proxyd ...@@ -2,7 +2,6 @@ package proxyd
import ( import (
"context" "context"
"encoding/json"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang/snappy" "github.com/golang/snappy"
...@@ -14,10 +13,9 @@ type Cache interface { ...@@ -14,10 +13,9 @@ type Cache interface {
Put(ctx context.Context, key string, value string) error Put(ctx context.Context, key string, value string) error
} }
// assuming an average RPCRes size of 3 KB
const ( const (
memoryCacheLimit = 4096 // assuming an average RPCRes size of 3 KB
numBlockConfirmations = 50 memoryCacheLimit = 4096
) )
type cache struct { type cache struct {
...@@ -76,7 +74,36 @@ func (c *redisCache) Put(ctx context.Context, key string, value string) error { ...@@ -76,7 +74,36 @@ func (c *redisCache) Put(ctx context.Context, key string, value string) error {
return err return err
} }
type cacheWithCompression struct {
cache Cache
}
func newCacheWithCompression(cache Cache) *cacheWithCompression {
return &cacheWithCompression{cache}
}
func (c *cacheWithCompression) Get(ctx context.Context, key string) (string, error) {
encodedVal, err := c.cache.Get(ctx, key)
if err != nil {
return "", err
}
if encodedVal == "" {
return "", nil
}
val, err := snappy.Decode(nil, []byte(encodedVal))
if err != nil {
return "", err
}
return string(val), nil
}
func (c *cacheWithCompression) Put(ctx context.Context, key string, value string) error {
encodedVal := snappy.Encode(nil, []byte(value))
return c.cache.Put(ctx, key, string(encodedVal))
}
type GetLatestBlockNumFn func(ctx context.Context) (uint64, error) type GetLatestBlockNumFn func(ctx context.Context) (uint64, error)
type GetLatestGasPriceFn func(ctx context.Context) (uint64, error)
type RPCCache interface { type RPCCache interface {
GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error)
...@@ -84,19 +111,24 @@ type RPCCache interface { ...@@ -84,19 +111,24 @@ type RPCCache interface {
} }
type rpcCache struct { type rpcCache struct {
cache Cache cache Cache
getLatestBlockNumFn GetLatestBlockNumFn handlers map[string]RPCMethodHandler
handlers map[string]RPCMethodHandler
} }
func newRPCCache(cache Cache, getLatestBlockNumFn GetLatestBlockNumFn) RPCCache { func newRPCCache(cache Cache, getLatestBlockNumFn GetLatestBlockNumFn, getLatestGasPriceFn GetLatestGasPriceFn, numBlockConfirmations int) RPCCache {
handlers := map[string]RPCMethodHandler{ handlers := map[string]RPCMethodHandler{
"eth_chainId": &StaticRPCMethodHandler{"eth_chainId"}, "eth_chainId": &StaticMethodHandler{},
"net_version": &StaticRPCMethodHandler{"net_version"}, "net_version": &StaticMethodHandler{},
"eth_getBlockByNumber": &EthGetBlockByNumberMethod{getLatestBlockNumFn}, "eth_getBlockByNumber": &EthGetBlockByNumberMethodHandler{cache, getLatestBlockNumFn, numBlockConfirmations},
"eth_getBlockRange": &EthGetBlockRangeMethod{getLatestBlockNumFn}, "eth_getBlockRange": &EthGetBlockRangeMethodHandler{cache, getLatestBlockNumFn, numBlockConfirmations},
"eth_blockNumber": &EthBlockNumberMethodHandler{getLatestBlockNumFn},
"eth_gasPrice": &EthGasPriceMethodHandler{getLatestGasPriceFn},
"eth_call": &EthCallMethodHandler{cache, getLatestBlockNumFn, numBlockConfirmations},
}
return &rpcCache{
cache: cache,
handlers: handlers,
} }
return &rpcCache{cache: cache, getLatestBlockNumFn: getLatestBlockNumFn, handlers: handlers}
} }
func (c *rpcCache) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) { func (c *rpcCache) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) {
...@@ -104,37 +136,15 @@ func (c *rpcCache) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) { ...@@ -104,37 +136,15 @@ func (c *rpcCache) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) {
if handler == nil { if handler == nil {
return nil, nil return nil, nil
} }
cacheable, err := handler.IsCacheable(req) res, err := handler.GetRPCMethod(ctx, req)
if err != nil { if res != nil {
return nil, err if res == nil {
} RecordCacheMiss(req.Method)
if !cacheable { } else {
RecordCacheMiss(req.Method) RecordCacheHit(req.Method)
return nil, nil }
}
key := handler.CacheKey(req)
encodedVal, err := c.cache.Get(ctx, key)
if err != nil {
return nil, err
}
if encodedVal == "" {
RecordCacheMiss(req.Method)
return nil, nil
} }
val, err := snappy.Decode(nil, []byte(encodedVal)) return res, err
if err != nil {
return nil, err
}
RecordCacheHit(req.Method)
res := new(RPCRes)
err = json.Unmarshal(val, res)
if err != nil {
return nil, err
}
res.ID = req.ID
return res, nil
} }
func (c *rpcCache) PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error { func (c *rpcCache) PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error {
...@@ -142,23 +152,5 @@ func (c *rpcCache) PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error { ...@@ -142,23 +152,5 @@ func (c *rpcCache) PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error {
if handler == nil { if handler == nil {
return nil return nil
} }
cacheable, err := handler.IsCacheable(req) return handler.PutRPCMethod(ctx, req, res)
if err != nil {
return err
}
if !cacheable {
return nil
}
requiresConfirmations, err := handler.RequiresUnconfirmedBlocks(ctx, req)
if err != nil {
return err
}
if requiresConfirmations {
return nil
}
key := handler.CacheKey(req)
val := mustMarshalJSON(res)
encodedVal := snappy.Encode(nil, val)
return c.cache.Put(ctx, key, string(encodedVal))
} }
...@@ -9,14 +9,16 @@ import ( ...@@ -9,14 +9,16 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestRPCCacheWhitelist(t *testing.T) { const numBlockConfirmations = 10
func TestRPCCacheImmutableRPCs(t *testing.T) {
const blockHead = math.MaxUint64 const blockHead = math.MaxUint64
ctx := context.Background() ctx := context.Background()
fn := func(ctx context.Context) (uint64, error) { getBlockNum := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn) cache := newRPCCache(newMemoryCache(), getBlockNum, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -120,6 +122,82 @@ func TestRPCCacheWhitelist(t *testing.T) { ...@@ -120,6 +122,82 @@ func TestRPCCacheWhitelist(t *testing.T) {
} }
} }
func TestRPCCacheBlockNumber(t *testing.T) {
var blockHead uint64 = 0x1000
var gasPrice uint64 = 0x100
ctx := context.Background()
ID := []byte(strconv.Itoa(1))
getGasPrice := func(ctx context.Context) (uint64, error) {
return gasPrice, nil
}
getBlockNum := func(ctx context.Context) (uint64, error) {
return blockHead, nil
}
cache := newRPCCache(newMemoryCache(), getBlockNum, getGasPrice, numBlockConfirmations)
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_blockNumber",
ID: ID,
}
res := &RPCRes{
JSONRPC: "2.0",
Result: `0x1000`,
ID: ID,
}
err := cache.PutRPC(ctx, req, res)
require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Equal(t, res, cachedRes)
blockHead = 0x1001
cachedRes, err = cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Equal(t, &RPCRes{JSONRPC: "2.0", Result: `0x1001`, ID: ID}, cachedRes)
}
func TestRPCCacheGasPrice(t *testing.T) {
var blockHead uint64 = 0x1000
var gasPrice uint64 = 0x100
ctx := context.Background()
ID := []byte(strconv.Itoa(1))
getGasPrice := func(ctx context.Context) (uint64, error) {
return gasPrice, nil
}
getBlockNum := func(ctx context.Context) (uint64, error) {
return blockHead, nil
}
cache := newRPCCache(newMemoryCache(), getBlockNum, getGasPrice, numBlockConfirmations)
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_gasPrice",
ID: ID,
}
res := &RPCRes{
JSONRPC: "2.0",
Result: `0x100`,
ID: ID,
}
err := cache.PutRPC(ctx, req, res)
require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Equal(t, res, cachedRes)
gasPrice = 0x101
cachedRes, err = cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Equal(t, &RPCRes{JSONRPC: "2.0", Result: `0x101`, ID: ID}, cachedRes)
}
func TestRPCCacheUnsupportedMethod(t *testing.T) { func TestRPCCacheUnsupportedMethod(t *testing.T) {
const blockHead = math.MaxUint64 const blockHead = math.MaxUint64
ctx := context.Background() ctx := context.Background()
...@@ -127,17 +205,17 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) { ...@@ -127,17 +205,17 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) {
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
req := &RPCReq{ req := &RPCReq{
JSONRPC: "2.0", JSONRPC: "2.0",
Method: "eth_blockNumber", Method: "eth_syncing",
ID: ID, ID: ID,
} }
res := &RPCRes{ res := &RPCRes{
JSONRPC: "2.0", JSONRPC: "2.0",
Result: `0x1000`, Result: false,
ID: ID, ID: ID,
} }
...@@ -149,6 +227,62 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) { ...@@ -149,6 +227,62 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) {
require.Nil(t, cachedRes) require.Nil(t, cachedRes)
} }
func TestRPCCacheEthGetBlockByNumber(t *testing.T) {
ctx := context.Background()
var blockHead uint64
fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil
}
makeCache := func() RPCCache { return newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations) }
ID := []byte(strconv.Itoa(1))
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockByNumber",
Params: []byte(`["0xa", false]`),
ID: ID,
}
res := &RPCRes{
JSONRPC: "2.0",
Result: `{"difficulty": "0x1", "number": "0x1"}`,
ID: ID,
}
req2 := &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockByNumber",
Params: []byte(`["0xb", false]`),
ID: ID,
}
res2 := &RPCRes{
JSONRPC: "2.0",
Result: `{"difficulty": "0x2", "number": "0x2"}`,
ID: ID,
}
t.Run("set multiple finalized blocks", func(t *testing.T) {
blockHead = 100
cache := makeCache()
require.NoError(t, cache.PutRPC(ctx, req, res))
require.NoError(t, cache.PutRPC(ctx, req2, res2))
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Equal(t, res, cachedRes)
cachedRes, err = cache.GetRPC(ctx, req2)
require.NoError(t, err)
require.Equal(t, res2, cachedRes)
})
t.Run("unconfirmed block", func(t *testing.T) {
blockHead = 0xc
cache := makeCache()
require.NoError(t, cache.PutRPC(ctx, req, res))
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Nil(t, cachedRes)
})
}
func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) { func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) {
ctx := context.Background() ctx := context.Background()
...@@ -156,7 +290,7 @@ func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) { ...@@ -156,7 +290,7 @@ func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) {
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -164,20 +298,6 @@ func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) { ...@@ -164,20 +298,6 @@ func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) {
res *RPCRes res *RPCRes
name string name string
}{ }{
{
req: &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockByNumber",
Params: []byte(`["0x1", false]`),
ID: ID,
},
res: &RPCRes{
JSONRPC: "2.0",
Result: `{"difficulty": "0x1", "number": "0x1"}`,
ID: ID,
},
name: "recent block num",
},
{ {
req: &RPCReq{ req: &RPCReq{
JSONRPC: "2.0", JSONRPC: "2.0",
...@@ -227,7 +347,7 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) { ...@@ -227,7 +347,7 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) {
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
req := &RPCReq{ req := &RPCReq{
...@@ -250,6 +370,56 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) { ...@@ -250,6 +370,56 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) {
require.Nil(t, cachedRes) require.Nil(t, cachedRes)
} }
func TestRPCCacheEthGetBlockRange(t *testing.T) {
ctx := context.Background()
var blockHead uint64
fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil
}
makeCache := func() RPCCache { return newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations) }
ID := []byte(strconv.Itoa(1))
t.Run("finalized block", func(t *testing.T) {
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockRange",
Params: []byte(`["0x1", "0x10", false]`),
ID: ID,
}
res := &RPCRes{
JSONRPC: "2.0",
Result: `[{"number": "0x1"}, {"number": "0x10"}]`,
ID: ID,
}
blockHead = 0x1000
cache := makeCache()
require.NoError(t, cache.PutRPC(ctx, req, res))
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Equal(t, res, cachedRes)
})
t.Run("unconfirmed block", func(t *testing.T) {
cache := makeCache()
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockRange",
Params: []byte(`["0x1", "0x1000", false]`),
ID: ID,
}
res := &RPCRes{
JSONRPC: "2.0",
Result: `[{"number": "0x1"}, {"number": "0x2"}]`,
ID: ID,
}
require.NoError(t, cache.PutRPC(ctx, req, res))
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Nil(t, cachedRes)
})
}
func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) { func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) {
ctx := context.Background() ctx := context.Background()
...@@ -257,7 +427,7 @@ func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) { ...@@ -257,7 +427,7 @@ func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) {
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -265,20 +435,6 @@ func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) { ...@@ -265,20 +435,6 @@ func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) {
res *RPCRes res *RPCRes
name string name string
}{ }{
{
req: &RPCReq{
JSONRPC: "2.0",
Method: "eth_getBlockRange",
Params: []byte(`["0x1", "0x1000", false]`),
ID: ID,
},
res: &RPCRes{
JSONRPC: "2.0",
Result: `[{"number": "0x1"}, {"number": "0x2"}]`,
ID: ID,
},
name: "recent block num",
},
{ {
req: &RPCReq{ req: &RPCReq{
JSONRPC: "2.0", JSONRPC: "2.0",
...@@ -342,7 +498,7 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) { ...@@ -342,7 +498,7 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) {
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -391,3 +547,76 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) { ...@@ -391,3 +547,76 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) {
}) })
} }
} }
func TestRPCCacheEthCall(t *testing.T) {
ctx := context.Background()
var blockHead uint64
fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil
}
makeCache := func() RPCCache { return newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations) }
ID := []byte(strconv.Itoa(1))
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_call",
Params: []byte(`[{"to": "0xDEADBEEF", "data": "0x1"}, "0x10"]`),
ID: ID,
}
res := &RPCRes{
JSONRPC: "2.0",
Result: `0x0`,
ID: ID,
}
t.Run("finalized block", func(t *testing.T) {
blockHead = 0x100
cache := makeCache()
err := cache.PutRPC(ctx, req, res)
require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Equal(t, res, cachedRes)
})
t.Run("unconfirmed block", func(t *testing.T) {
blockHead = 0x10
cache := makeCache()
require.NoError(t, cache.PutRPC(ctx, req, res))
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Nil(t, cachedRes)
})
t.Run("latest block", func(t *testing.T) {
blockHead = 0x100
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_call",
Params: []byte(`[{"to": "0xDEADBEEF", "data": "0x1"}, "latest"]`),
ID: ID,
}
cache := makeCache()
require.NoError(t, cache.PutRPC(ctx, req, res))
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Nil(t, cachedRes)
})
t.Run("pending block", func(t *testing.T) {
blockHead = 0x100
req := &RPCReq{
JSONRPC: "2.0",
Method: "eth_call",
Params: []byte(`[{"to": "0xDEADBEEF", "data": "0x1"}, "pending"]`),
ID: ID,
}
cache := makeCache()
require.NoError(t, cache.PutRPC(ctx, req, res))
cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
require.Nil(t, cachedRes)
})
}
...@@ -15,8 +15,9 @@ type ServerConfig struct { ...@@ -15,8 +15,9 @@ type ServerConfig struct {
} }
type CacheConfig struct { type CacheConfig struct {
Enabled bool `toml:"enabled"` Enabled bool `toml:"enabled"`
BlockSyncRPCURL string `toml:"block_sync_rpc_url"` BlockSyncRPCURL string `toml:"block_sync_rpc_url"`
NumBlockConfirmations int `toml:"num_block_confirmations"`
} }
type RedisConfig struct { type RedisConfig struct {
......
...@@ -12,7 +12,7 @@ import ( ...@@ -12,7 +12,7 @@ import (
) )
const ( const (
goodResponse = `{"jsonrpc": "2.0", "result": "hello", "id": 999}` goodResponse = `{"jsonrpc": "2.0", "result": "hello", "id": 999}`
noBackendsResponse = `{"error":{"code":-32011,"message":"no backends available for method"},"id":999,"jsonrpc":"2.0"}` noBackendsResponse = `{"error":{"code":-32011,"message":"no backends available for method"},"id":999,"jsonrpc":"2.0"}`
) )
......
package proxyd
import (
"context"
"sync"
"time"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
const blockHeadSyncPeriod = 1 * time.Second
type LatestBlockHead struct {
url string
client *ethclient.Client
quit chan struct{}
done chan struct{}
mutex sync.RWMutex
blockNum uint64
}
func newLatestBlockHead(url string) (*LatestBlockHead, error) {
client, err := ethclient.Dial(url)
if err != nil {
return nil, err
}
return &LatestBlockHead{
url: url,
client: client,
quit: make(chan struct{}),
done: make(chan struct{}),
}, nil
}
func (h *LatestBlockHead) Start() {
go func() {
ticker := time.NewTicker(blockHeadSyncPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
blockNum, err := h.getBlockNum()
if err != nil {
log.Error("error retrieving latest block number", "error", err)
continue
}
log.Trace("polling block number", "blockNum", blockNum)
h.mutex.Lock()
h.blockNum = blockNum
h.mutex.Unlock()
case <-h.quit:
close(h.done)
return
}
}
}()
}
func (h *LatestBlockHead) getBlockNum() (uint64, error) {
const maxRetries = 5
var err error
for i := 0; i <= maxRetries; i++ {
var blockNum uint64
blockNum, err = h.client.BlockNumber(context.Background())
if err != nil {
backoff := calcBackoff(i)
log.Warn("http operation failed. retrying...", "error", err, "backoff", backoff)
time.Sleep(backoff)
continue
}
return blockNum, nil
}
return 0, wrapErr(err, "exceeded retries")
}
func (h *LatestBlockHead) Stop() {
close(h.quit)
<-h.done
h.client.Close()
}
func (h *LatestBlockHead) GetBlockNum() uint64 {
h.mutex.RLock()
defer h.mutex.RUnlock()
return h.blockNum
}
package proxyd
import (
"context"
"time"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
const cacheSyncRate = 1 * time.Second
type lvcUpdateFn func(context.Context, *ethclient.Client) (string, error)
type EthLastValueCache struct {
client *ethclient.Client
cache Cache
key string
updater lvcUpdateFn
quit chan struct{}
}
func newLVC(client *ethclient.Client, cache Cache, cacheKey string, updater lvcUpdateFn) *EthLastValueCache {
return &EthLastValueCache{
client: client,
cache: cache,
key: cacheKey,
updater: updater,
quit: make(chan struct{}),
}
}
func (h *EthLastValueCache) Start() {
go func() {
ticker := time.NewTicker(cacheSyncRate)
defer ticker.Stop()
for {
select {
case <-ticker.C:
lvcPollTimeGauge.WithLabelValues(h.key).SetToCurrentTime()
value, err := h.getUpdate()
if err != nil {
log.Error("error retrieving latest value", "key", h.key, "error", err)
continue
}
log.Trace("polling latest value", "value", value)
if err := h.cache.Put(context.Background(), h.key, value); err != nil {
log.Error("error writing last value to cache", "key", h.key, "error", err)
}
case <-h.quit:
return
}
}
}()
}
func (h *EthLastValueCache) getUpdate() (string, error) {
const maxRetries = 5
var err error
for i := 0; i <= maxRetries; i++ {
var value string
value, err = h.updater(context.Background(), h.client)
if err != nil {
backoff := calcBackoff(i)
log.Warn("http operation failed. retrying...", "error", err, "backoff", backoff)
lvcErrorsTotal.WithLabelValues(h.key).Inc()
time.Sleep(backoff)
continue
}
return value, nil
}
return "", wrapErr(err, "exceeded retries")
}
func (h *EthLastValueCache) Stop() {
close(h.quit)
}
func (h *EthLastValueCache) Read(ctx context.Context) (string, error) {
return h.cache.Get(ctx, h.key)
}
...@@ -5,36 +5,56 @@ import ( ...@@ -5,36 +5,56 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"sync"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
) )
var errInvalidRPCParams = errors.New("invalid RPC params") var (
errInvalidRPCParams = errors.New("invalid RPC params")
)
type RPCMethodHandler interface { type RPCMethodHandler interface {
CacheKey(req *RPCReq) string GetRPCMethod(context.Context, *RPCReq) (*RPCRes, error)
IsCacheable(req *RPCReq) (bool, error) PutRPCMethod(context.Context, *RPCReq, *RPCRes) error
RequiresUnconfirmedBlocks(ctx context.Context, req *RPCReq) (bool, error)
} }
type StaticRPCMethodHandler struct { type StaticMethodHandler struct {
method string cache interface{}
m sync.RWMutex
} }
func (s *StaticRPCMethodHandler) CacheKey(req *RPCReq) string { func (e *StaticMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) {
return fmt.Sprintf("method:%s", s.method) e.m.RLock()
cache := e.cache
e.m.RUnlock()
if cache == nil {
return nil, nil
}
return &RPCRes{
JSONRPC: req.JSONRPC,
Result: cache,
ID: req.ID,
}, nil
} }
func (s *StaticRPCMethodHandler) IsCacheable(*RPCReq) (bool, error) { return true, nil } func (e *StaticMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
func (s *StaticRPCMethodHandler) RequiresUnconfirmedBlocks(context.Context, *RPCReq) (bool, error) { e.m.Lock()
return false, nil if e.cache == nil {
e.cache = res.Result
}
e.m.Unlock()
return nil
} }
type EthGetBlockByNumberMethod struct { type EthGetBlockByNumberMethodHandler struct {
getLatestBlockNumFn GetLatestBlockNumFn cache Cache
getLatestBlockNumFn GetLatestBlockNumFn
numBlockConfirmations int
} }
func (e *EthGetBlockByNumberMethod) CacheKey(req *RPCReq) string { func (e *EthGetBlockByNumberMethodHandler) cacheKey(req *RPCReq) string {
input, includeTx, err := decodeGetBlockByNumberParams(req.Params) input, includeTx, err := decodeGetBlockByNumberParams(req.Params)
if err != nil { if err != nil {
return "" return ""
...@@ -42,7 +62,7 @@ func (e *EthGetBlockByNumberMethod) CacheKey(req *RPCReq) string { ...@@ -42,7 +62,7 @@ func (e *EthGetBlockByNumberMethod) CacheKey(req *RPCReq) string {
return fmt.Sprintf("method:eth_getBlockByNumber:%s:%t", input, includeTx) return fmt.Sprintf("method:eth_getBlockByNumber:%s:%t", input, includeTx)
} }
func (e *EthGetBlockByNumberMethod) IsCacheable(req *RPCReq) (bool, error) { func (e *EthGetBlockByNumberMethodHandler) cacheable(req *RPCReq) (bool, error) {
blockNum, _, err := decodeGetBlockByNumberParams(req.Params) blockNum, _, err := decodeGetBlockByNumberParams(req.Params)
if err != nil { if err != nil {
return false, err return false, err
...@@ -50,33 +70,51 @@ func (e *EthGetBlockByNumberMethod) IsCacheable(req *RPCReq) (bool, error) { ...@@ -50,33 +70,51 @@ func (e *EthGetBlockByNumberMethod) IsCacheable(req *RPCReq) (bool, error) {
return !isBlockDependentParam(blockNum), nil return !isBlockDependentParam(blockNum), nil
} }
func (e *EthGetBlockByNumberMethod) RequiresUnconfirmedBlocks(ctx context.Context, req *RPCReq) (bool, error) { func (e *EthGetBlockByNumberMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) {
curBlock, err := e.getLatestBlockNumFn(ctx) if ok, err := e.cacheable(req); !ok || err != nil {
if err != nil { return nil, err
return false, err }
key := e.cacheKey(req)
return getImmutableRPCResponse(ctx, e.cache, key, req)
}
func (e *EthGetBlockByNumberMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
if ok, err := e.cacheable(req); !ok || err != nil {
return err
} }
blockInput, _, err := decodeGetBlockByNumberParams(req.Params) blockInput, _, err := decodeGetBlockByNumberParams(req.Params)
if err != nil { if err != nil {
return false, err return err
} }
if isBlockDependentParam(blockInput) { if isBlockDependentParam(blockInput) {
return true, nil return nil
} }
if blockInput == "earliest" { if blockInput != "earliest" {
return false, nil curBlock, err := e.getLatestBlockNumFn(ctx)
} if err != nil {
blockNum, err := decodeBlockInput(blockInput) return err
if err != nil { }
return false, err blockNum, err := decodeBlockInput(blockInput)
if err != nil {
return err
}
if curBlock <= blockNum+uint64(e.numBlockConfirmations) {
return nil
}
} }
return curBlock <= blockNum+numBlockConfirmations, nil
key := e.cacheKey(req)
return putImmutableRPCResponse(ctx, e.cache, key, req, res)
} }
type EthGetBlockRangeMethod struct { type EthGetBlockRangeMethodHandler struct {
getLatestBlockNumFn GetLatestBlockNumFn cache Cache
getLatestBlockNumFn GetLatestBlockNumFn
numBlockConfirmations int
} }
func (e *EthGetBlockRangeMethod) CacheKey(req *RPCReq) string { func (e *EthGetBlockRangeMethodHandler) cacheKey(req *RPCReq) string {
start, end, includeTx, err := decodeGetBlockRangeParams(req.Params) start, end, includeTx, err := decodeGetBlockRangeParams(req.Params)
if err != nil { if err != nil {
return "" return ""
...@@ -84,7 +122,7 @@ func (e *EthGetBlockRangeMethod) CacheKey(req *RPCReq) string { ...@@ -84,7 +122,7 @@ func (e *EthGetBlockRangeMethod) CacheKey(req *RPCReq) string {
return fmt.Sprintf("method:eth_getBlockRange:%s:%s:%t", start, end, includeTx) return fmt.Sprintf("method:eth_getBlockRange:%s:%s:%t", start, end, includeTx)
} }
func (e *EthGetBlockRangeMethod) IsCacheable(req *RPCReq) (bool, error) { func (e *EthGetBlockRangeMethodHandler) cacheable(req *RPCReq) (bool, error) {
start, end, _, err := decodeGetBlockRangeParams(req.Params) start, end, _, err := decodeGetBlockRangeParams(req.Params)
if err != nil { if err != nil {
return false, err return false, err
...@@ -92,42 +130,144 @@ func (e *EthGetBlockRangeMethod) IsCacheable(req *RPCReq) (bool, error) { ...@@ -92,42 +130,144 @@ func (e *EthGetBlockRangeMethod) IsCacheable(req *RPCReq) (bool, error) {
return !isBlockDependentParam(start) && !isBlockDependentParam(end), nil return !isBlockDependentParam(start) && !isBlockDependentParam(end), nil
} }
func (e *EthGetBlockRangeMethod) RequiresUnconfirmedBlocks(ctx context.Context, req *RPCReq) (bool, error) { func (e *EthGetBlockRangeMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) {
curBlock, err := e.getLatestBlockNumFn(ctx) if ok, err := e.cacheable(req); !ok || err != nil {
if err != nil { return nil, err
return false, err }
key := e.cacheKey(req)
return getImmutableRPCResponse(ctx, e.cache, key, req)
}
func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
if ok, err := e.cacheable(req); !ok || err != nil {
return err
} }
start, end, _, err := decodeGetBlockRangeParams(req.Params) start, end, _, err := decodeGetBlockRangeParams(req.Params)
if err != nil { if err != nil {
return false, err return err
}
if isBlockDependentParam(start) || isBlockDependentParam(end) {
return true, nil
} }
if start == "earliest" && end == "earliest" { curBlock, err := e.getLatestBlockNumFn(ctx)
return false, nil if err != nil {
return err
} }
if start != "earliest" { if start != "earliest" {
startNum, err := decodeBlockInput(start) startNum, err := decodeBlockInput(start)
if err != nil { if err != nil {
return false, err return err
} }
if curBlock <= startNum+numBlockConfirmations { if curBlock <= startNum+uint64(e.numBlockConfirmations) {
return true, nil return nil
} }
} }
if end != "earliest" { if end != "earliest" {
endNum, err := decodeBlockInput(end) endNum, err := decodeBlockInput(end)
if err != nil { if err != nil {
return false, err return err
} }
if curBlock <= endNum+numBlockConfirmations { if curBlock <= endNum+uint64(e.numBlockConfirmations) {
return true, nil return nil
} }
} }
return false, nil
key := e.cacheKey(req)
return putImmutableRPCResponse(ctx, e.cache, key, req, res)
}
type EthCallMethodHandler struct {
cache Cache
getLatestBlockNumFn GetLatestBlockNumFn
numBlockConfirmations int
}
func (e *EthCallMethodHandler) cacheable(params *ethCallParams, blockTag string) bool {
if isBlockDependentParam(blockTag) {
return false
}
if params.From != "" || params.Gas != "" {
return false
}
if params.Value != "" && params.Value != "0x0" {
return false
}
return true
}
func (e *EthCallMethodHandler) cacheKey(params *ethCallParams, blockTag string) string {
keyParams := fmt.Sprintf("%s:%s:%s", params.To, params.Data, blockTag)
return fmt.Sprintf("method:eth_call:%s", keyParams)
}
func (e *EthCallMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) {
params, blockTag, err := decodeEthCallParams(req)
if err != nil {
return nil, err
}
if !e.cacheable(params, blockTag) {
return nil, nil
}
key := e.cacheKey(params, blockTag)
return getImmutableRPCResponse(ctx, e.cache, key, req)
}
func (e *EthCallMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
params, blockTag, err := decodeEthCallParams(req)
if err != nil {
return err
}
if !e.cacheable(params, blockTag) {
return nil
}
if blockTag != "earliest" {
curBlock, err := e.getLatestBlockNumFn(ctx)
if err != nil {
return err
}
blockNum, err := decodeBlockInput(blockTag)
if err != nil {
return err
}
if curBlock <= blockNum+uint64(e.numBlockConfirmations) {
return nil
}
}
key := e.cacheKey(params, blockTag)
return putImmutableRPCResponse(ctx, e.cache, key, req, res)
}
type EthBlockNumberMethodHandler struct {
getLatestBlockNumFn GetLatestBlockNumFn
}
func (e *EthBlockNumberMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) {
blockNum, err := e.getLatestBlockNumFn(ctx)
if err != nil {
return nil, err
}
return makeRPCRes(req, hexutil.EncodeUint64(blockNum)), nil
}
func (e *EthBlockNumberMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRes) error {
return nil
}
type EthGasPriceMethodHandler struct {
getLatestGasPrice GetLatestGasPriceFn
}
func (e *EthGasPriceMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) {
gasPrice, err := e.getLatestGasPrice(ctx)
if err != nil {
return nil, err
}
return makeRPCRes(req, hexutil.EncodeUint64(gasPrice)), nil
}
func (e *EthGasPriceMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRes) error {
return nil
} }
func isBlockDependentParam(s string) bool { func isBlockDependentParam(s string) bool {
...@@ -186,6 +326,34 @@ func decodeBlockInput(input string) (uint64, error) { ...@@ -186,6 +326,34 @@ func decodeBlockInput(input string) (uint64, error) {
return hexutil.DecodeUint64(input) return hexutil.DecodeUint64(input)
} }
type ethCallParams struct {
From string `json:"from"`
To string `json:"to"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Value string `json:"value"`
Data string `json:"data"`
}
func decodeEthCallParams(req *RPCReq) (*ethCallParams, string, error) {
var input []json.RawMessage
if err := json.Unmarshal(req.Params, &input); err != nil {
return nil, "", err
}
if len(input) != 2 {
return nil, "", fmt.Errorf("invalid eth_call parameters")
}
params := new(ethCallParams)
if err := json.Unmarshal(input[0], params); err != nil {
return nil, "", err
}
var blockTag string
if err := json.Unmarshal(input[1], &blockTag); err != nil {
return nil, "", err
}
return params, blockTag, nil
}
func validBlockInput(input string) bool { func validBlockInput(input string) bool {
if input == "earliest" || input == "pending" || input == "latest" { if input == "earliest" || input == "pending" || input == "latest" {
return true return true
...@@ -193,3 +361,39 @@ func validBlockInput(input string) bool { ...@@ -193,3 +361,39 @@ func validBlockInput(input string) bool {
_, err := decodeBlockInput(input) _, err := decodeBlockInput(input)
return err == nil return err == nil
} }
func makeRPCRes(req *RPCReq, result interface{}) *RPCRes {
return &RPCRes{
JSONRPC: JSONRPCVersion,
ID: req.ID,
Result: result,
}
}
func getImmutableRPCResponse(ctx context.Context, cache Cache, key string, req *RPCReq) (*RPCRes, error) {
val, err := cache.Get(ctx, key)
if err != nil {
return nil, err
}
if val == "" {
return nil, nil
}
var result interface{}
if err := json.Unmarshal([]byte(val), &result); err != nil {
return nil, err
}
return &RPCRes{
JSONRPC: req.JSONRPC,
Result: result,
ID: req.ID,
}, nil
}
func putImmutableRPCResponse(ctx context.Context, cache Cache, key string, req *RPCReq, res *RPCRes) error {
if key == "" {
return nil
}
val := mustMarshalJSON(res.Result)
return cache.Put(ctx, key, string(val))
}
...@@ -145,7 +145,7 @@ var ( ...@@ -145,7 +145,7 @@ var (
requestPayloadSizesGauge = promauto.NewHistogramVec(prometheus.HistogramOpts{ requestPayloadSizesGauge = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: MetricsNamespace, Namespace: MetricsNamespace,
Name: "request_payload_sizes", Name: "request_payload_sizes",
Help: "Gauge of client request payload sizes.", Help: "Histogram of client request payload sizes.",
Buckets: PayloadSizeBuckets, Buckets: PayloadSizeBuckets,
}, []string{ }, []string{
"auth", "auth",
...@@ -154,7 +154,7 @@ var ( ...@@ -154,7 +154,7 @@ var (
responsePayloadSizesGauge = promauto.NewHistogramVec(prometheus.HistogramOpts{ responsePayloadSizesGauge = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: MetricsNamespace, Namespace: MetricsNamespace,
Name: "response_payload_sizes", Name: "response_payload_sizes",
Help: "Gauge of client response payload sizes.", Help: "Histogram of client response payload sizes.",
Buckets: PayloadSizeBuckets, Buckets: PayloadSizeBuckets,
}, []string{ }, []string{
"auth", "auth",
...@@ -176,6 +176,22 @@ var ( ...@@ -176,6 +176,22 @@ var (
"method", "method",
}) })
lvcErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Name: "lvc_errors_total",
Help: "Count of lvc errors.",
}, []string{
"key",
})
lvcPollTimeGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Name: "lvc_poll_time_gauge",
Help: "Gauge of lvc poll time.",
}, []string{
"key",
})
rpcSpecialErrors = []string{ rpcSpecialErrors = []string{
"nonce too low", "nonce too low",
"gas price too high", "gas price too high",
......
...@@ -7,8 +7,10 @@ import ( ...@@ -7,8 +7,10 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strconv"
"time" "time"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
) )
...@@ -162,10 +164,18 @@ func Start(config *Config) (func(), error) { ...@@ -162,10 +164,18 @@ func Start(config *Config) (func(), error) {
} }
} }
var rpcCache RPCCache var (
var latestHead *LatestBlockHead rpcCache RPCCache
blockNumLVC *EthLastValueCache
gasPriceLVC *EthLastValueCache
)
if config.Cache.Enabled { if config.Cache.Enabled {
var getLatestBlockNumFn GetLatestBlockNumFn var (
cache Cache
blockNumFn GetLatestBlockNumFn
gasPriceFn GetLatestGasPriceFn
)
if config.Cache.BlockSyncRPCURL == "" { if config.Cache.BlockSyncRPCURL == "" {
return nil, fmt.Errorf("block sync node required for caching") return nil, fmt.Errorf("block sync node required for caching")
} }
...@@ -174,7 +184,6 @@ func Start(config *Config) (func(), error) { ...@@ -174,7 +184,6 @@ func Start(config *Config) (func(), error) {
return nil, err return nil, err
} }
var cache Cache
if redisURL != "" { if redisURL != "" {
if cache, err = newRedisCache(redisURL); err != nil { if cache, err = newRedisCache(redisURL); err != nil {
return nil, err return nil, err
...@@ -183,17 +192,16 @@ func Start(config *Config) (func(), error) { ...@@ -183,17 +192,16 @@ func Start(config *Config) (func(), error) {
log.Warn("redis is not configured, using in-memory cache") log.Warn("redis is not configured, using in-memory cache")
cache = newMemoryCache() cache = newMemoryCache()
} }
// Ideally, the BlocKSyncRPCURL should be the sequencer or a HA replica that's not far behind
latestHead, err = newLatestBlockHead(blockSyncRPCURL) ethClient, err := ethclient.Dial(blockSyncRPCURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
latestHead.Start() defer ethClient.Close()
getLatestBlockNumFn = func(ctx context.Context) (uint64, error) { blockNumLVC, blockNumFn = makeGetLatestBlockNumFn(ethClient, cache)
return latestHead.GetBlockNum(), nil gasPriceLVC, gasPriceFn = makeGetLatestGasPriceFn(ethClient, cache)
} rpcCache = newRPCCache(newCacheWithCompression(cache), blockNumFn, gasPriceFn, config.Cache.NumBlockConfirmations)
rpcCache = newRPCCache(cache, getLatestBlockNumFn)
} }
srv := NewServer( srv := NewServer(
...@@ -246,8 +254,11 @@ func Start(config *Config) (func(), error) { ...@@ -246,8 +254,11 @@ func Start(config *Config) (func(), error) {
return func() { return func() {
log.Info("shutting down proxyd") log.Info("shutting down proxyd")
if latestHead != nil { if blockNumLVC != nil {
latestHead.Stop() blockNumLVC.Stop()
}
if gasPriceLVC != nil {
gasPriceLVC.Stop()
} }
srv.Shutdown() srv.Shutdown()
if err := lim.FlushBackendWSConns(backendNames); err != nil { if err := lim.FlushBackendWSConns(backendNames); err != nil {
...@@ -281,3 +292,39 @@ func configureBackendTLS(cfg *BackendConfig) (*tls.Config, error) { ...@@ -281,3 +292,39 @@ func configureBackendTLS(cfg *BackendConfig) (*tls.Config, error) {
return tlsConfig, nil return tlsConfig, nil
} }
func makeUint64LastValueFn(client *ethclient.Client, cache Cache, key string, updater lvcUpdateFn) (*EthLastValueCache, func(context.Context) (uint64, error)) {
lvc := newLVC(client, cache, key, updater)
lvc.Start()
return lvc, func(ctx context.Context) (uint64, error) {
value, err := lvc.Read(ctx)
if err != nil {
return 0, err
}
if value == "" {
return 0, fmt.Errorf("%s is unavailable", key)
}
valueUint, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return 0, err
}
return valueUint, nil
}
}
func makeGetLatestBlockNumFn(client *ethclient.Client, cache Cache) (*EthLastValueCache, GetLatestBlockNumFn) {
return makeUint64LastValueFn(client, cache, "lvc:block_number", func(ctx context.Context, c *ethclient.Client) (string, error) {
blockNum, err := c.BlockNumber(ctx)
return strconv.FormatUint(blockNum, 10), err
})
}
func makeGetLatestGasPriceFn(client *ethclient.Client, cache Cache) (*EthLastValueCache, GetLatestGasPriceFn) {
return makeUint64LastValueFn(client, cache, "lvc:gas_price", func(ctx context.Context, c *ethclient.Client) (string, error) {
gasPrice, err := c.SuggestGasPrice(ctx)
if err != nil {
return "", err
}
return gasPrice.String(), nil
})
}
...@@ -121,4 +121,4 @@ func IsBatch(raw []byte) bool { ...@@ -121,4 +121,4 @@ func IsBatch(raw []byte) bool {
return c == '[' return c == '['
} }
return false return false
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment