Merge pull request #6358 from ethereum-optimism/felipe/rate-limit-dos

feat(proxyd): sender rate limiter should check chain id
This commit is contained in:
OptimismBot 2023-07-26 18:28:12 -07:00 committed by GitHub
commit 73ce23c025
5 changed files with 39 additions and 13 deletions

@ -2,6 +2,7 @@ package proxyd
import ( import (
"fmt" "fmt"
"math/big"
"os" "os"
"strings" "strings"
"time" "time"
@ -124,6 +125,7 @@ type SenderRateLimitConfig struct {
Enabled bool Enabled bool
Interval TOMLDuration Interval TOMLDuration
Limit int Limit int
AllowedChainIds []*big.Int `toml:"allowed_chain_ids"`
} }
type Config struct { type Config struct {

@ -20,12 +20,10 @@ const txHex1 = "0x02f8b28201a406849502f931849502f931830147f9948f3ddd0fbf3e78ca1d
"1145d2f3e759d49209fe96011ac012884ec5b017a0763b58f6fa6096e6ba28ee" + "1145d2f3e759d49209fe96011ac012884ec5b017a0763b58f6fa6096e6ba28ee" +
"08bfac58f58fb3b8bcef5af98578bdeaddf40bde42" "08bfac58f58fb3b8bcef5af98578bdeaddf40bde42"
const txHex2 = "0xf8aa82afd2830f4240830493e094464959ad46e64046b891f562cff202a465d5" + const txHex2 = "0x02f8758201a48217fd84773594008504a817c80082520894be53e587975603" +
"22f380b844d5bade070000000000000000000000004200000000000000000000" + "a13d0923d0aa6d37c5233dd750865af3107a400080c080a04aefbd5819c35729" +
"0000000000000000060000000000000000000000000000000000000000000000" + "138fe26b6ae1783ebf08d249b356c2f920345db97877f3f7a008d5ae92560a3c" +
"0000000025ef43fc0038a05d8ea9837ea81469bda4dadbe852fdd37fcfbcd666" + "65f723439887205713af7ce7d7f6b24fba198f2afa03435867"
"5641a35e4726fbc04364e7a0107e20bb34aea53c695a551204a11d42fe465055" +
"510ee240e8f884fb70289be6"
const dummyRes = `{"id": 123, "jsonrpc": "2.0", "result": "dummy"}` const dummyRes = `{"id": 123, "jsonrpc": "2.0", "result": "dummy"}`
@ -81,10 +79,10 @@ func TestSenderRateLimitLimiting(t *testing.T) {
// should be rate limited. // should be rate limited.
res1, code1, err := client.SendRequest(makeSendRawTransaction(txHex1)) res1, code1, err := client.SendRequest(makeSendRawTransaction(txHex1))
require.NoError(t, err) require.NoError(t, err)
res2, code2, err := client.SendRequest(makeSendRawTransaction(txHex1))
require.NoError(t, err)
RequireEqualJSON(t, []byte(dummyRes), res1) RequireEqualJSON(t, []byte(dummyRes), res1)
require.Equal(t, 200, code1) require.Equal(t, 200, code1)
res2, code2, err := client.SendRequest(makeSendRawTransaction(txHex1))
require.NoError(t, err)
RequireEqualJSON(t, []byte(limRes), res2) RequireEqualJSON(t, []byte(limRes), res2)
require.Equal(t, 429, code2) require.Equal(t, 429, code2)

@ -21,3 +21,4 @@ eth_sendRawTransaction = "main"
enabled = true enabled = true
interval = "1s" interval = "1s"
limit = 1 limit = 1
allowed_chain_ids = [420]

@ -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} 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 - 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"} 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}] 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}]

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io" "io"
"math" "math"
"math/big"
"net/http" "net/http"
"regexp" "regexp"
"strconv" "strconv"
@ -18,6 +19,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core" "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/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
@ -56,6 +58,7 @@ type Server struct {
mainLim FrontendRateLimiter mainLim FrontendRateLimiter
overrideLims map[string]FrontendRateLimiter overrideLims map[string]FrontendRateLimiter
senderLim FrontendRateLimiter senderLim FrontendRateLimiter
allowedChainIds []*big.Int
limExemptOrigins []*regexp.Regexp limExemptOrigins []*regexp.Regexp
limExemptUserAgents []*regexp.Regexp limExemptUserAgents []*regexp.Regexp
globallyLimitedMethods map[string]bool globallyLimitedMethods map[string]bool
@ -173,6 +176,7 @@ func NewServer(
overrideLims: overrideLims, overrideLims: overrideLims,
globallyLimitedMethods: globalMethodLims, globallyLimitedMethods: globalMethodLims,
senderLim: senderLim, senderLim: senderLim,
allowedChainIds: senderRateLimitConfig.AllowedChainIds,
limExemptOrigins: limExemptOrigins, limExemptOrigins: limExemptOrigins,
limExemptUserAgents: limExemptUserAgents, limExemptUserAgents: limExemptUserAgents,
}, nil }, nil
@ -654,6 +658,13 @@ func (s *Server) rateLimitSender(ctx context.Context, req *RPCReq) error {
return ErrInvalidParams(err.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 // Convert the transaction into a Message object so that we can get the
// sender. This method performs an ecrecover, which can be expensive. // sender. This method performs an ecrecover, which can be expensive.
msg, err := core.TransactionToMessage(tx, types.LatestSignerForChainID(tx.ChainId()), nil) 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 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) { func setCacheHeader(w http.ResponseWriter, cached bool) {
if cached { if cached {
w.Header().Set(cacheStatusHdr, "HIT") w.Header().Set(cacheStatusHdr, "HIT")