Skip to content
Open
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
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# stage: build ---------------------------------------------------------

FROM golang:1.22-alpine as build
FROM golang:1.24-alpine AS build

RUN apk add --no-cache gcc musl-dev linux-headers

Expand All @@ -11,7 +11,11 @@ RUN go mod download

COPY . .

RUN go build -o bin/bproxy -ldflags "-s -w" github.com/flashbots/bproxy/cmd
RUN SOURCE_DATE_EPOCH=0 CGO_ENABLED=0 go build \
-trimpath \
-ldflags "-s -w -buildid=" \
-o bin/bproxy \
github.com/flashbots/bproxy/cmd

# stage: run -----------------------------------------------------------

Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ VERSION := $(VERSION:v%=%)

.PHONY: build
build:
@CGO_ENABLED=0 go build \
-ldflags "-X main.version=${VERSION}" \
@CGO_ENABLED=0 SOURCE_DATE_EPOCH=0 go build \
-trimpath \
-ldflags "-s -w -X main.version=${VERSION} -buildid=" \
-o ./bin/bproxy \
github.com/flashbots/bproxy/cmd

Expand Down
17 changes: 14 additions & 3 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ const (
func CommandServe(cfg *config.Config) *cli.Command {
makeProxyFlags := func(
cfg *config.HttpProxy, category string, backendURL, listenAddress string,
) (flags []cli.Flag, extraMirroredJrpcMethods, peerURLsFlag *cli.StringSlice) {
) (flags []cli.Flag, extraMirroredJrpcMethods, logMethodsFlag, peerURLsFlag *cli.StringSlice) {
extraMirroredJrpcMethods = &cli.StringSlice{}
logMethodsFlag = &cli.StringSlice{}
peerURLsFlag = &cli.StringSlice{}

flags = []cli.Flag{
Expand Down Expand Up @@ -206,6 +207,14 @@ func CommandServe(cfg *config.Config) *cli.Command {
Value: 4096,
},

&cli.StringSliceFlag{ // --xxx-log-methods
Category: strings.ToUpper(category),
Destination: logMethodsFlag,
EnvVars: []string{envPrefix + strings.ToUpper(category) + "_LOG_METHODS"},
Name: category + "-log-methods",
Usage: "only log requests/responses for these jrpc `methods` (empty = log all)",
},

&cli.DurationFlag{ // --xxx-max-backend-connection-wait-timeout
Category: strings.ToUpper(category),
Destination: &cfg.MaxBackendConnectionWaitTimeout,
Expand Down Expand Up @@ -307,7 +316,7 @@ func CommandServe(cfg *config.Config) *cli.Command {
return
}

authrpcFlags, extraMirroredJrpcMethodsAuthRPC, peerURLsAuthRPC := makeProxyFlags(
authrpcFlags, extraMirroredJrpcMethodsAuthRPC, logMethodsAuthRPC, peerURLsAuthRPC := makeProxyFlags(
cfg.Authrpc.HttpProxy, categoryAuthrpc, "http://127.0.0.1:18551", "0.0.0.0:8551",
)

Expand Down Expand Up @@ -528,7 +537,7 @@ func CommandServe(cfg *config.Config) *cli.Command {
},
}

rpcFlags, extraMirroredJrpcMethodsRPC, peerURLsRPC := makeProxyFlags(
rpcFlags, extraMirroredJrpcMethodsRPC, logMethodsRPC, peerURLsRPC := makeProxyFlags(
cfg.Rpc.HttpProxy, categoryRPC, "http://127.0.0.1:18545", "0.0.0.0:8545",
)

Expand Down Expand Up @@ -569,9 +578,11 @@ func CommandServe(cfg *config.Config) *cli.Command {
Before: func(_ *cli.Context) error {
cfg.Authrpc.PeerURLs = peerURLsAuthRPC.Value()
cfg.Authrpc.ExtraMirroredJrpcMethods = extraMirroredJrpcMethodsAuthRPC.Value()
cfg.Authrpc.LogMethods = logMethodsAuthRPC.Value()

cfg.Rpc.PeerURLs = peerURLsRPC.Value()
cfg.Rpc.ExtraMirroredJrpcMethods = extraMirroredJrpcMethodsRPC.Value()
cfg.Rpc.LogMethods = logMethodsRPC.Value()

return cfg.Validate()
},
Expand Down
1 change: 1 addition & 0 deletions config/http_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type HttpProxy struct {
LogRequestsMaxSize int `yaml:"log_requests_max_size"`
LogResponses bool `yaml:"log_responses"`
LogResponsesMaxSize int `yaml:"log_responses_max_size"`
LogMethods []string `yaml:"log_methods"`
MaxBackendConnectionsPerHost int `yaml:"max_backend_connections_per_host"`
MaxBackendConnectionWaitTimeout time.Duration `yaml:"max_client_connection_wait_timeout"`
MaxClientConnectionsPerIP int `yaml:"max_client_connections_per_ip"`
Expand Down
103 changes: 103 additions & 0 deletions jrpc/sanitize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package jrpc

// Sanitize sanitizes a JSON-RPC message by replacing raw transaction data
// with transaction hashes. It handles both single messages and batch requests.
// The input should be an unmarshalled JSON value.
func Sanitize(message any) {
switch msg := message.(type) {
case []any:
// Batch request
for _, item := range msg {
Sanitize(item)
}
case map[string]any:
sanitizeMessage(msg)
}
}

func sanitizeMessage(message map[string]any) {
method, _ := message["method"].(string)

if method != "" {
// Request: sanitize params based on method
params, ok := message["params"].([]any)
if !ok {
return
}

switch method {
case "engine_forkchoiceUpdatedV3":
if len(params) < 2 {
return
}
executionPayload, ok := params[1].(map[string]any)
if !ok {
return
}
sanitizeTransactions(executionPayload, "transactions")

case "engine_newPayloadV4":
if len(params) == 0 {
return
}
executionPayload, ok := params[0].(map[string]any)
if !ok {
return
}
sanitizeTransactions(executionPayload, "transactions")

case "eth_sendBundle":
if len(params) == 0 {
return
}
bundleParams, ok := params[0].(map[string]any)
if !ok {
return
}
sanitizeTransactions(bundleParams, "txs")

case "eth_sendRawTransaction":
for i := range params {
rawTransactionToHash(&params[i])
}
}
return
}

// Response: check for result.executionPayload.transactions (engine_getPayloadV4)
result, ok := message["result"].(map[string]any)
if !ok {
return
}
executionPayload, ok := result["executionPayload"].(map[string]any)
if !ok {
return
}
sanitizeTransactions(executionPayload, "transactions")
}

func sanitizeTransactions(obj map[string]any, key string) {
transactions, ok := obj[key].([]any)
if !ok {
return
}
for i := range transactions {
rawTransactionToHash(&transactions[i])
}
}

func rawTransactionToHash(transaction *any) {
str, ok := (*transaction).(string)
if !ok {
*transaction = "[error casting tx to string]"
return
}

_, tx, err := DecodeEthRawTransaction(str)
if err != nil {
*transaction = "[error decoding tx: " + err.Error() + "]"
return
}

*transaction = tx.Hash().Hex()
}
139 changes: 139 additions & 0 deletions jrpc/sanitize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package jrpc

import (
"encoding/json"
"math/big"
"strings"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)

func createSignedTx(t *testing.T, nonce uint64) (rawTx, txHash string) {
t.Helper()
key, _ := crypto.GenerateKey()
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: big.NewInt(1),
Nonce: nonce,
GasTipCap: big.NewInt(1e9),
GasFeeCap: big.NewInt(100e9),
Gas: 21000,
To: &common.Address{},
})
signed, _ := types.SignTx(tx, types.NewLondonSigner(big.NewInt(1)), key)
bytes, _ := signed.MarshalBinary()
return hexutil.Encode(bytes), signed.Hash().Hex()
}

func TestSanitize_ReplacesWithHash(t *testing.T) {
rawTx, expectedHash := createSignedTx(t, 0)

methods := []struct {
name string
input string
path func(any) string
}{
{
name: "eth_sendRawTransaction",
input: `{"method":"eth_sendRawTransaction","params":["` + rawTx + `"]}`,
path: func(m any) string { return m.(map[string]any)["params"].([]any)[0].(string) },
},
{
name: "engine_newPayloadV4",
input: `{"method":"engine_newPayloadV4","params":[{"transactions":["` + rawTx + `"]}]}`,
path: func(m any) string {
return m.(map[string]any)["params"].([]any)[0].(map[string]any)["transactions"].([]any)[0].(string)
},
},
{
name: "engine_forkchoiceUpdatedV3",
input: `{"method":"engine_forkchoiceUpdatedV3","params":[{},{"transactions":["` + rawTx + `"]}]}`,
path: func(m any) string {
return m.(map[string]any)["params"].([]any)[1].(map[string]any)["transactions"].([]any)[0].(string)
},
},
{
name: "eth_sendBundle",
input: `{"method":"eth_sendBundle","params":[{"txs":["` + rawTx + `"]}]}`,
path: func(m any) string {
return m.(map[string]any)["params"].([]any)[0].(map[string]any)["txs"].([]any)[0].(string)
},
},
{
name: "engine_getPayloadV4 response",
input: `{"result":{"executionPayload":{"transactions":["` + rawTx + `"]}}}`,
path: func(m any) string {
return m.(map[string]any)["result"].(map[string]any)["executionPayload"].(map[string]any)["transactions"].([]any)[0].(string)
},
},
}

for _, m := range methods {
t.Run(m.name, func(t *testing.T) {
var msg any
json.Unmarshal([]byte(m.input), &msg)
Sanitize(msg)
if got := m.path(msg); got != expectedHash {
t.Errorf("got %s, want %s", got, expectedHash)
}
})
}
}

func TestSanitize_InvalidTxShowsError(t *testing.T) {
input := `{"method":"eth_sendRawTransaction","params":["0xdeadbeef"]}`
var msg any
json.Unmarshal([]byte(input), &msg)
Sanitize(msg)

got := msg.(map[string]any)["params"].([]any)[0].(string)
if !strings.HasPrefix(got, "[error") {
t.Errorf("expected error message, got %s", got)
}
}

func TestSanitize_BatchRequest(t *testing.T) {
rawTx, expectedHash := createSignedTx(t, 0)
input := `[{"method":"eth_sendRawTransaction","params":["` + rawTx + `"]},{"method":"eth_blockNumber","params":[]}]`

var msg any
json.Unmarshal([]byte(input), &msg)
Sanitize(msg)

batch := msg.([]any)
got := batch[0].(map[string]any)["params"].([]any)[0].(string)
if got != expectedHash {
t.Errorf("got %s, want %s", got, expectedHash)
}
}

func TestSanitize_PreservesOtherFields(t *testing.T) {
rawTx, expectedHash := createSignedTx(t, 0)
input := `{"method":"engine_newPayloadV4","params":[{"transactions":["` + rawTx + `"],"blockNumber":"0x1"}]}`

var msg any
json.Unmarshal([]byte(input), &msg)
Sanitize(msg)

payload := msg.(map[string]any)["params"].([]any)[0].(map[string]any)
if payload["transactions"].([]any)[0] != expectedHash {
t.Error("transaction should be replaced with hash")
}
if payload["blockNumber"] != "0x1" {
t.Error("blockNumber should be preserved")
}
}

func TestSanitize_UnrelatedMethodUnchanged(t *testing.T) {
input := `{"method":"eth_blockNumber","params":["0x1"]}`
var msg any
json.Unmarshal([]byte(input), &msg)
Sanitize(msg)

if msg.(map[string]any)["params"].([]any)[0] != "0x1" {
t.Error("unrelated method params should be unchanged")
}
}
Loading