-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/ten-protocol/go-ten into je…
…nnifer/2964-update-tenscan-logo
- Loading branch information
Showing
7 changed files
with
464 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Ten Gateway Caching Design | ||
|
||
## 1. Why cache requests on Gateway? | ||
|
||
Currently, all `eth_` requests that hit the gateway are forwarded to the Ten Nodes and are executed by the Ten enclave. | ||
This is not ideal since there is only one Sequencer and it can be overloaded with requests | ||
and there can be timeouts or errors in the sequencer. | ||
|
||
To solve this problem, we can cache the responses of the `eth_` requests on the Gateway. | ||
Not all requests can be cached, that is why I analyzed the Ethereum JSON-RPC Specification and formed two groups of requests that can be cached and those that cannot be cached. | ||
For some methods, I used a rule of thumb since results can be cached for a certain period of time, but quickly become outdated. | ||
|
||
|
||
Cacheable request methods are: | ||
|
||
- `eth_accounts` | ||
- `eth_chainID` | ||
- `eth_coinbase` | ||
- `eth_getBlockByHash` | ||
- `eth_getBlockByNumber` | ||
- `eth_getBlockRecipients` | ||
- `eth_getBlockTransactionCountByHash` | ||
- `eth_getBlockTransactionCountByNumber` | ||
- `eth_getCode` | ||
- `eth_getStorageAt` | ||
- `eth_getTransactionByBlockHashAndIndex` | ||
- `eth_getTransactionByBlockNumberAndIndex` | ||
- `eth_getTransactionByHash` | ||
- `eth_getTransactionReceipt` | ||
- `eth_getUncleCountByBlockHash` | ||
- `eth_getUncleCountByBlockNumber` | ||
- `eth_maxPriorityFeePerGas` | ||
- `eth_sign` | ||
- `eth_signTransaction` | ||
|
||
Methods cachable for a short period of time (TTL until next batch): | ||
|
||
- `eth_getBalance` | ||
- `eth_blockNumber` | ||
- `eth_call` | ||
- `eth_createAccessList` | ||
- `eth_estimateGas` | ||
- `eth_feeHistory` | ||
- `eth_getProof` | ||
- `eth_gasPrice` | ||
- `eth_getFilterChanges` | ||
- `eth_getFilterLogs` | ||
- `eth_getTransactionCount` | ||
- `eth_newBlockFilter` | ||
- `eth_newFilter` | ||
- `eth_newPendingTransactionFilter` | ||
- `eth_sendRawTransaction` | ||
- `eth_sendTransaction` | ||
- `eth_syncing` | ||
- `eth_uninstallFilter` | ||
|
||
### Expiration time | ||
Some cacheable methods will always produce the same result and can be cached indefinitely, while others will have a short expiration time. | ||
Even for those that can be cached indefinitely I don't think it's a good idea to cache them for a long time, | ||
since a large percentage of the requests will probably be requested only once | ||
and caching will only consume memory and don't provide any benefit. | ||
As far as I can see it can help | ||
to cache the results for shorter amount of time to help reduce the load on the sequencer / help in case of DDoS attacks | ||
(but we need to do also other things to prevent them). | ||
|
||
|
||
|
||
## 3. Implementation | ||
|
||
For the implementation we can use some of the Go libraries for caching, such as: https://github.com/patrickmn/go-cache. | ||
All the requests come from a single point in the code, so we can easily add caching to the gateway. | ||
|
||
|
||
|
||
## Pros and cos of caching | ||
I want to present also some negative effects of caching, so we can make a better decision. | ||
|
||
### Pros | ||
- it can reduce the load on the sequencer | ||
- cah help if there is a spike of requests (e.g. DDoS attack / high load from specific user) | ||
|
||
### Cons | ||
- caching can consume additional memory | ||
- caching can lead to outdated results (only in some methods) | ||
- it is not a good way to prevent DDoS attacks, since users can easily request non-cacheable methods |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package cache | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/log" | ||
|
||
"github.com/dgraph-io/ristretto" | ||
) | ||
|
||
const ( | ||
numCounters = 1e7 // number of keys to track frequency of (10M). | ||
maxCost = 1 << 30 // maximum cost of cache (1GB). | ||
bufferItems = 64 // number of keys per Get buffer. | ||
defaultConst = 1 // default cost of cache. | ||
) | ||
|
||
type RistrettoCache struct { | ||
cache *ristretto.Cache | ||
quit chan struct{} | ||
} | ||
|
||
// NewRistrettoCache returns a new RistrettoCache. | ||
func NewRistrettoCache(logger log.Logger) (*RistrettoCache, error) { | ||
cache, err := ristretto.NewCache(&ristretto.Config{ | ||
NumCounters: numCounters, | ||
MaxCost: maxCost, | ||
BufferItems: bufferItems, | ||
Metrics: true, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
c := &RistrettoCache{ | ||
cache: cache, | ||
quit: make(chan struct{}), | ||
} | ||
|
||
// Start the metrics logging | ||
go c.startMetricsLogging(logger) | ||
|
||
return c, nil | ||
} | ||
|
||
// Set adds the key and value to the cache. | ||
func (c *RistrettoCache) Set(key string, value map[string]interface{}, ttl time.Duration) bool { | ||
return c.cache.SetWithTTL(key, value, defaultConst, ttl) | ||
} | ||
|
||
// Get returns the value for the given key if it exists. | ||
func (c *RistrettoCache) Get(key string) (value map[string]interface{}, ok bool) { | ||
item, found := c.cache.Get(key) | ||
if !found { | ||
return nil, false | ||
} | ||
|
||
// Assuming the item is stored as a map[string]interface{}, otherwise you need to type assert to the correct type. | ||
value, ok = item.(map[string]interface{}) | ||
if !ok { | ||
// The item isn't of type map[string]interface{} | ||
return nil, false | ||
} | ||
|
||
return value, true | ||
} | ||
|
||
// startMetricsLogging starts logging cache metrics every hour. | ||
func (c *RistrettoCache) startMetricsLogging(logger log.Logger) { | ||
ticker := time.NewTicker(1 * time.Hour) | ||
for { | ||
select { | ||
case <-ticker.C: | ||
metrics := c.cache.Metrics | ||
logger.Info("Cache metrics: Hits: %d, Misses: %d, Cost Added: %d\n", | ||
metrics.Hits(), metrics.Misses(), metrics.CostAdded()) | ||
case <-c.quit: | ||
ticker.Stop() | ||
return | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package cache | ||
|
||
import ( | ||
"crypto/sha256" | ||
"encoding/json" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/log" | ||
|
||
"github.com/ten-protocol/go-ten/tools/walletextension/common" | ||
) | ||
|
||
const ( | ||
longCacheTTL = 5 * time.Hour | ||
shortCacheTTL = 1 * time.Second | ||
) | ||
|
||
// CacheableRPCMethods is a map of Ethereum JSON-RPC methods that can be cached and their TTL | ||
var cacheableRPCMethods = map[string]time.Duration{ | ||
// Ethereum JSON-RPC methods that can be cached long time | ||
"eth_getBlockByNumber": longCacheTTL, | ||
"eth_getBlockByHash": longCacheTTL, | ||
"eth_getTransactionByHash": longCacheTTL, | ||
"eth_chainId": longCacheTTL, | ||
|
||
// Ethereum JSON-RPC methods that can be cached short time | ||
"eth_blockNumber": shortCacheTTL, | ||
"eth_getCode": shortCacheTTL, | ||
"eth_getBalance": shortCacheTTL, | ||
"eth_getTransactionReceipt": shortCacheTTL, | ||
"eth_call": shortCacheTTL, | ||
"eth_gasPrice": shortCacheTTL, | ||
"eth_getTransactionCount": shortCacheTTL, | ||
"eth_estimateGas": shortCacheTTL, | ||
"eth_feeHistory": shortCacheTTL, | ||
} | ||
|
||
type Cache interface { | ||
Set(key string, value map[string]interface{}, ttl time.Duration) bool | ||
Get(key string) (value map[string]interface{}, ok bool) | ||
} | ||
|
||
func NewCache(logger log.Logger) (Cache, error) { | ||
return NewRistrettoCache(logger) | ||
} | ||
|
||
// 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) { | ||
if key == nil || key.Method == "" { | ||
return false, "", 0 | ||
} | ||
|
||
// Check if the method is cacheable | ||
ttl, isCacheable := cacheableRPCMethods[key.Method] | ||
|
||
if isCacheable { | ||
// method is cacheable - select cache key | ||
switch key.Method { | ||
case "eth_getCode", "eth_getBalance", "eth_getTransactionCount", "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 | ||
} | ||
// 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 | ||
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 | ||
} | ||
// 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 | ||
default: | ||
return true, GenerateCacheKey(key.Method, key.Params...), ttl | ||
} | ||
} | ||
|
||
// 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 { | ||
// Serialize parameters | ||
paramBytes, err := json.Marshal(params) | ||
if err != nil { | ||
return "" | ||
} | ||
|
||
// Concatenate method name and parameters | ||
rawKey := method + string(paramBytes) | ||
|
||
// Optional: Apply hashing | ||
hasher := sha256.New() | ||
hasher.Write([]byte(rawKey)) | ||
hashedKey := fmt.Sprintf("%x", hasher.Sum(nil)) | ||
|
||
return hashedKey | ||
} |
Oops, something went wrong.