Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 17 additions & 16 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,22 +223,23 @@ type EthCallOverrideConfig struct {
}

type Config struct {
WSBackendGroup string `toml:"ws_backend_group"`
Server ServerConfig `toml:"server"`
Cache CacheConfig `toml:"cache"`
Redis RedisConfig `toml:"redis"`
Metrics MetricsConfig `toml:"metrics"`
RateLimit RateLimitConfig `toml:"rate_limit"`
BackendOptions BackendOptions `toml:"backend"`
Backends BackendsConfig `toml:"backends"`
BatchConfig BatchConfig `toml:"batch"`
Authentication map[string]string `toml:"authentication"`
BackendGroups BackendGroupsConfig `toml:"backend_groups"`
RPCMethodMappings map[string]string `toml:"rpc_method_mappings"`
WSMethodWhitelist []string `toml:"ws_method_whitelist"`
WhitelistErrorMessage string `toml:"whitelist_error_message"`
SenderRateLimit SenderRateLimitConfig `toml:"sender_rate_limit"`
EthCallOverride EthCallOverrideConfig `toml:"eth_call_override"`
WSBackendGroup string `toml:"ws_backend_group"`
Server ServerConfig `toml:"server"`
Cache CacheConfig `toml:"cache"`
Redis RedisConfig `toml:"redis"`
Metrics MetricsConfig `toml:"metrics"`
RateLimit RateLimitConfig `toml:"rate_limit"`
BackendOptions BackendOptions `toml:"backend"`
Backends BackendsConfig `toml:"backends"`
BatchConfig BatchConfig `toml:"batch"`
Authentication map[string]string `toml:"authentication"`
BackendGroups BackendGroupsConfig `toml:"backend_groups"`
RPCMethodMappings map[string]string `toml:"rpc_method_mappings"`
DomainRPCMethodMappings map[string]map[string]string `toml:"domain_rpc_method_mappings"`
WSMethodWhitelist []string `toml:"ws_method_whitelist"`
WhitelistErrorMessage string `toml:"whitelist_error_message"`
SenderRateLimit SenderRateLimitConfig `toml:"sender_rate_limit"`
EthCallOverride EthCallOverrideConfig `toml:"eth_call_override"`
}

