From 16b03049aef330f3e54ebcb9d3d84f5bc8b9c659 Mon Sep 17 00:00:00 2001 From: Felipe Andrade Date: Wed, 19 Jul 2023 13:21:00 -0700 Subject: [PATCH] feat(proxyd): sender rate limiter should check chain id --- proxyd/proxyd/config.go | 8 ++++--- .../sender_rate_limit_test.go | 14 +++++------ .../testdata/sender_rate_limit.toml | 3 ++- .../integration_tests/testdata/testdata.txt | 4 +++- proxyd/proxyd/server.go | 23 +++++++++++++++++++ 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/proxyd/proxyd/config.go b/proxyd/proxyd/config.go index bd58f62..978506f 100644 --- a/proxyd/proxyd/config.go +++ b/proxyd/proxyd/config.go @@ -2,6 +2,7 @@ package proxyd import ( "fmt" + "math/big" "os" "strings" "time" @@ -121,9 +122,10 @@ type BatchConfig struct { // SenderRateLimitConfig configures the sender-based rate limiter // for eth_sendRawTransaction requests. type SenderRateLimitConfig struct { - Enabled bool - Interval TOMLDuration - Limit int + Enabled bool + Interval TOMLDuration + Limit int + AllowedChainIds []*big.Int `toml:"allowed_chain_ids"` } type Config struct { diff --git a/proxyd/proxyd/integration_tests/sender_rate_limit_test.go b/proxyd/proxyd/integration_tests/sender_rate_limit_test.go index a31a077..20c5f0a 100644 --- a/proxyd/proxyd/integration_tests/sender_rate_limit_test.go +++ b/proxyd/proxyd/integration_tests/sender_rate_limit_test.go @@ -20,12 +20,10 @@ const txHex1 = "0x02f8b28201a406849502f931849502f931830147f9948f3ddd0fbf3e78ca1d "1145d2f3e759d49209fe96011ac012884ec5b017a0763b58f6fa6096e6ba28ee" + "08bfac58f58fb3b8bcef5af98578bdeaddf40bde42" -const txHex2 = "0xf8aa82afd2830f4240830493e094464959ad46e64046b891f562cff202a465d5" + - "22f380b844d5bade070000000000000000000000004200000000000000000000" + - "0000000000000000060000000000000000000000000000000000000000000000" + - "0000000025ef43fc0038a05d8ea9837ea81469bda4dadbe852fdd37fcfbcd666" + - "5641a35e4726fbc04364e7a0107e20bb34aea53c695a551204a11d42fe465055" + - "510ee240e8f884fb70289be6" +const txHex2 = "0x02f8758201a48217fd84773594008504a817c80082520894be53e587975603" + + "a13d0923d0aa6d37c5233dd750865af3107a400080c080a04aefbd5819c35729" + + "138fe26b6ae1783ebf08d249b356c2f920345db97877f3f7a008d5ae92560a3c" + + "65f723439887205713af7ce7d7f6b24fba198f2afa03435867" const dummyRes = `{"id": 123, "jsonrpc": "2.0", "result": "dummy"}` @@ -81,10 +79,10 @@ func TestSenderRateLimitLimiting(t *testing.T) { // should be rate limited. res1, code1, err := client.SendRequest(makeSendRawTransaction(txHex1)) require.NoError(t, err) - res2, code2, err := client.SendRequest(makeSendRawTransaction(txHex1)) - require.NoError(t, err) RequireEqualJSON(t, []byte(dummyRes), res1) require.Equal(t, 200, code1) + res2, code2, err := client.SendRequest(makeSendRawTransaction(txHex1)) + require.NoError(t, err) RequireEqualJSON(t, []byte(limRes), res2) require.Equal(t, 429, code2) diff --git a/proxyd/proxyd/integration_tests/testdata/sender_rate_limit.toml b/proxyd/proxyd/integration_tests/testdata/sender_rate_limit.toml index 840c295..024858a 100644 --- a/proxyd/proxyd/integration_tests/testdata/sender_rate_limit.toml +++ b/proxyd/proxyd/integration_tests/testdata/sender_rate_limit.toml @@ -20,4 +20,5 @@ eth_sendRawTransaction = "main" [sender_rate_limit] enabled = true interval = "1s" -limit = 1 \ No newline at end of file +limit = 1 +allowed_chain_ids = [420] diff --git a/proxyd/proxyd/integration_tests/testdata/testdata.txt b/proxyd/proxyd/integration_tests/testdata/testdata.txt index 49d0a87..7e31927 100644 --- a/proxyd/proxyd/integration_tests/testdata/testdata.txt +++ b/proxyd/proxyd/integration_tests/testdata/testdata.txt @@ -8,4 +8,6 @@ invalid transaction data|{"jsonrpc":"2.0","method":"eth_sendRawTransaction","par invalid transaction data|{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x1234"],"id":1}|{"jsonrpc":"2.0","error":{"code":-32602,"message":"transaction type not supported"},"id":1} valid transaction data - simple send|{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f8748201a415843b9aca31843b9aca3182520894f80267194936da1e98db10bce06f3147d580a62e880de0b6b3a764000080c001a0b50ee053102360ff5fedf0933b912b7e140c90fe57fa07a0cebe70dbd72339dda072974cb7bfe5c3dc54dde110e2b049408ccab8a879949c3b4d42a3a7555a618b"],"id":1}|{"id": 123, "jsonrpc": "2.0", "result": "dummy"} valid transaction data - contract call|{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f8b28201a406849502f931849502f931830147f9948f3ddd0fbf3e78ca1d6cd17379ed88e261249b5280b84447e7ef2400000000000000000000000089c8b1b2774201bac50f627403eac1b732459cf70000000000000000000000000000000000000000000000056bc75e2d63100000c080a0473c95566026c312c9664cd61145d2f3e759d49209fe96011ac012884ec5b017a0763b58f6fa6096e6ba28ee08bfac58f58fb3b8bcef5af98578bdeaddf40bde42"],"id":1}|{"id": 123, "jsonrpc": "2.0", "result": "dummy"} -batch with mixed results|[{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f8748201a415843b9aca31843b9aca3182520894f80267194936da1e98db10bce06f3147d580a62e880de0b6b3a764000080c001a0b50ee053102360ff5fedf0933b912b7e140c90fe57fa07a0cebe70dbd72339dda072974cb7bfe5c3dc54dde110e2b049408ccab8a879949c3b4d42a3a7555a618b"],"id":1},{"bad":"json"},{"jsonrpc":"2.0","method":"eth_fooTheBar","params":[],"id":123}]|[{"id": 123, "jsonrpc": "2.0", "result": "dummy"},{"jsonrpc":"2.0","error":{"code":-32600,"message":"invalid JSON-RPC version"},"id":null},{"jsonrpc":"2.0","error":{"code":-32001,"message":"rpc method is not whitelisted"},"id":123}] \ No newline at end of file +valid chain id - simple send|{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f8748201a415843b9aca31843b9aca3182520894f80267194936da1e98db10bce06f3147d580a62e880de0b6b3a764000080c001a0b50ee053102360ff5fedf0933b912b7e140c90fe57fa07a0cebe70dbd72339dda072974cb7bfe5c3dc54dde110e2b049408ccab8a879949c3b4d42a3a7555a618b"],"id":1}|{"id": 123, "jsonrpc": "2.0", "result": "dummy"} +invalid chain id - simple send|{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f87683ab41308217af84773594008504a817c80082520894be53e587975603a13d0923d0aa6d37c5233dd750865af3107a400080c001a04ae265f17e882b922d39f0f0cb058a6378df1dc89da8b8165ab6bc53851b426aa0682079486be2aa23bc7514477473362cc7d63afa12c99f7d8fb15e68d69d9a48"],"id":1}|{"jsonrpc":"2.0","error":{"code":-32000,"message":"invalid sender"},"id":1} +batch with mixed results|[{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f87683ab41308217af84773594008504a817c80082520894be53e587975603a13d0923d0aa6d37c5233dd750865af3107a400080c001a04ae265f17e882b922d39f0f0cb058a6378df1dc89da8b8165ab6bc53851b426aa0682079486be2aa23bc7514477473362cc7d63afa12c99f7d8fb15e68d69d9a48"],"id":1},{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f8748201a415843b9aca31843b9aca3182520894f80267194936da1e98db10bce06f3147d580a62e880de0b6b3a764000080c001a0b50ee053102360ff5fedf0933b912b7e140c90fe57fa07a0cebe70dbd72339dda072974cb7bfe5c3dc54dde110e2b049408ccab8a879949c3b4d42a3a7555a618b"],"id":1},{"bad":"json"},{"jsonrpc":"2.0","method":"eth_fooTheBar","params":[],"id":123}]|[{"jsonrpc":"2.0","error":{"code":-32000,"message":"invalid sender"},"id":1},{"id": 123, "jsonrpc": "2.0", "result": "dummy"},{"jsonrpc":"2.0","error":{"code":-32600,"message":"invalid JSON-RPC version"},"id":null},{"jsonrpc":"2.0","error":{"code":-32001,"message":"rpc method is not whitelisted"},"id":123}] diff --git a/proxyd/proxyd/server.go b/proxyd/proxyd/server.go index 847ad51..efe7c5f 100644 --- a/proxyd/proxyd/server.go +++ b/proxyd/proxyd/server.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "math" + "math/big" "net/http" "regexp" "strconv" @@ -18,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/go-redis/redis/v8" @@ -56,6 +58,7 @@ type Server struct { mainLim FrontendRateLimiter overrideLims map[string]FrontendRateLimiter senderLim FrontendRateLimiter + allowedChainIds []*big.Int limExemptOrigins []*regexp.Regexp limExemptUserAgents []*regexp.Regexp globallyLimitedMethods map[string]bool @@ -173,6 +176,7 @@ func NewServer( overrideLims: overrideLims, globallyLimitedMethods: globalMethodLims, senderLim: senderLim, + allowedChainIds: senderRateLimitConfig.AllowedChainIds, limExemptOrigins: limExemptOrigins, limExemptUserAgents: limExemptUserAgents, }, nil @@ -654,6 +658,13 @@ func (s *Server) rateLimitSender(ctx context.Context, req *RPCReq) error { return ErrInvalidParams(err.Error()) } + // Check if the transaction is for the expected chain, + // otherwise reject before rate limiting to avoid replay attacks. + if !s.isAllowedChainId(tx.ChainId()) { + log.Debug("chain id is not allowed", "req_id", GetReqID(ctx)) + return txpool.ErrInvalidSender + } + // Convert the transaction into a Message object so that we can get the // sender. This method performs an ecrecover, which can be expensive. msg, err := core.TransactionToMessage(tx, types.LatestSignerForChainID(tx.ChainId()), nil) @@ -674,6 +685,18 @@ func (s *Server) rateLimitSender(ctx context.Context, req *RPCReq) error { return nil } +func (s *Server) isAllowedChainId(chainId *big.Int) bool { + if s.allowedChainIds == nil || len(s.allowedChainIds) == 0 { + return true + } + for _, id := range s.allowedChainIds { + if chainId.Cmp(id) == 0 { + return true + } + } + return false +} + func setCacheHeader(w http.ResponseWriter, cached bool) { if cached { w.Header().Set(cacheStatusHdr, "HIT")