Commit 2b34a1ee authored by inphi's avatar inphi

removed eth_call sync code; cache mod latest blocks

parent 1cf8b15d
...@@ -16,9 +16,7 @@ import ( ...@@ -16,9 +16,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
...@@ -86,7 +84,6 @@ type Backend struct { ...@@ -86,7 +84,6 @@ type Backend struct {
authPassword string authPassword string
rateLimiter RateLimiter rateLimiter RateLimiter
client *http.Client client *http.Client
blockNumberLVC *EthLastValueCache
dialer *websocket.Dialer dialer *websocket.Dialer
maxRetries int maxRetries int
maxResponseSize int64 maxResponseSize int64
...@@ -169,7 +166,7 @@ func NewBackend( ...@@ -169,7 +166,7 @@ func NewBackend(
wsURL string, wsURL string,
rateLimiter RateLimiter, rateLimiter RateLimiter,
opts ...BackendOpt, opts ...BackendOpt,
) (*Backend, error) { ) *Backend {
backend := &Backend{ backend := &Backend{
Name: name, Name: name,
rpcURL: rpcURL, rpcURL: rpcURL,
...@@ -186,28 +183,11 @@ func NewBackend( ...@@ -186,28 +183,11 @@ func NewBackend(
opt(backend) opt(backend)
} }
rpcClient, err := rpc.DialHTTPWithClient(rpcURL, backend.client)
if err != nil {
return nil, err
}
backend.blockNumberLVC = newLVC(ethclient.NewClient(rpcClient), func(ctx context.Context, client *ethclient.Client) (interface{}, error) {
blockNumber, err := client.BlockNumber(ctx)
return blockNumber, err
})
if !backend.stripTrailingXFF && backend.proxydIP == "" { if !backend.stripTrailingXFF && backend.proxydIP == "" {
log.Warn("proxied requests' XFF header will not contain the proxyd ip address") log.Warn("proxied requests' XFF header will not contain the proxyd ip address")
} }
return backend, nil return backend
}
func (b *Backend) Start() {
b.blockNumberLVC.Start()
}
func (b *Backend) Stop() {
b.blockNumberLVC.Stop()
} }
func (b *Backend) Forward(ctx context.Context, req *RPCReq) (*RPCRes, error) { func (b *Backend) Forward(ctx context.Context, req *RPCReq) (*RPCRes, error) {
...@@ -288,14 +268,6 @@ func (b *Backend) ProxyWS(clientConn *websocket.Conn, methodWhitelist *StringSet ...@@ -288,14 +268,6 @@ func (b *Backend) ProxyWS(clientConn *websocket.Conn, methodWhitelist *StringSet
return NewWSProxier(b, clientConn, backendConn, methodWhitelist), nil return NewWSProxier(b, clientConn, backendConn, methodWhitelist), nil
} }
func (b *Backend) BlockNumber() uint64 {
var blockNum uint64
if val := b.blockNumberLVC.Read(); val != nil {
blockNum = val.(uint64)
}
return blockNum
}
func (b *Backend) Online() bool { func (b *Backend) Online() bool {
online, err := b.rateLimiter.IsBackendOnline(b.Name) online, err := b.rateLimiter.IsBackendOnline(b.Name)
if err != nil { if err != nil {
...@@ -423,16 +395,13 @@ type BackendGroup struct { ...@@ -423,16 +395,13 @@ type BackendGroup struct {
Backends []*Backend Backends []*Backend
} }
func (b *BackendGroup) Forward(ctx context.Context, rpcReq *RPCReq) (*RPCRes, uint64, error) { func (b *BackendGroup) Forward(ctx context.Context, rpcReq *RPCReq) (*RPCRes, error) {
rpcRequestsTotal.Inc() rpcRequestsTotal.Inc()
for _, back := range b.Backends { for _, back := range b.Backends {
// The blockNum must precede the forwarded RPC to establish a synchronization point
blockNum := back.BlockNumber()
res, err := back.Forward(ctx, rpcReq) res, err := back.Forward(ctx, rpcReq)
if errors.Is(err, ErrMethodNotWhitelisted) { if errors.Is(err, ErrMethodNotWhitelisted) {
return nil, 0, err return nil, err
} }
if errors.Is(err, ErrBackendOffline) { if errors.Is(err, ErrBackendOffline) {
log.Warn( log.Warn(
...@@ -462,11 +431,11 @@ func (b *BackendGroup) Forward(ctx context.Context, rpcReq *RPCReq) (*RPCRes, ui ...@@ -462,11 +431,11 @@ func (b *BackendGroup) Forward(ctx context.Context, rpcReq *RPCReq) (*RPCRes, ui
) )
continue continue
} }
return res, blockNum, nil return res, nil
} }
RecordUnserviceableRequest(ctx, RPCRequestSourceHTTP) RecordUnserviceableRequest(ctx, RPCRequestSourceHTTP)
return nil, 0, ErrNoBackends return nil, ErrNoBackends
} }
func (b *BackendGroup) ProxyWS(ctx context.Context, clientConn *websocket.Conn, methodWhitelist *StringSet) (*WSProxier, error) { func (b *BackendGroup) ProxyWS(ctx context.Context, clientConn *websocket.Conn, methodWhitelist *StringSet) (*WSProxier, error) {
......
...@@ -4,13 +4,13 @@ import ( ...@@ -4,13 +4,13 @@ import (
"context" "context"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang/snappy"
lru "github.com/hashicorp/golang-lru" lru "github.com/hashicorp/golang-lru"
) )
type Cache interface { type Cache interface {
Get(ctx context.Context, key string) (string, error) Get(ctx context.Context, key string) (string, error)
Put(ctx context.Context, key string, value string) error Put(ctx context.Context, key string, value string) error
Remove(ctx context.Context, key string) error
} }
const ( const (
...@@ -39,11 +39,6 @@ func (c *cache) Put(ctx context.Context, key string, value string) error { ...@@ -39,11 +39,6 @@ func (c *cache) Put(ctx context.Context, key string, value string) error {
return nil return nil
} }
func (c *cache) Remove(ctx context.Context, key string) error {
c.lru.Remove(key)
return nil
}
type redisCache struct { type redisCache struct {
rdb *redis.Client rdb *redis.Client
} }
...@@ -79,12 +74,32 @@ func (c *redisCache) Put(ctx context.Context, key string, value string) error { ...@@ -79,12 +74,32 @@ func (c *redisCache) Put(ctx context.Context, key string, value string) error {
return err return err
} }
func (c *redisCache) Remove(ctx context.Context, key string) error { type cacheWithCompression struct {
err := c.rdb.Del(ctx, key).Err() 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 { if err != nil {
RecordRedisError("CacheDel") return "", err
} }
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)
...@@ -92,11 +107,7 @@ type GetLatestGasPriceFn func(ctx context.Context) (uint64, error) ...@@ -92,11 +107,7 @@ 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)
PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error
// The blockNumberSync is used to enforce Sequential Consistency for cache invalidation. We make the following assumptions to do this:
// 1. blockNumberSync is monotonically increasing (sans reorgs)
// 2. blockNumberSync is ordered before block state of the RPCRes
PutRPC(ctx context.Context, req *RPCReq, res *RPCRes, blockNumberSync uint64) error
} }
type rpcCache struct { type rpcCache struct {
...@@ -104,15 +115,15 @@ type rpcCache struct { ...@@ -104,15 +115,15 @@ type rpcCache struct {
handlers map[string]RPCMethodHandler handlers map[string]RPCMethodHandler
} }
func newRPCCache(cache Cache, getLatestBlockNumFn GetLatestBlockNumFn, getLatestGasPriceFn GetLatestGasPriceFn) RPCCache { func newRPCCache(cache Cache, getLatestBlockNumFn GetLatestBlockNumFn, getLatestGasPriceFn GetLatestGasPriceFn, numBlockConfirmations int) RPCCache {
handlers := map[string]RPCMethodHandler{ handlers := map[string]RPCMethodHandler{
"eth_chainId": &StaticMethodHandler{}, "eth_chainId": &StaticMethodHandler{},
"net_version": &StaticMethodHandler{}, "net_version": &StaticMethodHandler{},
"eth_getBlockByNumber": &EthGetBlockByNumberMethodHandler{cache, getLatestBlockNumFn}, "eth_getBlockByNumber": &EthGetBlockByNumberMethodHandler{cache, getLatestBlockNumFn, numBlockConfirmations},
"eth_getBlockRange": &EthGetBlockRangeMethodHandler{cache, getLatestBlockNumFn}, "eth_getBlockRange": &EthGetBlockRangeMethodHandler{cache, getLatestBlockNumFn, numBlockConfirmations},
"eth_blockNumber": &EthBlockNumberMethodHandler{getLatestBlockNumFn}, "eth_blockNumber": &EthBlockNumberMethodHandler{getLatestBlockNumFn},
"eth_gasPrice": &EthGasPriceMethodHandler{getLatestGasPriceFn}, "eth_gasPrice": &EthGasPriceMethodHandler{getLatestGasPriceFn},
"eth_call": &EthCallMethodHandler{cache, getLatestBlockNumFn}, "eth_call": &EthCallMethodHandler{cache, getLatestBlockNumFn, numBlockConfirmations},
} }
return &rpcCache{ return &rpcCache{
cache: cache, cache: cache,
...@@ -136,10 +147,10 @@ func (c *rpcCache) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) { ...@@ -136,10 +147,10 @@ func (c *rpcCache) GetRPC(ctx context.Context, req *RPCReq) (*RPCRes, error) {
return res, err return res, err
} }
func (c *rpcCache) PutRPC(ctx context.Context, req *RPCReq, res *RPCRes, blockNumberSync uint64) error { func (c *rpcCache) PutRPC(ctx context.Context, req *RPCReq, res *RPCRes) error {
handler := c.handlers[req.Method] handler := c.handlers[req.Method]
if handler == nil { if handler == nil {
return nil return nil
} }
return handler.PutRPCMethod(ctx, req, res, blockNumberSync) return handler.PutRPCMethod(ctx, req, res)
} }
...@@ -5,11 +5,12 @@ import ( ...@@ -5,11 +5,12 @@ import (
"math" "math"
"strconv" "strconv"
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const numBlockConfirmations = 10
func TestRPCCacheImmutableRPCs(t *testing.T) { func TestRPCCacheImmutableRPCs(t *testing.T) {
const blockHead = math.MaxUint64 const blockHead = math.MaxUint64
ctx := context.Background() ctx := context.Background()
...@@ -17,7 +18,7 @@ func TestRPCCacheImmutableRPCs(t *testing.T) { ...@@ -17,7 +18,7 @@ func TestRPCCacheImmutableRPCs(t *testing.T) {
getBlockNum := func(ctx context.Context) (uint64, error) { getBlockNum := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), getBlockNum, nil) cache := newRPCCache(newMemoryCache(), getBlockNum, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -111,7 +112,7 @@ func TestRPCCacheImmutableRPCs(t *testing.T) { ...@@ -111,7 +112,7 @@ func TestRPCCacheImmutableRPCs(t *testing.T) {
for _, rpc := range rpcs { for _, rpc := range rpcs {
t.Run(rpc.name, func(t *testing.T) { t.Run(rpc.name, func(t *testing.T) {
err := cache.PutRPC(ctx, rpc.req, rpc.res, blockHead) err := cache.PutRPC(ctx, rpc.req, rpc.res)
require.NoError(t, err) require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, rpc.req) cachedRes, err := cache.GetRPC(ctx, rpc.req)
...@@ -133,7 +134,7 @@ func TestRPCCacheBlockNumber(t *testing.T) { ...@@ -133,7 +134,7 @@ func TestRPCCacheBlockNumber(t *testing.T) {
getBlockNum := func(ctx context.Context) (uint64, error) { getBlockNum := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), getBlockNum, getGasPrice) cache := newRPCCache(newMemoryCache(), getBlockNum, getGasPrice, numBlockConfirmations)
req := &RPCReq{ req := &RPCReq{
JSONRPC: "2.0", JSONRPC: "2.0",
...@@ -146,12 +147,17 @@ func TestRPCCacheBlockNumber(t *testing.T) { ...@@ -146,12 +147,17 @@ func TestRPCCacheBlockNumber(t *testing.T) {
ID: ID, ID: ID,
} }
err := cache.PutRPC(ctx, req, res, blockHead) err := cache.PutRPC(ctx, req, res)
require.NoError(t, err) require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, req) cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, res, cachedRes) 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) { func TestRPCCacheGasPrice(t *testing.T) {
...@@ -166,7 +172,7 @@ func TestRPCCacheGasPrice(t *testing.T) { ...@@ -166,7 +172,7 @@ func TestRPCCacheGasPrice(t *testing.T) {
getBlockNum := func(ctx context.Context) (uint64, error) { getBlockNum := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), getBlockNum, getGasPrice) cache := newRPCCache(newMemoryCache(), getBlockNum, getGasPrice, numBlockConfirmations)
req := &RPCReq{ req := &RPCReq{
JSONRPC: "2.0", JSONRPC: "2.0",
...@@ -179,12 +185,17 @@ func TestRPCCacheGasPrice(t *testing.T) { ...@@ -179,12 +185,17 @@ func TestRPCCacheGasPrice(t *testing.T) {
ID: ID, ID: ID,
} }
err := cache.PutRPC(ctx, req, res, blockHead) err := cache.PutRPC(ctx, req, res)
require.NoError(t, err) require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, req) cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, res, cachedRes) 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) {
...@@ -194,7 +205,7 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) { ...@@ -194,7 +205,7 @@ 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, nil) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
req := &RPCReq{ req := &RPCReq{
...@@ -208,7 +219,7 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) { ...@@ -208,7 +219,7 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) {
ID: ID, ID: ID,
} }
err := cache.PutRPC(ctx, req, res, blockHead) err := cache.PutRPC(ctx, req, res)
require.NoError(t, err) require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, req) cachedRes, err := cache.GetRPC(ctx, req)
...@@ -219,12 +230,11 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) { ...@@ -219,12 +230,11 @@ func TestRPCCacheUnsupportedMethod(t *testing.T) {
func TestRPCCacheEthGetBlockByNumber(t *testing.T) { func TestRPCCacheEthGetBlockByNumber(t *testing.T) {
ctx := context.Background() ctx := context.Background()
var blockHead uint64 = 100 var blockHead uint64
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn, nil) makeCache := func() RPCCache { return newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations) }
resetCache := func() { cache = newRPCCache(newMemoryCache(), fn, nil) }
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
req := &RPCReq{ req := &RPCReq{
...@@ -250,22 +260,27 @@ func TestRPCCacheEthGetBlockByNumber(t *testing.T) { ...@@ -250,22 +260,27 @@ func TestRPCCacheEthGetBlockByNumber(t *testing.T) {
ID: ID, ID: ID,
} }
require.NoError(t, cache.PutRPC(ctx, req, res, blockHead)) t.Run("set multiple finalized blocks", func(t *testing.T) {
require.NoError(t, cache.PutRPC(ctx, req2, res2, blockHead)) blockHead = 100
cachedRes, err := cache.GetRPC(ctx, req) cache := makeCache()
require.NoError(t, err) require.NoError(t, cache.PutRPC(ctx, req, res))
require.Equal(t, res, cachedRes) require.NoError(t, cache.PutRPC(ctx, req2, res2))
cachedRes, err = cache.GetRPC(ctx, req2) cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, res2, cachedRes) require.Equal(t, res, cachedRes)
cachedRes, err = cache.GetRPC(ctx, req2)
// scenario: input references block that has not yet finalized require.NoError(t, err)
resetCache() require.Equal(t, res2, cachedRes)
blockHead = 0xc })
require.NoError(t, cache.PutRPC(ctx, req, res, blockHead))
cachedRes, err = cache.GetRPC(ctx, req) t.Run("unconfirmed block", func(t *testing.T) {
require.NoError(t, err) blockHead = 0xc
require.Nil(t, cachedRes) 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) {
...@@ -275,7 +290,7 @@ func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) { ...@@ -275,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, nil) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -315,7 +330,7 @@ func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) { ...@@ -315,7 +330,7 @@ func TestRPCCacheEthGetBlockByNumberForRecentBlocks(t *testing.T) {
for _, rpc := range rpcs { for _, rpc := range rpcs {
t.Run(rpc.name, func(t *testing.T) { t.Run(rpc.name, func(t *testing.T) {
err := cache.PutRPC(ctx, rpc.req, rpc.res, blockHead) err := cache.PutRPC(ctx, rpc.req, rpc.res)
require.NoError(t, err) require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, rpc.req) cachedRes, err := cache.GetRPC(ctx, rpc.req)
...@@ -332,7 +347,7 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) { ...@@ -332,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, nil) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
req := &RPCReq{ req := &RPCReq{
...@@ -347,7 +362,7 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) { ...@@ -347,7 +362,7 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) {
ID: ID, ID: ID,
} }
err := cache.PutRPC(ctx, req, res, blockHead) err := cache.PutRPC(ctx, req, res)
require.Error(t, err) require.Error(t, err)
cachedRes, err := cache.GetRPC(ctx, req) cachedRes, err := cache.GetRPC(ctx, req)
...@@ -358,46 +373,51 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) { ...@@ -358,46 +373,51 @@ func TestRPCCacheEthGetBlockByNumberInvalidRequest(t *testing.T) {
func TestRPCCacheEthGetBlockRange(t *testing.T) { func TestRPCCacheEthGetBlockRange(t *testing.T) {
ctx := context.Background() ctx := context.Background()
var blockHead uint64 = 0x1000 var blockHead uint64
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn, nil) makeCache := func() RPCCache { return newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations) }
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
req := &RPCReq{ t.Run("finalized block", func(t *testing.T) {
JSONRPC: "2.0", req := &RPCReq{
Method: "eth_getBlockRange", JSONRPC: "2.0",
Params: []byte(`["0x1", "0x10", false]`), Method: "eth_getBlockRange",
ID: ID, Params: []byte(`["0x1", "0x10", false]`),
} ID: ID,
res := &RPCRes{ }
JSONRPC: "2.0", res := &RPCRes{
Result: `[{"number": "0x1"}, {"number": "0x10"}]`, JSONRPC: "2.0",
ID: ID, Result: `[{"number": "0x1"}, {"number": "0x10"}]`,
} ID: ID,
}
require.NoError(t, cache.PutRPC(ctx, req, res, blockHead)) blockHead = 0x1000
cachedRes, err := cache.GetRPC(ctx, req) cache := makeCache()
require.NoError(t, err) require.NoError(t, cache.PutRPC(ctx, req, res))
require.Equal(t, res, cachedRes) cachedRes, err := cache.GetRPC(ctx, req)
require.NoError(t, err)
// scenario: input references block that has not yet finalized require.Equal(t, res, cachedRes)
req = &RPCReq{ })
JSONRPC: "2.0",
Method: "eth_getBlockRange", t.Run("unconfirmed block", func(t *testing.T) {
Params: []byte(`["0x1", "0x1000", false]`), cache := makeCache()
ID: ID, req := &RPCReq{
} JSONRPC: "2.0",
res = &RPCRes{ Method: "eth_getBlockRange",
JSONRPC: "2.0", Params: []byte(`["0x1", "0x1000", false]`),
Result: `[{"number": "0x1"}, {"number": "0x2"}]`, ID: ID,
ID: ID, }
} res := &RPCRes{
require.NoError(t, cache.PutRPC(ctx, req, res, blockHead)) JSONRPC: "2.0",
cachedRes, err = cache.GetRPC(ctx, req) Result: `[{"number": "0x1"}, {"number": "0x2"}]`,
require.NoError(t, err) ID: ID,
require.Nil(t, cachedRes) }
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) {
...@@ -407,7 +427,7 @@ func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) { ...@@ -407,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, nil) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -461,7 +481,7 @@ func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) { ...@@ -461,7 +481,7 @@ func TestRPCCacheEthGetBlockRangeForRecentBlocks(t *testing.T) {
for _, rpc := range rpcs { for _, rpc := range rpcs {
t.Run(rpc.name, func(t *testing.T) { t.Run(rpc.name, func(t *testing.T) {
err := cache.PutRPC(ctx, rpc.req, rpc.res, blockHead) err := cache.PutRPC(ctx, rpc.req, rpc.res)
require.NoError(t, err) require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, rpc.req) cachedRes, err := cache.GetRPC(ctx, rpc.req)
...@@ -478,7 +498,7 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) { ...@@ -478,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, nil) cache := newRPCCache(newMemoryCache(), fn, nil, numBlockConfirmations)
ID := []byte(strconv.Itoa(1)) ID := []byte(strconv.Itoa(1))
rpcs := []struct { rpcs := []struct {
...@@ -518,7 +538,7 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) { ...@@ -518,7 +538,7 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) {
for _, rpc := range rpcs { for _, rpc := range rpcs {
t.Run(rpc.name, func(t *testing.T) { t.Run(rpc.name, func(t *testing.T) {
err := cache.PutRPC(ctx, rpc.req, rpc.res, blockHead) err := cache.PutRPC(ctx, rpc.req, rpc.res)
require.Error(t, err) require.Error(t, err)
cachedRes, err := cache.GetRPC(ctx, rpc.req) cachedRes, err := cache.GetRPC(ctx, rpc.req)
...@@ -531,18 +551,18 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) { ...@@ -531,18 +551,18 @@ func TestRPCCacheEthGetBlockRangeInvalidRequest(t *testing.T) {
func TestRPCCacheEthCall(t *testing.T) { func TestRPCCacheEthCall(t *testing.T) {
ctx := context.Background() ctx := context.Background()
var blockHead uint64 = 0x1000 var blockHead uint64
fn := func(ctx context.Context) (uint64, error) { fn := func(ctx context.Context) (uint64, error) {
return blockHead, nil return blockHead, nil
} }
cache := newRPCCache(newMemoryCache(), fn, nil)
resetCache := func() { cache = newRPCCache(newMemoryCache(), fn, nil) } makeCache := func() RPCCache { return 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_call", Method: "eth_call",
Params: []byte(`[{"to": "0xDEADBEEF", "data": "0x1"}, "latest"]`), Params: []byte(`[{"to": "0xDEADBEEF", "data": "0x1"}, "0x10"]`),
ID: ID, ID: ID,
} }
res := &RPCRes{ res := &RPCRes{
...@@ -551,38 +571,52 @@ func TestRPCCacheEthCall(t *testing.T) { ...@@ -551,38 +571,52 @@ func TestRPCCacheEthCall(t *testing.T) {
ID: ID, ID: ID,
} }
err := cache.PutRPC(ctx, req, res, blockHead) t.Run("finalized block", func(t *testing.T) {
require.NoError(t, err) blockHead = 0x100
cachedRes, err := cache.GetRPC(ctx, req) cache := makeCache()
require.NoError(t, err) err := cache.PutRPC(ctx, req, res)
require.Equal(t, res, cachedRes) require.NoError(t, err)
cachedRes, err := cache.GetRPC(ctx, req)
// scenario: no new block, but we've exceeded cacheTTL require.NoError(t, err)
resetCache() require.Equal(t, res, cachedRes)
cacheTTL = 24 * time.Hour })
err = cache.PutRPC(ctx, req, res, blockHead)
require.NoError(t, err) t.Run("unconfirmed block", func(t *testing.T) {
cachedRes, err = cache.GetRPC(ctx, req) blockHead = 0x10
require.NoError(t, err) cache := makeCache()
require.Equal(t, res, cachedRes) require.NoError(t, cache.PutRPC(ctx, req, res))
cachedRes, err := cache.GetRPC(ctx, req)
// scenario: new block, but cached TTL is live require.NoError(t, err)
resetCache() require.Nil(t, cachedRes)
cacheTTL = 24 * time.Hour })
err = cache.PutRPC(ctx, req, res, blockHead)
require.NoError(t, err) t.Run("latest block", func(t *testing.T) {
blockHead += 1 // new block blockHead = 0x100
cachedRes, err = cache.GetRPC(ctx, req) req := &RPCReq{
require.NoError(t, err) JSONRPC: "2.0",
require.Equal(t, res, cachedRes) Method: "eth_call",
Params: []byte(`[{"to": "0xDEADBEEF", "data": "0x1"}, "latest"]`),
// scenario: new block, cache TTL exceeded; cache invalidation ID: ID,
resetCache() }
cacheTTL = 0 * time.Second cache := makeCache()
err = cache.PutRPC(ctx, req, res, blockHead) require.NoError(t, cache.PutRPC(ctx, req, res))
require.NoError(t, err) cachedRes, err := cache.GetRPC(ctx, req)
blockHead += 1 // new block require.NoError(t, err)
cachedRes, err = cache.GetRPC(ctx, req) require.Nil(t, cachedRes)
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 {
......
...@@ -2,7 +2,6 @@ package proxyd ...@@ -2,7 +2,6 @@ package proxyd
import ( import (
"context" "context"
"sync"
"time" "time"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
...@@ -11,20 +10,21 @@ import ( ...@@ -11,20 +10,21 @@ import (
const cacheSyncRate = 1 * time.Second const cacheSyncRate = 1 * time.Second
type lvcUpdateFn func(context.Context, *ethclient.Client) (interface{}, error) type lvcUpdateFn func(context.Context, *ethclient.Client) (string, error)
type EthLastValueCache struct { type EthLastValueCache struct {
client *ethclient.Client client *ethclient.Client
cache Cache
key string
updater lvcUpdateFn updater lvcUpdateFn
quit chan struct{} quit chan struct{}
mutex sync.RWMutex
value interface{}
} }
func newLVC(client *ethclient.Client, updater lvcUpdateFn) *EthLastValueCache { func newLVC(client *ethclient.Client, cache Cache, cacheKey string, updater lvcUpdateFn) *EthLastValueCache {
return &EthLastValueCache{ return &EthLastValueCache{
client: client, client: client,
cache: cache,
key: cacheKey,
updater: updater, updater: updater,
quit: make(chan struct{}), quit: make(chan struct{}),
} }
...@@ -38,15 +38,18 @@ func (h *EthLastValueCache) Start() { ...@@ -38,15 +38,18 @@ func (h *EthLastValueCache) Start() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
lvcPollTimeGauge.WithLabelValues(h.key).SetToCurrentTime()
value, err := h.getUpdate() value, err := h.getUpdate()
if err != nil { if err != nil {
log.Error("error retrieving latest value", "error", err) log.Error("error retrieving latest value", "key", h.key, "error", err)
continue continue
} }
log.Trace("polling latest value", "value", value) log.Trace("polling latest value", "value", value)
h.mutex.Lock()
h.value = value if err := h.cache.Put(context.Background(), h.key, value); err != nil {
h.mutex.Unlock() log.Error("error writing last value to cache", "key", h.key, "error", err)
}
case <-h.quit: case <-h.quit:
return return
...@@ -55,31 +58,30 @@ func (h *EthLastValueCache) Start() { ...@@ -55,31 +58,30 @@ func (h *EthLastValueCache) Start() {
}() }()
} }
func (h *EthLastValueCache) getUpdate() (interface{}, error) { func (h *EthLastValueCache) getUpdate() (string, error) {
const maxRetries = 5 const maxRetries = 5
var err error var err error
for i := 0; i <= maxRetries; i++ { for i := 0; i <= maxRetries; i++ {
var value interface{} var value string
value, err = h.updater(context.Background(), h.client) value, err = h.updater(context.Background(), h.client)
if err != nil { if err != nil {
backoff := calcBackoff(i) backoff := calcBackoff(i)
log.Warn("http operation failed. retrying...", "error", err, "backoff", backoff) log.Warn("http operation failed. retrying...", "error", err, "backoff", backoff)
lvcErrorsTotal.WithLabelValues(h.key).Inc()
time.Sleep(backoff) time.Sleep(backoff)
continue continue
} }
return value, nil return value, nil
} }
return 0, wrapErr(err, "exceeded retries") return "", wrapErr(err, "exceeded retries")
} }
func (h *EthLastValueCache) Stop() { func (h *EthLastValueCache) Stop() {
close(h.quit) close(h.quit)
} }
func (h *EthLastValueCache) Read() interface{} { func (h *EthLastValueCache) Read(ctx context.Context) (string, error) {
h.mutex.RLock() return h.cache.Get(ctx, h.key)
defer h.mutex.RUnlock()
return h.value
} }
...@@ -6,23 +6,17 @@ import ( ...@@ -6,23 +6,17 @@ import (
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
"time"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/snappy"
) )
const numBlockConfirmations = 50
var ( var (
cacheTTL = 5 * time.Second
errInvalidRPCParams = errors.New("invalid RPC params") errInvalidRPCParams = errors.New("invalid RPC params")
) )
type RPCMethodHandler interface { type RPCMethodHandler interface {
GetRPCMethod(context.Context, *RPCReq) (*RPCRes, error) GetRPCMethod(context.Context, *RPCReq) (*RPCRes, error)
PutRPCMethod(context.Context, *RPCReq, *RPCRes, uint64) error PutRPCMethod(context.Context, *RPCReq, *RPCRes) error
} }
type StaticMethodHandler struct { type StaticMethodHandler struct {
...@@ -42,7 +36,7 @@ func (e *StaticMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*R ...@@ -42,7 +36,7 @@ func (e *StaticMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*R
return cache, nil return cache, nil
} }
func (e *StaticMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes, blockNumSync uint64) error { func (e *StaticMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
e.m.Lock() e.m.Lock()
if e.cache == nil { if e.cache == nil {
e.cache = copyRes(res) e.cache = copyRes(res)
...@@ -52,8 +46,9 @@ func (e *StaticMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res ...@@ -52,8 +46,9 @@ func (e *StaticMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res
} }
type EthGetBlockByNumberMethodHandler struct { type EthGetBlockByNumberMethodHandler struct {
cache Cache cache Cache
getLatestBlockNumFn GetLatestBlockNumFn getLatestBlockNumFn GetLatestBlockNumFn
numBlockConfirmations int
} }
func (e *EthGetBlockByNumberMethodHandler) cacheKey(req *RPCReq) string { func (e *EthGetBlockByNumberMethodHandler) cacheKey(req *RPCReq) string {
...@@ -80,7 +75,7 @@ func (e *EthGetBlockByNumberMethodHandler) GetRPCMethod(ctx context.Context, req ...@@ -80,7 +75,7 @@ func (e *EthGetBlockByNumberMethodHandler) GetRPCMethod(ctx context.Context, req
return getImmutableRPCResponse(ctx, e.cache, key, req) return getImmutableRPCResponse(ctx, e.cache, key, req)
} }
func (e *EthGetBlockByNumberMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes, blockNumberSync uint64) error { func (e *EthGetBlockByNumberMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
if ok, err := e.cacheable(req); !ok || err != nil { if ok, err := e.cacheable(req); !ok || err != nil {
return err return err
} }
...@@ -101,7 +96,7 @@ func (e *EthGetBlockByNumberMethodHandler) PutRPCMethod(ctx context.Context, req ...@@ -101,7 +96,7 @@ func (e *EthGetBlockByNumberMethodHandler) PutRPCMethod(ctx context.Context, req
if err != nil { if err != nil {
return err return err
} }
if curBlock <= blockNum+numBlockConfirmations { if curBlock <= blockNum+uint64(e.numBlockConfirmations) {
return nil return nil
} }
} }
...@@ -111,8 +106,9 @@ func (e *EthGetBlockByNumberMethodHandler) PutRPCMethod(ctx context.Context, req ...@@ -111,8 +106,9 @@ func (e *EthGetBlockByNumberMethodHandler) PutRPCMethod(ctx context.Context, req
} }
type EthGetBlockRangeMethodHandler struct { type EthGetBlockRangeMethodHandler struct {
cache Cache cache Cache
getLatestBlockNumFn GetLatestBlockNumFn getLatestBlockNumFn GetLatestBlockNumFn
numBlockConfirmations int
} }
func (e *EthGetBlockRangeMethodHandler) cacheKey(req *RPCReq) string { func (e *EthGetBlockRangeMethodHandler) cacheKey(req *RPCReq) string {
...@@ -140,7 +136,7 @@ func (e *EthGetBlockRangeMethodHandler) GetRPCMethod(ctx context.Context, req *R ...@@ -140,7 +136,7 @@ func (e *EthGetBlockRangeMethodHandler) GetRPCMethod(ctx context.Context, req *R
return getImmutableRPCResponse(ctx, e.cache, key, req) return getImmutableRPCResponse(ctx, e.cache, key, req)
} }
func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes, blockNumberSync uint64) error { func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
if ok, err := e.cacheable(req); !ok || err != nil { if ok, err := e.cacheable(req); !ok || err != nil {
return err return err
} }
...@@ -158,7 +154,7 @@ func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *R ...@@ -158,7 +154,7 @@ func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *R
if err != nil { if err != nil {
return err return err
} }
if curBlock <= startNum+numBlockConfirmations { if curBlock <= startNum+uint64(e.numBlockConfirmations) {
return nil return nil
} }
} }
...@@ -167,7 +163,7 @@ func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *R ...@@ -167,7 +163,7 @@ func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *R
if err != nil { if err != nil {
return err return err
} }
if curBlock <= endNum+numBlockConfirmations { if curBlock <= endNum+uint64(e.numBlockConfirmations) {
return nil return nil
} }
} }
...@@ -177,58 +173,66 @@ func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *R ...@@ -177,58 +173,66 @@ func (e *EthGetBlockRangeMethodHandler) PutRPCMethod(ctx context.Context, req *R
} }
type EthCallMethodHandler struct { type EthCallMethodHandler struct {
cache Cache cache Cache
getLatestBlockNumFn GetLatestBlockNumFn getLatestBlockNumFn GetLatestBlockNumFn
numBlockConfirmations int
} }
func (e *EthCallMethodHandler) cacheKey(req *RPCReq) string { func (e *EthCallMethodHandler) cacheable(params *ethCallParams, blockTag string) bool {
type ethCallParams struct { if isBlockDependentParam(blockTag) {
From string `json:"from"` return false
To string `json:"to"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Value string `json:"value"`
Data string `json:"data"`
}
var input []json.RawMessage
if err := json.Unmarshal(req.Params, &input); err != nil {
return ""
}
if len(input) != 2 {
return ""
}
var blockTag string
if err := json.Unmarshal(input[1], &blockTag); err != nil {
return ""
}
// The eth_call cache is used as a LVC. Only the latest calls are cached as these are used the most
if blockTag != "latest" {
return ""
}
var params ethCallParams
if err := json.Unmarshal(input[0], &params); err != nil {
return ""
} }
if params.From != "" || params.Gas != "" { if params.From != "" || params.Gas != "" {
return "" return false
} }
if params.Value != "" && params.Value != "0x0" { if params.Value != "" && params.Value != "0x0" {
return "" return false
} }
// ensure the order is consistent return true
keyParams := fmt.Sprintf("%s:%s", params.To, params.Data) }
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) return fmt.Sprintf("method:eth_call:%s", keyParams)
} }
func (e *EthCallMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) { func (e *EthCallMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq) (*RPCRes, error) {
key := e.cacheKey(req) params, blockTag, err := decodeEthCallParams(req)
return getBlockDependentCachedRPCResponse(ctx, e.cache, e.getLatestBlockNumFn, key, 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, blockNumberSync uint64) error { func (e *EthCallMethodHandler) PutRPCMethod(ctx context.Context, req *RPCReq, res *RPCRes) error {
key := e.cacheKey(req) params, blockTag, err := decodeEthCallParams(req)
return putBlockDependentCachedRPCResponse(ctx, e.cache, key, res, blockNumberSync) 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 { type EthBlockNumberMethodHandler struct {
...@@ -243,7 +247,7 @@ func (e *EthBlockNumberMethodHandler) GetRPCMethod(ctx context.Context, req *RPC ...@@ -243,7 +247,7 @@ func (e *EthBlockNumberMethodHandler) GetRPCMethod(ctx context.Context, req *RPC
return makeRPCRes(req, hexutil.EncodeUint64(blockNum)), nil return makeRPCRes(req, hexutil.EncodeUint64(blockNum)), nil
} }
func (e *EthBlockNumberMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRes, uint64) error { func (e *EthBlockNumberMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRes) error {
return nil return nil
} }
...@@ -259,7 +263,7 @@ func (e *EthGasPriceMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq ...@@ -259,7 +263,7 @@ func (e *EthGasPriceMethodHandler) GetRPCMethod(ctx context.Context, req *RPCReq
return makeRPCRes(req, hexutil.EncodeUint64(gasPrice)), nil return makeRPCRes(req, hexutil.EncodeUint64(gasPrice)), nil
} }
func (e *EthGasPriceMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRes, uint64) error { func (e *EthGasPriceMethodHandler) PutRPCMethod(context.Context, *RPCReq, *RPCRes) error {
return nil return nil
} }
...@@ -319,6 +323,34 @@ func decodeBlockInput(input string) (uint64, error) { ...@@ -319,6 +323,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
...@@ -355,100 +387,30 @@ func copyRes(res *RPCRes) *RPCRes { ...@@ -355,100 +387,30 @@ func copyRes(res *RPCRes) *RPCRes {
} }
} }
type CachedRPC struct {
BlockNum uint64 `json:"blockNum"`
Res *RPCRes `json:"res"`
Expiration int64 `json:"expiration"` // in millis since epoch
}
func (c *CachedRPC) Encode() []byte {
return mustMarshalJSON(c)
}
func (c *CachedRPC) Decode(b []byte) error {
return json.Unmarshal(b, c)
}
func (c *CachedRPC) ExpirationTime() time.Time {
return time.Unix(0, c.Expiration*int64(time.Millisecond))
}
func getImmutableRPCResponse(ctx context.Context, cache Cache, key string, req *RPCReq) (*RPCRes, error) { func getImmutableRPCResponse(ctx context.Context, cache Cache, key string, req *RPCReq) (*RPCRes, error) {
encodedVal, err := cache.Get(ctx, key) val, err := cache.Get(ctx, key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if encodedVal == "" { if val == "" {
return nil, nil return nil, nil
} }
val, err := snappy.Decode(nil, []byte(encodedVal))
if err != nil {
return nil, err
}
res := new(RPCRes) var result interface{}
if err := json.Unmarshal(val, res); err != nil { if err := json.Unmarshal([]byte(val), &result); err != nil {
return nil, err return nil, err
} }
res.ID = req.ID return &RPCRes{
return res, nil JSONRPC: req.JSONRPC,
Result: result,
ID: req.ID,
}, nil
} }
func putImmutableRPCResponse(ctx context.Context, cache Cache, key string, req *RPCReq, res *RPCRes) error { func putImmutableRPCResponse(ctx context.Context, cache Cache, key string, req *RPCReq, res *RPCRes) error {
if key == "" { if key == "" {
return nil return nil
} }
val := mustMarshalJSON(res) val := mustMarshalJSON(res.Result)
encodedVal := snappy.Encode(nil, val) return cache.Put(ctx, key, string(val))
return cache.Put(ctx, key, string(encodedVal))
}
func getBlockDependentCachedRPCResponse(ctx context.Context, cache Cache, getLatestBlockNumFn GetLatestBlockNumFn, key string, req *RPCReq) (*RPCRes, error) {
encodedVal, err := cache.Get(ctx, key)
if err != nil {
return nil, err
}
if encodedVal == "" {
return nil, nil
}
val, err := snappy.Decode(nil, []byte(encodedVal))
if err != nil {
return nil, err
}
item := new(CachedRPC)
if err := json.Unmarshal(val, item); err != nil {
return nil, err
}
curBlockNum, err := getLatestBlockNumFn(ctx)
if err != nil {
return nil, err
}
expired := time.Now().After(item.ExpirationTime())
if curBlockNum > item.BlockNum && expired {
// Remove the key now to avoid stale entries from biasing the LRU list
// TODO: be careful removing keys once there are multiple proxyd instances
return nil, cache.Remove(ctx, key)
} else if curBlockNum < item.BlockNum { /* desync: reorgs, backend failover, slow sequencer I/O, etc */
return nil, nil
}
res := item.Res
res.ID = req.ID
return res, nil
}
func putBlockDependentCachedRPCResponse(ctx context.Context, cache Cache, key string, res *RPCRes, blockNumberSync uint64) error {
if key == "" {
return nil
}
item := CachedRPC{
BlockNum: blockNumberSync,
Res: res,
Expiration: time.Now().Add(cacheTTL).UnixNano() / int64(time.Millisecond),
}
val := item.Encode()
encodedVal := snappy.Encode(nil, val)
return cache.Put(ctx, key, string(encodedVal))
} }
...@@ -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",
......
...@@ -5,9 +5,9 @@ import ( ...@@ -5,9 +5,9 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"math/big"
"net/http" "net/http"
"os" "os"
"strconv"
"time" "time"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
...@@ -111,12 +111,7 @@ func Start(config *Config) (func(), error) { ...@@ -111,12 +111,7 @@ func Start(config *Config) (func(), error) {
opts = append(opts, WithStrippedTrailingXFF()) opts = append(opts, WithStrippedTrailingXFF())
} }
opts = append(opts, WithProxydIP(os.Getenv("PROXYD_IP"))) opts = append(opts, WithProxydIP(os.Getenv("PROXYD_IP")))
back, err := NewBackend(name, rpcURL, wsURL, lim, opts...) back := NewBackend(name, rpcURL, wsURL, lim, opts...)
if err != nil {
return nil, err
}
back.Start()
defer back.Stop()
backendNames = append(backendNames, name) backendNames = append(backendNames, name)
backendsByName[name] = back backendsByName[name] = back
log.Info("configured backend", "name", name, "rpc_url", rpcURL, "ws_url", wsURL) log.Info("configured backend", "name", name, "rpc_url", rpcURL, "ws_url", wsURL)
...@@ -169,9 +164,18 @@ func Start(config *Config) (func(), error) { ...@@ -169,9 +164,18 @@ func Start(config *Config) (func(), error) {
} }
} }
var rpcCache RPCCache var (
stopLVCs := make(chan struct{}) rpcCache RPCCache
blockNumLVC *EthLastValueCache
gasPriceLVC *EthLastValueCache
)
if config.Cache.Enabled { if config.Cache.Enabled {
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")
} }
...@@ -180,7 +184,6 @@ func Start(config *Config) (func(), error) { ...@@ -180,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
...@@ -195,9 +198,10 @@ func Start(config *Config) (func(), error) { ...@@ -195,9 +198,10 @@ func Start(config *Config) (func(), error) {
return nil, err return nil, err
} }
defer ethClient.Close() defer ethClient.Close()
blockNumFn := makeGetLatestBlockNumFn(ethClient, stopLVCs)
gasPriceFn := makeGetLatestGasPriceFn(ethClient, stopLVCs) blockNumLVC, blockNumFn = makeGetLatestBlockNumFn(ethClient, cache)
rpcCache = newRPCCache(cache, blockNumFn, gasPriceFn) gasPriceLVC, gasPriceFn = makeGetLatestGasPriceFn(ethClient, cache)
rpcCache = newRPCCache(newCacheWithCompression(cache), blockNumFn, gasPriceFn, config.Cache.NumBlockConfirmations)
} }
srv := NewServer( srv := NewServer(
...@@ -250,8 +254,12 @@ func Start(config *Config) (func(), error) { ...@@ -250,8 +254,12 @@ func Start(config *Config) (func(), error) {
return func() { return func() {
log.Info("shutting down proxyd") log.Info("shutting down proxyd")
// TODO(inphi): Stop LVCs here if blockNumLVC != nil {
close(stopLVCs) 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 {
log.Error("error flushing backend ws conns", "err", err) log.Error("error flushing backend ws conns", "err", err)
...@@ -285,38 +293,38 @@ func configureBackendTLS(cfg *BackendConfig) (*tls.Config, error) { ...@@ -285,38 +293,38 @@ func configureBackendTLS(cfg *BackendConfig) (*tls.Config, error) {
return tlsConfig, nil return tlsConfig, nil
} }
func makeGetLatestBlockNumFn(client *ethclient.Client, quit <-chan struct{}) GetLatestBlockNumFn { func makeUint64LastValueFn(client *ethclient.Client, cache Cache, key string, updater lvcUpdateFn) (*EthLastValueCache, func(context.Context) (uint64, error)) {
lvc := newLVC(client, func(ctx context.Context, c *ethclient.Client) (interface{}, error) { lvc := newLVC(client, cache, key, updater)
return c.BlockNumber(ctx)
})
lvc.Start() lvc.Start()
go func() { return lvc, func(ctx context.Context) (uint64, error) {
<-quit value, err := lvc.Read(ctx)
lvc.Stop() if err != nil {
}() return 0, err
return func(ctx context.Context) (uint64, error) { }
value := lvc.Read() if value == "" {
if value == nil { return 0, fmt.Errorf("%s is unavailable", key)
return 0, fmt.Errorf("block number is unavailable") }
} valueUint, err := strconv.ParseUint(value, 10, 64)
return value.(uint64), nil if err != nil {
return 0, err
}
return valueUint, nil
} }
} }
func makeGetLatestGasPriceFn(client *ethclient.Client, quit <-chan struct{}) GetLatestGasPriceFn { func makeGetLatestBlockNumFn(client *ethclient.Client, cache Cache) (*EthLastValueCache, GetLatestBlockNumFn) {
lvc := newLVC(client, func(ctx context.Context, c *ethclient.Client) (interface{}, error) { return makeUint64LastValueFn(client, cache, "lvc:block_number", func(ctx context.Context, c *ethclient.Client) (string, error) {
return c.SuggestGasPrice(ctx) 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
}) })
lvc.Start()
go func() {
<-lvc.quit
lvc.Stop()
}()
return func(ctx context.Context) (uint64, error) {
value := lvc.Read()
if value == nil {
return 0, fmt.Errorf("gas price is unavailable")
}
return value.(*big.Int).Uint64(), nil
}
} }
...@@ -218,9 +218,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes { ...@@ -218,9 +218,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
return backendRes return backendRes
} }
// NOTE: We call into the specific backend here to ensure that the RPCRes is synchronized with the blockNum. backendRes, err = s.backendGroups[group].Forward(ctx, req)
var blockNum uint64
backendRes, blockNum, err = s.backendGroups[group].Forward(ctx, req)
if err != nil { if err != nil {
log.Error( log.Error(
"error forwarding RPC request", "error forwarding RPC request",
...@@ -232,7 +230,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes { ...@@ -232,7 +230,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
} }
if backendRes.Error == nil { if backendRes.Error == nil {
if err = s.cache.PutRPC(ctx, req, backendRes, blockNum); err != nil { if err = s.cache.PutRPC(ctx, req, backendRes); err != nil {
log.Warn( log.Warn(
"cache put error", "cache put error",
"req_id", GetReqID(ctx), "req_id", GetReqID(ctx),
...@@ -427,6 +425,6 @@ func (n *NoopRPCCache) GetRPC(context.Context, *RPCReq) (*RPCRes, error) { ...@@ -427,6 +425,6 @@ func (n *NoopRPCCache) GetRPC(context.Context, *RPCReq) (*RPCRes, error) {
return nil, nil return nil, nil
} }
func (n *NoopRPCCache) PutRPC(context.Context, *RPCReq, *RPCRes, uint64) error { func (n *NoopRPCCache) PutRPC(context.Context, *RPCReq, *RPCRes) error {
return nil return nil
} }
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