Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix cache for authenticated method calls #1808

Merged
merged 4 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
}
34 changes: 17 additions & 17 deletions tools/walletextension/wallet_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,20 @@ func (w *WalletExtension) ProxyEthRequest(request *common.RPCRequest, conn userc
// start measuring time for request
requestStartTime := time.Now()

//// Check if the request is in the cache
//isCacheable, key, ttl := cache.IsCacheable(request)
//
//// in case of cache hit return the response from the cache
//if isCacheable {
// if value, ok := w.cache.Get(key); ok {
// requestEndTime := time.Now()
// duration := requestEndTime.Sub(requestStartTime)
// w.fileLogger.Info(fmt.Sprintf("Request method: %s, request params: %s, encryptionToken of sender: %s, response: %s, duration: %d ", request.Method, request.Params, hexUserID, value, duration.Milliseconds()))
// // adjust requestID
// value[common.JSONKeyID] = request.ID
// return value, nil
// }
//}
// Check if the request is in the cache
isCacheable, key, ttl := cache.IsCacheable(request, hexUserID)

// in case of cache hit return the response from the cache
if isCacheable {
if value, ok := w.cache.Get(key); ok {
requestEndTime := time.Now()
duration := requestEndTime.Sub(requestStartTime)
w.fileLogger.Info(fmt.Sprintf("Request method: %s, request params: %s, encryptionToken of sender: %s, response: %s, duration: %d ", request.Method, request.Params, hexUserID, value, duration.Milliseconds()))
// adjust requestID
value[common.JSONKeyID] = request.ID
return value, nil
}
}

// proxyRequest will find the correct client to proxy the request (or try them all if appropriate)
var rpcResp interface{}
Expand Down Expand Up @@ -166,9 +166,9 @@ func (w *WalletExtension) ProxyEthRequest(request *common.RPCRequest, conn userc
w.fileLogger.Info(fmt.Sprintf("Request method: %s, request params: %s, encryptionToken of sender: %s, response: %s, duration: %d ", request.Method, request.Params, hexUserID, response, duration.Milliseconds()))

// if the request is cacheable, store the response in the cache
//if isCacheable {
// w.cache.Set(key, response, ttl)
//}
if isCacheable {
w.cache.Set(key, response, ttl)
}

return response, nil
}
Expand Down
Loading