From 7792864af0a050ce02f319ca39d1d6147bc0fd25 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Fri, 16 Feb 2024 09:39:42 +0000 Subject: [PATCH 1/4] adjust port offset (#1798) --- integration/constants.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration/constants.go b/integration/constants.go index 37449c6dc5..fe4ce15bc8 100644 --- a/integration/constants.go +++ b/integration/constants.go @@ -25,9 +25,9 @@ const ( DefaultEnclaveOffset = 700 // The default offset between a Geth nodes port and the enclave ports. Used in Socket Simulations. DefaultHostRPCHTTPOffset = 800 // The default offset for the host's RPC HTTP port DefaultHostRPCWSOffset = 900 // The default offset for the host's RPC websocket port - DefaultTenscanHTTPPortOffset = 1000 - DefaultTenGatewayHTTPPortOffset = 1001 - DefaultTenGatewayWSPortOffset = 1002 + DefaultTenscanHTTPPortOffset = 950 + DefaultTenGatewayHTTPPortOffset = 951 + DefaultTenGatewayWSPortOffset = 952 ) const ( From 5b5e7d98e1ae53c9729742b79967bbf3eb80a30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Kokelj?= Date: Fri, 16 Feb 2024 11:00:43 +0100 Subject: [PATCH 2/4] Ten Gateway Caching (#1779) --- tools/walletextension/cache/RistrettoCache.go | 82 ++++++++++ tools/walletextension/cache/cache.go | 98 ++++++++++++ tools/walletextension/cache/cache_test.go | 147 ++++++++++++++++++ tools/walletextension/wallet_extension.go | 28 ++++ 4 files changed, 355 insertions(+) create mode 100644 tools/walletextension/cache/RistrettoCache.go create mode 100644 tools/walletextension/cache/cache.go create mode 100644 tools/walletextension/cache/cache_test.go diff --git a/tools/walletextension/cache/RistrettoCache.go b/tools/walletextension/cache/RistrettoCache.go new file mode 100644 index 0000000000..af417115b7 --- /dev/null +++ b/tools/walletextension/cache/RistrettoCache.go @@ -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 + } + } +} diff --git a/tools/walletextension/cache/cache.go b/tools/walletextension/cache/cache.go new file mode 100644 index 0000000000..66e8b35f63 --- /dev/null +++ b/tools/walletextension/cache/cache.go @@ -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 +} diff --git a/tools/walletextension/cache/cache_test.go b/tools/walletextension/cache/cache_test.go new file mode 100644 index 0000000000..f4bb05d941 --- /dev/null +++ b/tools/walletextension/cache/cache_test.go @@ -0,0 +1,147 @@ +package cache + +import ( + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/log" + + "github.com/ten-protocol/go-ten/tools/walletextension/common" +) + +var tests = map[string]func(t *testing.T){ + "testCacheableMethods": testCacheableMethods, + "testNonCacheableMethods": testNonCacheableMethods, + "testMethodsWithLatestOrPendingParameter": testMethodsWithLatestOrPendingParameter, +} + +var cacheTests = map[string]func(cache Cache, t *testing.T){ + "testResultsAreCached": testResultsAreCached, + "testCacheTTL": testCacheTTL, +} + +var nonCacheableMethods = []string{"eth_sendrawtransaction", "eth_sendtransaction", "join", "authenticate"} + +func TestGatewayCaching(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + test(t) + }) + } + + // cache tests + for name, test := range cacheTests { + t.Run(name, func(t *testing.T) { + logger := log.New() + cache, err := NewCache(logger) + if err != nil { + t.Errorf("failed to create cache: %v", err) + } + test(cache, t) + }) + } +} + +// testCacheableMethods tests if the cacheable methods are cacheable +func testCacheableMethods(t *testing.T) { + for method := range cacheableRPCMethods { + key := &common.RPCRequest{Method: method} + isCacheable, _, _ := IsCacheable(key) + if isCacheable != true { + t.Errorf("method %s should be cacheable", method) + } + } +} + +// testNonCacheableMethods tests if the non-cacheable methods are not cacheable +func testNonCacheableMethods(t *testing.T) { + for _, method := range nonCacheableMethods { + key := &common.RPCRequest{Method: method} + isCacheable, _, _ := IsCacheable(key) + if isCacheable == true { + t.Errorf("method %s should not be cacheable", method) + } + } +} + +// testMethodsWithLatestOrPendingParameter tests if the methods with latest or pending parameter are cacheable +func testMethodsWithLatestOrPendingParameter(t *testing.T) { + methods := []string{"eth_getCode", "eth_getBalance", "eth_getTransactionCount", "eth_estimateGas", "eth_call"} + for _, method := range methods { + key := &common.RPCRequest{Method: method, Params: []interface{}{"0x123", "latest"}} + _, _, ttl := IsCacheable(key) + 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) + if ttl != shortCacheTTL { + t.Errorf("method %s with pending parameter should have TTL of %s, but %s received", method, shortCacheTTL, ttl) + } + } +} + +// testResultsAreCached tests if the results are cached as expected +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) + 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 + value, ok := cache.Get(key) + if !ok { + t.Errorf("failed to get cached value for %s", req) + } + + if !reflect.DeepEqual(value, res) { + t.Errorf("expected %v, got %v", res, value) + } +} + +// testCacheTTL tests if the cache TTL is working as expected +func testCacheTTL(cache Cache, t *testing.T) { + req := &common.RPCRequest{Method: "eth_getBalance", Params: []interface{}{"0x123"}} + res := map[string]interface{}{"result": "100"} + isCacheable, key, ttl := IsCacheable(req) + + if !isCacheable { + t.Errorf("method %s should be cacheable", req.Method) + } + + if ttl != shortCacheTTL { + t.Errorf("method %s should have TTL of %s, but %s received", req.Method, shortCacheTTL, ttl) + } + + // 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) + } + + if !reflect.DeepEqual(value, res) { + t.Errorf("expected %v, got %v", res, value) + } + + // sleep for the TTL to expire + time.Sleep(shortCacheTTL + 100*time.Millisecond) + _, ok = cache.Get(key) + if ok { + t.Errorf("value should not be in the cache after TTL") + } +} diff --git a/tools/walletextension/wallet_extension.go b/tools/walletextension/wallet_extension.go index 237e79c980..68f2917ab0 100644 --- a/tools/walletextension/wallet_extension.go +++ b/tools/walletextension/wallet_extension.go @@ -7,6 +7,8 @@ import ( "fmt" "time" + "github.com/ten-protocol/go-ten/tools/walletextension/cache" + "github.com/ten-protocol/go-ten/tools/walletextension/accountmanager" "github.com/ten-protocol/go-ten/tools/walletextension/config" @@ -43,6 +45,7 @@ type WalletExtension struct { version string config *config.Config tenClient *obsclient.ObsClient + cache cache.Cache } func New( @@ -62,6 +65,12 @@ func New( } newTenClient := obsclient.NewObsClient(rpcClient) newFileLogger := common.NewFileLogger() + newGatewayCache, err := cache.NewCache(logger) + if err != nil { + logger.Error(fmt.Errorf("could not create cache. Cause: %w", err).Error()) + panic(err) + } + return &WalletExtension{ hostAddrHTTP: hostAddrHTTP, hostAddrWS: hostAddrWS, @@ -74,6 +83,7 @@ func New( version: version, config: config, tenClient: newTenClient, + cache: newGatewayCache, } } @@ -92,6 +102,19 @@ 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())) + return value, nil + } + } + response := map[string]interface{}{} // all responses must contain the request id. Both successful and unsuccessful. response[common.JSONKeyRPCVersion] = jsonrpc.Version @@ -140,6 +163,11 @@ func (w *WalletExtension) ProxyEthRequest(request *common.RPCRequest, conn userc 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, response, duration.Milliseconds())) + // if the request is cacheable, store the response in the cache + if isCacheable { + w.cache.Set(key, response, ttl) + } + return response, nil } From a2a080aa1a6a1de530d926c39b8eaa8860fdc78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=BDiga=20Kokelj?= Date: Fri, 16 Feb 2024 11:36:25 +0100 Subject: [PATCH 3/4] Gateway caching proposal (#1773) --- design/gateway/gateway_caching.md | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 design/gateway/gateway_caching.md diff --git a/design/gateway/gateway_caching.md b/design/gateway/gateway_caching.md new file mode 100644 index 0000000000..8eb3c70bb7 --- /dev/null +++ b/design/gateway/gateway_caching.md @@ -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 From 597cf0ec31ee2e65013ee3677ebdfe24c92cf654 Mon Sep 17 00:00:00 2001 From: Moray Grieve Date: Fri, 16 Feb 2024 10:42:40 +0000 Subject: [PATCH 4/4] Change log for the v0.22 release (#1799) --- changelog.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 96a082efbb..7ee6e9c0f8 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,26 @@ --- # Ten Testnet Change Log -# Oct 2024-01-15 (v0.21.0) +# Feb 2024-02-16 (v0.22.0) +* Validator nodes now return errors on transaction submission. Previously, transactions that would fail validation were + accepted into the mempool of the validator without error (e.g. insufficient funds for a transfer, gas below the + intrinsic gas limit etc). This change will mean that invalid transactions are rejected immediately. +* The Gateway now performs caching on RPC requests. For a list of the cached requests see the design proposal in + go-ten/design/gateway/gateway_caching.md. Note that we expect the caching logic to change over time as we optimise + the user experience. +* A full list of the PRs merged in this release is as below; + * `5b5e7d98` Ten gateway caching (#1779) + * `7792864a` Adjust port offset (#1798) + * `1837bcc6` More flakiness fixes (#1795) + * `83469ca8` Validator nodes return error on tx submission (#1792) + * `aee40a81` Network tests: fix local run gas error (#1789) + * `337c8544` Improve multi-acct logic (#1793) + * `a5061634` Copy update (#1791) + * `4d79eea2` Tenscan: update api domain (#1790) + * `c83b965a` Contract deployers: configure signer for address publishing (#1676) + * `31e25322` Refactor the encryption/vk logic (#1769) + +# Feb 2024-02-14 (v0.21.0) * A list of the PRs merged in this release is as below; * `cc3b1048` Upgrade oz libraries (#1707) * `0a573d75` Deploy scripts: enable debug api on upgrades (#1780) @@ -84,7 +103,7 @@ * `4551c81b` Enclave no panic for bad rlp format (#1679) * `fb50c7ba` Use the cached head block (#1678) -# Oct 2023-12-01 (v0.20.0) +# Dec 2023-12-01 (v0.20.0) * A list of the PRs merged in this release is as below; * `1433e41e` Tenscan updates (#1661) * `dc769561` Host rpc: use checksum format for contract addresses (#1674)