func ReadFromEnvOrConfig(value string) (string, error) {
Expand Down
14 changes: 14 additions & 0 deletions example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ eth_call = "query"
eth_estimateGas = "query"
eth_sendRawTransaction = "multicall"

# Domain-specific RPC method mappings (optional)
# Different domains can have different routing rules
# If no domain-specific mapping is found, it will fallback to rpc_method_mappings above
# [domain_rpc_method_mappings]
# [domain_rpc_method_mappings."domain1.example.com"]
# eth_blockNumber = "query"
# eth_sendRawTransaction = "multicall"
# eth_call = "query"
#
# [domain_rpc_method_mappings."domain2.example.com"]
# eth_blockNumber = "query"
# eth_sendRawTransaction = "query"
# eth_call = "multicall"

[eth_call_override]
# 48Club
[[eth_call_override.rules]]
Expand Down
193 changes: 193 additions & 0 deletions integration_tests/domain_routing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package integration_tests

import (
"os"
"testing"

"github.com/stretchr/testify/require"

"github.com/ethereum-optimism/infra/proxyd"
)

func TestDomainRPCMethodMappings(t *testing.T) {
goodBackend1 := NewMockBackend(BatchedResponseHandler(200, goodResponse))
defer goodBackend1.Close()

goodBackend2 := NewMockBackend(BatchedResponseHandler(200, goodResponse))
defer goodBackend2.Close()

require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL_1", goodBackend1.URL()))
require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL_2", goodBackend2.URL()))

config := ReadConfig("domain_routing")
_, shutdown, err := proxyd.Start(config)
require.NoError(t, err)
defer shutdown()

t.Run("default domain uses default mappings", func(t *testing.T) {
// Reset counters
goodBackend1.Reset()
goodBackend2.Reset()

client := NewProxydClient("http://127.0.0.1:8545")
res, statusCode, err := client.SendRPC("eth_blockNumber", nil)
require.NoError(t, err)
require.Equal(t, 200, statusCode)
require.NotNil(t, res)

// eth_blockNumber should route to backend1 based on default rpc_method_mappings
require.Equal(t, 1, len(goodBackend1.Requests()))
require.Equal(t, 0, len(goodBackend2.Requests()))
})

t.Run("domain1 uses custom mappings", func(t *testing.T) {
// Reset counters
goodBackend1.Reset()
goodBackend2.Reset()

client := NewProxydClient("http://127.0.0.1:8545")
// Set X-Forwarded-Host header to match domain1.example.com
req := NewRPCReq("1", "eth_blockNumber", nil)
res, statusCode, err := client.SendRequestWithHeaders(req, map[string]string{
"X-Forwarded-Host": "domain1.example.com",
})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
require.NotNil(t, res)

// For domain1.example.com, eth_blockNumber should route to backend2
require.Equal(t, 0, len(goodBackend1.Requests()))
require.Equal(t, 1, len(goodBackend2.Requests()))
})

t.Run("domain2 uses custom mappings", func(t *testing.T) {
// Reset counters
goodBackend1.Reset()
goodBackend2.Reset()

client := NewProxydClient("http://127.0.0.1:8545")
req := NewRPCReq("1", "eth_call", []interface{}{
map[string]interface{}{"to": "0x1234"},
"latest",
})
res, statusCode, err := client.SendRequestWithHeaders(req, map[string]string{
"X-Forwarded-Host": "domain2.example.com",
})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
require.NotNil(t, res)

// For domain2.example.com, eth_call should route to backend1
require.Equal(t, 1, len(goodBackend1.Requests()))
require.Equal(t, 0, len(goodBackend2.Requests()))
})

t.Run("unknown domain falls back to default mappings", func(t *testing.T) {
// Reset counters
goodBackend1.Reset()
goodBackend2.Reset()

client := NewProxydClient("http://127.0.0.1:8545")
req := NewRPCReq("1", "eth_blockNumber", nil)
res, statusCode, err := client.SendRequestWithHeaders(req, map[string]string{
"X-Forwarded-Host": "unknown.example.com",
})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
require.NotNil(t, res)

// Unknown domain should use default mappings (backend1)
require.Equal(t, 1, len(goodBackend1.Requests()))
require.Equal(t, 0, len(goodBackend2.Requests()))
})

t.Run("batch requests use domain-specific mappings", func(t *testing.T) {
// Reset counters
goodBackend1.Reset()
goodBackend2.Reset()

client := NewProxydClient("http://127.0.0.1:8545")
batch := []*proxyd.RPCReq{
NewRPCReq("1", "eth_blockNumber", nil),
NewRPCReq("2", "eth_chainId", nil),
}

res, statusCode, err := client.SendBatchRequestWithHeaders(batch, map[string]string{
"X-Forwarded-Host": "domain1.example.com",
})
require.NoError(t, err)
require.Equal(t, 200, statusCode)
require.NotNil(t, res)

// For domain1.example.com, both methods should route to backend2
require.Equal(t, 0, len(goodBackend1.Requests()))
require.Equal(t, 1, len(goodBackend2.Requests())) // batched into one request
})
}

func TestDomainRPCMethodMappingsWithMultipleBackendGroups(t *testing.T) {
backend1 := NewMockBackend(BatchedResponseHandler(200, goodResponse))
defer backend1.Close()

backend2 := NewMockBackend(BatchedResponseHandler(200, goodResponse))
defer backend2.Close()

backend3 := NewMockBackend(BatchedResponseHandler(200, goodResponse))
defer backend3.Close()

require.NoError(t, os.Setenv("BACKEND_1_URL", backend1.URL()))
require.NoError(t, os.Setenv("BACKEND_2_URL", backend2.URL()))
require.NoError(t, os.Setenv("BACKEND_3_URL", backend3.URL()))

config := ReadConfig("domain_routing_multigroup")
_, shutdown, err := proxyd.Start(config)
require.NoError(t, err)
defer shutdown()

t.Run("different domains route to different backend groups", func(t *testing.T) {
// Reset counters
backend1.Reset()
backend2.Reset()
backend3.Reset()

client := NewProxydClient("http://127.0.0.1:8545")

// Domain A: eth_call -> group_a (backend1)
req1 := NewRPCReq("1", "eth_call", []interface{}{
map[string]interface{}{"to": "0x1234"},
"latest",
})
res1, statusCode1, err := client.SendRequestWithHeaders(req1, map[string]string{
"X-Forwarded-Host": "domainA.example.com",
})
require.NoError(t, err)
require.Equal(t, 200, statusCode1)
require.NotNil(t, res1)
require.Equal(t, 1, len(backend1.Requests()))

// Domain B: eth_call -> group_b (backend2)
backend1.Reset()
req2 := NewRPCReq("2", "eth_call", []interface{}{
map[string]interface{}{"to": "0x1234"},
"latest",
})
res2, statusCode2, err := client.SendRequestWithHeaders(req2, map[string]string{
"X-Forwarded-Host": "domainB.example.com",
})
require.NoError(t, err)
require.Equal(t, 200, statusCode2)
require.NotNil(t, res2)
require.Equal(t, 1, len(backend2.Requests()))

// Default: eth_call -> group_c (backend3)
backend2.Reset()
res3, statusCode3, err := client.SendRPC("eth_call", []interface{}{
map[string]interface{}{"to": "0x1234"},
"latest",
})
require.NoError(t, err)
require.Equal(t, 200, statusCode3)
require.NotNil(t, res3)
require.Equal(t, 1, len(backend3.Requests()))
})
}
44 changes: 44 additions & 0 deletions integration_tests/testdata/domain_routing.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[server]
rpc_port = 8545

[backend]
response_timeout_seconds = 1

[backends]
[backends.backend1]
rpc_url = "$GOOD_BACKEND_RPC_URL_1"

[backends.backend2]
rpc_url = "$GOOD_BACKEND_RPC_URL_2"

[backend_groups]
[backend_groups.group1]
backends = ["backend1"]

[backend_groups.group2]
backends = ["backend2"]

# Default RPC method mappings
[rpc_method_mappings]
eth_blockNumber = "group1"
eth_chainId = "group1"
eth_call = "group1"
eth_gasPrice = "group1"

# Domain-specific RPC method mappings
[domain_rpc_method_mappings]

# domain1.example.com routes to group2
[domain_rpc_method_mappings."domain1.example.com"]
eth_blockNumber = "group2"
eth_chainId = "group2"
eth_call = "group2"
eth_gasPrice = "group2"

# domain2.example.com has mixed routing
[domain_rpc_method_mappings."domain2.example.com"]
eth_blockNumber = "group2"
eth_chainId = "group2"
eth_call = "group1"
eth_gasPrice = "group1"

47 changes: 47 additions & 0 deletions integration_tests/testdata/domain_routing_multigroup.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[server]
rpc_port = 8545

[backend]
response_timeout_seconds = 1

[backends]
[backends.backend_a]
rpc_url = "$BACKEND_1_URL"

[backends.backend_b]
rpc_url = "$BACKEND_2_URL"

[backends.backend_c]
rpc_url = "$BACKEND_3_URL"

[backend_groups]
[backend_groups.group_a]
backends = ["backend_a"]

[backend_groups.group_b]
backends = ["backend_b"]

[backend_groups.group_c]
backends = ["backend_c"]

# Default RPC method mappings - use group_c
[rpc_method_mappings]
eth_call = "group_c"
eth_blockNumber = "group_c"
eth_chainId = "group_c"

# Domain-specific RPC method mappings
[domain_rpc_method_mappings]

# domainA.example.com routes to group_a
[domain_rpc_method_mappings."domainA.example.com"]
eth_call = "group_a"
eth_blockNumber = "group_a"
eth_chainId = "group_a"

# domainB.example.com routes to group_b
[domain_rpc_method_mappings."domainB.example.com"]
eth_call = "group_b"
eth_blockNumber = "group_b"
eth_chainId = "group_b"

Loading