diff --git a/tools/walletextension/cache/cache.go b/tools/walletextension/cache/cache.go index e1ebc81223..74be7c3a7b 100644 --- a/tools/walletextension/cache/cache.go +++ b/tools/walletextension/cache/cache.go @@ -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 { @@ -46,31 +52,37 @@ 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 } } @@ -78,8 +90,9 @@ func IsCacheable(key *common.RPCRequest) (bool, string, time.Duration) { 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 { @@ -87,7 +100,7 @@ func GenerateCacheKey(method string, params ...interface{}) string { } // Concatenate method name and parameters - rawKey := method + string(paramBytes) + rawKey := method + encryptionToken + string(paramBytes) // Optional: Apply hashing hasher := sha256.New() diff --git a/tools/walletextension/cache/cache_test.go b/tools/walletextension/cache/cache_test.go index d5f714d6d6..30012f9dcf 100644 --- a/tools/walletextension/cache/cache_test.go +++ b/tools/walletextension/cache/cache_test.go @@ -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 { @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) @@ -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) + } + } +} diff --git a/tools/walletextension/wallet_extension.go b/tools/walletextension/wallet_extension.go index df3af1ed9d..242b60ea5a 100644 --- a/tools/walletextension/wallet_extension.go +++ b/tools/walletextension/wallet_extension.go @@ -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 {