Skip to content

Commit

Permalink
fix cache for authenticated method calls (#1809)
Browse files Browse the repository at this point in the history
  • Loading branch information
zkokelj authored Feb 22, 2024
1 parent e4e6db9 commit 7049486
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 36 deletions.
65 changes: 39 additions & 26 deletions tools/walletextension/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,30 @@ const (
shortCacheTTL = 1 * time.Second
)

// Define a struct to hold the cache TTL and auth requirement
type RPCMethodCacheConfig struct {
CacheTTL time.Duration
RequiresAuth bool
}

// CacheableRPCMethods is a map of Ethereum JSON-RPC methods that can be cached and their TTL
var cacheableRPCMethods = map[string]time.Duration{
var cacheableRPCMethods = map[string]RPCMethodCacheConfig{
// Ethereum JSON-RPC methods that can be cached long time
"eth_getBlockByNumber": longCacheTTL,
"eth_getBlockByHash": longCacheTTL,
"eth_getTransactionByHash": longCacheTTL,
"eth_chainId": longCacheTTL,
"eth_getBlockByNumber": {longCacheTTL, false},
"eth_getBlockByHash": {longCacheTTL, false},
"eth_getTransactionByHash": {longCacheTTL, true},
"eth_chainId": {longCacheTTL, false},

// Ethereum JSON-RPC methods that can be cached short time
"eth_blockNumber": shortCacheTTL,
"eth_getCode": shortCacheTTL,
// "eth_getBalance": shortCacheTTL,// excluded for test: gen_cor_059
"eth_getTransactionReceipt": shortCacheTTL,
"eth_call": shortCacheTTL,
"eth_gasPrice": shortCacheTTL,
// "eth_getTransactionCount": shortCacheTTL, // excluded for test: gen_cor_009
"eth_estimateGas": shortCacheTTL,
"eth_feeHistory": shortCacheTTL,
"eth_blockNumber": {shortCacheTTL, false},
"eth_getCode": {shortCacheTTL, true},
// "eth_getBalance": {longCacheTTL, true},// excluded for test: gen_cor_059
"eth_getTransactionReceipt": {shortCacheTTL, true},
"eth_call": {shortCacheTTL, true},
"eth_gasPrice": {shortCacheTTL, false},
// "eth_getTransactionCount": {longCacheTTL, true}, // excluded for test: gen_cor_009
"eth_estimateGas": {shortCacheTTL, true},
"eth_feeHistory": {shortCacheTTL, false},
}

type Cache interface {
Expand All @@ -46,48 +52,55 @@ func NewCache(logger log.Logger) (Cache, error) {
}

// IsCacheable checks if the given RPC request is cacheable and returns the cache key and TTL
func IsCacheable(key *common.RPCRequest) (bool, string, time.Duration) {
func IsCacheable(key *common.RPCRequest, encryptionToken string) (bool, string, time.Duration) {
if key == nil || key.Method == "" {
return false, "", 0
}

// Check if the method is cacheable
ttl, isCacheable := cacheableRPCMethods[key.Method]
methodCacheConfig, isCacheable := cacheableRPCMethods[key.Method]

// If method does not need to be authenticated, we can don't need to cache it per user
if !methodCacheConfig.RequiresAuth {
encryptionToken = ""
}

if isCacheable {
// method is cacheable - select cache key
// method is cacheable - select cache key and ttl
switch key.Method {
case "eth_getCode", "eth_getBalance", "eth_getTransactionCount", "eth_estimateGas", "eth_call":
case "eth_getCode", "eth_getBalance", "eth_estimateGas", "eth_call":
if len(key.Params) == 1 || len(key.Params) == 2 && (key.Params[1] == "latest" || key.Params[1] == "pending") {
return true, GenerateCacheKey(key.Method, key.Params...), ttl
return true, GenerateCacheKey(key.Method, encryptionToken, key.Params...), methodCacheConfig.CacheTTL
}
// in this case, we have a fixed block number, and we can cache the result for a long time
return true, GenerateCacheKey(key.Method, key.Params...), longCacheTTL
return true, GenerateCacheKey(key.Method, encryptionToken, key.Params...), longCacheTTL
case "eth_feeHistory":
if len(key.Params) == 2 || len(key.Params) == 3 && (key.Params[2] == "latest" || key.Params[2] == "pending") {
return true, GenerateCacheKey(key.Method, key.Params...), ttl
return true, GenerateCacheKey(key.Method, encryptionToken, key.Params...), methodCacheConfig.CacheTTL
}
// in this case, we have a fixed block number, and we can cache the result for a long time
return true, GenerateCacheKey(key.Method, key.Params...), longCacheTTL
return true, GenerateCacheKey(key.Method, encryptionToken, key.Params...), longCacheTTL

default:
return true, GenerateCacheKey(key.Method, key.Params...), ttl
return true, GenerateCacheKey(key.Method, encryptionToken, key.Params...), methodCacheConfig.CacheTTL
}
}

// method is not cacheable
return false, "", 0
}

// GenerateCacheKey generates a cache key for the given method and parameters
func GenerateCacheKey(method string, params ...interface{}) string {
// GenerateCacheKey generates a cache key for the given method, encryptionToken and parameters
// encryptionToken is used to generate a unique cache key for each user and empty string should be used for public data
func GenerateCacheKey(method string, encryptionToken string, params ...interface{}) string {
// Serialize parameters
paramBytes, err := json.Marshal(params)
if err != nil {
return ""
}

// Concatenate method name and parameters
rawKey := method + string(paramBytes)
rawKey := method + encryptionToken + string(paramBytes)

// Optional: Apply hashing
hasher := sha256.New()
Expand Down
120 changes: 111 additions & 9 deletions tools/walletextension/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ var tests = map[string]func(t *testing.T){
}

var cacheTests = map[string]func(cache Cache, t *testing.T){
"testResultsAreCached": testResultsAreCached,
"testCacheTTL": testCacheTTL,
"testResultsAreCached": testResultsAreCached,
"testCacheTTL": testCacheTTL,
"testCachingAuthenticatedMethods": testCachingAuthenticatedMethods,
"testCachingNonAuthenticatedMethods": testCachingNonAuthenticatedMethods,
}

var nonCacheableMethods = []string{"eth_sendrawtransaction", "eth_sendtransaction", "join", "authenticate"}
var (
nonCacheableMethods = []string{"eth_sendrawtransaction", "eth_sendtransaction", "join", "authenticate"}
encryptionToken = "test"
encryptionToken2 = "not-test"
)

func TestGatewayCaching(t *testing.T) {
for name, test := range tests {
Expand All @@ -47,7 +53,7 @@ func TestGatewayCaching(t *testing.T) {
func testCacheableMethods(t *testing.T) {
for method := range cacheableRPCMethods {
key := &common.RPCRequest{Method: method}
isCacheable, _, _ := IsCacheable(key)
isCacheable, _, _ := IsCacheable(key, encryptionToken)
if isCacheable != true {
t.Errorf("method %s should be cacheable", method)
}
Expand All @@ -58,7 +64,7 @@ func testCacheableMethods(t *testing.T) {
func testNonCacheableMethods(t *testing.T) {
for _, method := range nonCacheableMethods {
key := &common.RPCRequest{Method: method}
isCacheable, _, _ := IsCacheable(key)
isCacheable, _, _ := IsCacheable(key, encryptionToken)
if isCacheable == true {
t.Errorf("method %s should not be cacheable", method)
}
Expand All @@ -70,13 +76,13 @@ func testMethodsWithLatestOrPendingParameter(t *testing.T) {
methods := []string{"eth_getCode", "eth_estimateGas", "eth_call"}
for _, method := range methods {
key := &common.RPCRequest{Method: method, Params: []interface{}{"0x123", "latest"}}
_, _, ttl := IsCacheable(key)
_, _, ttl := IsCacheable(key, encryptionToken)
if ttl != shortCacheTTL {
t.Errorf("method %s with latest parameter should have TTL of %s, but %s received", method, shortCacheTTL, ttl)
}

key = &common.RPCRequest{Method: method, Params: []interface{}{"0x123", "pending"}}
_, _, ttl = IsCacheable(key)
_, _, ttl = IsCacheable(key, encryptionToken)
if ttl != shortCacheTTL {
t.Errorf("method %s with pending parameter should have TTL of %s, but %s received", method, shortCacheTTL, ttl)
}
Expand All @@ -88,7 +94,7 @@ func testResultsAreCached(cache Cache, t *testing.T) {
// prepare a cacheable request and imaginary response
req := &common.RPCRequest{Method: "eth_getBlockByNumber", Params: []interface{}{"0x123"}}
res := map[string]interface{}{"result": "block"}
isCacheable, key, ttl := IsCacheable(req)
isCacheable, key, ttl := IsCacheable(req, encryptionToken)
if !isCacheable {
t.Errorf("method %s should be cacheable", req.Method)
}
Expand All @@ -112,7 +118,7 @@ func testResultsAreCached(cache Cache, t *testing.T) {
func testCacheTTL(cache Cache, t *testing.T) {
req := &common.RPCRequest{Method: "eth_blockNumber", Params: []interface{}{"0x123"}}
res := map[string]interface{}{"result": "100"}
isCacheable, key, ttl := IsCacheable(req)
isCacheable, key, ttl := IsCacheable(req, encryptionToken)

if !isCacheable {
t.Errorf("method %s should be cacheable", req.Method)
Expand Down Expand Up @@ -145,3 +151,99 @@ func testCacheTTL(cache Cache, t *testing.T) {
t.Errorf("value should not be in the cache after TTL")
}
}

func testCachingAuthenticatedMethods(cache Cache, t *testing.T) {
// eth_getTransactionByHash
authMethods := []string{
"eth_getTransactionByHash",
"eth_getCode",
"eth_getTransactionReceipt",
"eth_call",
"eth_estimateGas",
}
for _, method := range authMethods {
req := &common.RPCRequest{Method: method, Params: []interface{}{"0x123"}}
res := map[string]interface{}{"result": "transaction"}

// store the response in cache for the first user using encryptionToken
isCacheable, key, ttl := IsCacheable(req, encryptionToken)

if !isCacheable {
t.Errorf("method %s should be cacheable", req.Method)
}

// set the response in the cache with a TTL
if !cache.Set(key, res, ttl) {
t.Errorf("failed to set value in cache for %s", req)
}
time.Sleep(50 * time.Millisecond) // wait for the cache to be set

// check if the value is in the cache
value, ok := cache.Get(key)
if !ok {
t.Errorf("failed to get cached value for %s", req)
}

// for the first error we should have the value in cache
if !reflect.DeepEqual(value, res) {
t.Errorf("expected %v, got %v", res, value)
}

// now check with the second user asking for the same request, but with a different encryptionToken
_, key2, _ := IsCacheable(req, encryptionToken2)

_, okSecondUser := cache.Get(key2)
if okSecondUser {
t.Errorf("another user should not see a value the first user cached %s", req)
}
}
}

func testCachingNonAuthenticatedMethods(cache Cache, t *testing.T) {
// eth_getTransactionByHash
nonAuthMethods := []string{
"eth_getBlockByNumber",
"eth_getBlockByHash",
"eth_chainId",
"eth_blockNumber",
"eth_gasPrice",
"eth_feeHistory",
}

for _, method := range nonAuthMethods {
req := &common.RPCRequest{Method: method, Params: []interface{}{"0x123"}}
res := map[string]interface{}{"result": "transaction"}

// store the response in cache for the first user using encryptionToken
isCacheable, key, ttl := IsCacheable(req, encryptionToken)

if !isCacheable {
t.Errorf("method %s should be cacheable", req.Method)
}

// set the response in the cache with a TTL
if !cache.Set(key, res, ttl) {
t.Errorf("failed to set value in cache for %s", req)
}
time.Sleep(50 * time.Millisecond) // wait for the cache to be set

// check if the value is in the cache
value, ok := cache.Get(key)
if !ok {
t.Errorf("failed to get cached value for %s", req)
}

// for the first error we should have the value in cache
if !reflect.DeepEqual(value, res) {
t.Errorf("expected %v, got %v", res, value)
}

// now check with the second user asking for the same request, but with a different encryptionToken
_, key2, _ := IsCacheable(req, encryptionToken2)

_, okSecondUser := cache.Get(key2)
if !okSecondUser {
t.Errorf("another user should see a value the first user cached %s", req)
}
}
}
2 changes: 1 addition & 1 deletion tools/walletextension/wallet_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (w *WalletExtension) ProxyEthRequest(request *common.RPCRequest, conn userc
requestStartTime := time.Now()

// Check if the request is in the cache
isCacheable, key, ttl := cache.IsCacheable(request)
isCacheable, key, ttl := cache.IsCacheable(request, hexUserID)

// in case of cache hit return the response from the cache
if isCacheable {
Expand Down

0 comments on commit 7049486

Please sign in to comment.