├── .github └── workflows │ └── checks.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── blocksub ├── blocksub.go ├── metrics.go └── subscription.go ├── cli └── cli.go ├── envflag ├── envflag.go └── envflag_test.go ├── examples ├── blocksub │ ├── main.go │ └── multisub.go ├── httplogger │ └── main.go ├── rpcserver │ └── main.go ├── send-multioperator-orderflow │ └── main.go └── tls-server │ └── main.go ├── go.mod ├── go.sum ├── httplogger └── httplogger.go ├── jsonrpc ├── mockserver.go ├── mockserver_test.go ├── request.go ├── request_test.go └── response.go ├── logutils ├── context.go ├── flush.go ├── getlogger.go ├── httprequest.go └── levels.go ├── rpcclient ├── client.go └── client_test.go ├── rpcserver ├── jsonrpc_server.go ├── jsonrpc_server_test.go ├── metrics.go ├── reflect.go └── reflect_test.go ├── rpctypes ├── types.go └── types_test.go ├── signature ├── signature.go └── signature_test.go ├── staticcheck.conf ├── tls └── tls_generate.go └── truthy ├── truthy.go └── truthy_test.go /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 1.x 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ^1.24 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | 23 | - name: Test 24 | run: make test 25 | 26 | lint: 27 | name: Lint 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Set up Go 1.x 31 | uses: actions/setup-go@v2 32 | with: 33 | go-version: ^1.24 34 | id: go 35 | 36 | - name: Check out code into the Go module directory 37 | uses: actions/checkout@v2 38 | 39 | - name: Install revive linter 40 | run: go install github.com/mgechev/revive@v1.1.3 41 | 42 | - name: Install staticcheck 43 | run: go install honnef.co/go/tools/cmd/staticcheck@master 44 | 45 | - name: Lint 46 | run: make lint 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IDE 18 | .idea/ 19 | .vscode/ 20 | cert.pem -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Flashbots 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_VER := $(shell git describe --tags --always --dirty="-dev") 2 | 3 | v: 4 | @echo "Version: ${GIT_VER}" 5 | 6 | test: 7 | go test ./... 8 | 9 | bench: 10 | go test -bench=. -run=Bench ./... 11 | 12 | fmt: 13 | gofmt -s -w . 14 | gci write . 15 | gofumpt -w -extra . 16 | go mod tidy 17 | 18 | lint: 19 | gofmt -d ./ 20 | go vet ./... 21 | staticcheck ./... 22 | 23 | cover: 24 | go test -coverprofile=/tmp/go-sim-lb.cover.tmp ./... 25 | go tool cover -func /tmp/go-sim-lb.cover.tmp 26 | unlink /tmp/go-sim-lb.cover.tmp 27 | 28 | cover-html: 29 | go test -coverprofile=/tmp/go-sim-lb.cover.tmp ./... 30 | go tool cover -html=/tmp/go-sim-lb.cover.tmp 31 | unlink /tmp/go-sim-lb.cover.tmp 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-utils 2 | 3 | [![Test status](https://github.com/flashbots/go-utils/workflows/Checks/badge.svg)](https://github.com/flashbots/go-utils/actions?query=workflow%3A%22Checks%22) 4 | 5 | Various reusable Go utilities and modules 6 | 7 | 8 | ## `cli` 9 | 10 | Various minor command-line interface helpers: [`cli.go`](https://github.com/flashbots/go-utils/blob/main/cli/cli.go) 11 | 12 | ## `httplogger` 13 | 14 | Logging middleware for HTTP requests using [`go-ethereum/log`](https://github.com/ethereum/go-ethereum/tree/master/log). 15 | 16 | See [`examples/httplogger/main.go`](https://github.com/flashbots/goutils/blob/main/examples/httplogger/main.go) 17 | 18 | Install: 19 | 20 | ```bash 21 | go get github.com/flashbots/go-utils/httplogger 22 | ``` 23 | 24 | Use: 25 | 26 | ```go 27 | mux := http.NewServeMux() 28 | mux.HandleFunc("/v1/hello", HelloHandler) 29 | loggedRouter := httplogger.LoggingMiddleware(r) 30 | ``` 31 | 32 | ## `jsonrpc` 33 | 34 | Minimal JSON-RPC client implementation. 35 | 36 | ## `blocksub` 37 | 38 | Subscribe for new Ethereum block headers by polling and/or websocket subscription 39 | 40 | See [`examples/blocksub/main.go`](https://github.com/flashbots/goutils/blob/main/examples/blocksub/main.go) and [`examples/blocksub/multisub.go`](https://github.com/flashbots/goutils/blob/main/examples/blocksub/multisub.go) 41 | 42 | Install: 43 | 44 | ```bash 45 | go get github.com/flashbots/go-utils/blocksub 46 | ``` 47 | 48 | Use: 49 | 50 | ```go 51 | blocksub := blocksub.NewBlockSub(context.Background(), httpURI, wsURI) 52 | if err := blocksub.Start(); err != nil { 53 | panic(err) 54 | } 55 | 56 | // Subscribe to new headers 57 | sub := blocksub.Subscribe(context.Background()) 58 | for header := range sub.C { 59 | fmt.Println("new header", header.Number.Uint64(), header.Hash().Hex()) 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /blocksub/blocksub.go: -------------------------------------------------------------------------------- 1 | // Package blocksub implements an Ethereum block subscriber that works with polling and/or websockets. 2 | package blocksub 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "sync" 8 | "time" 9 | 10 | "github.com/ethereum/go-ethereum" 11 | ethtypes "github.com/ethereum/go-ethereum/core/types" 12 | "github.com/ethereum/go-ethereum/ethclient" 13 | "github.com/ethereum/go-ethereum/log" 14 | "go.uber.org/atomic" 15 | ) 16 | 17 | var ErrStopped = errors.New("already stopped") 18 | var ( 19 | defaultPollTimeout = 10 * time.Second 20 | defaultSubTimeout = 60 * time.Second 21 | ) 22 | 23 | type BlockSubscriber interface { 24 | IsRunning() bool 25 | Subscribe(ctx context.Context) Subscription 26 | Start() (err error) 27 | Stop() 28 | } 29 | 30 | type BlockSub struct { 31 | PollTimeout time.Duration // 10 seconds by default (8,640 requests per day) 32 | SubTimeout time.Duration // 60 seconds by default, after this timeout the subscriber will reconnect 33 | DebugOutput bool 34 | EnableMetrics bool 35 | 36 | ethNodeHTTPURI string // usually port 8545 37 | ethNodeWebsocketURI string // usually port 8546 38 | 39 | subscriptions []*Subscription 40 | 41 | ctx context.Context 42 | cancel context.CancelFunc 43 | stopped atomic.Bool 44 | 45 | httpClient *ethclient.Client 46 | wsClient *ethclient.Client 47 | wsClientSub ethereum.Subscription 48 | internalHeaderC chan *ethtypes.Header // internal subscription channel 49 | 50 | CurrentHeader *ethtypes.Header 51 | CurrentBlockNumber uint64 52 | CurrentBlockHash string 53 | 54 | latestWsHeader *ethtypes.Header 55 | wsIsConnecting atomic.Bool 56 | wsConnectingCond *sync.Cond 57 | } 58 | 59 | func NewBlockSub(ctx context.Context, ethNodeHTTPURI, ethNodeWebsocketURI string) *BlockSub { 60 | return NewBlockSubWithTimeout(ctx, ethNodeHTTPURI, ethNodeWebsocketURI, defaultPollTimeout, defaultSubTimeout) 61 | } 62 | 63 | func NewBlockSubWithTimeout(ctx context.Context, ethNodeHTTPURI, ethNodeWebsocketURI string, pollTimeout, subTimeout time.Duration) *BlockSub { 64 | ctx, cancel := context.WithCancel(ctx) 65 | sub := &BlockSub{ 66 | PollTimeout: pollTimeout, 67 | SubTimeout: subTimeout, 68 | ethNodeHTTPURI: ethNodeHTTPURI, 69 | ethNodeWebsocketURI: ethNodeWebsocketURI, 70 | ctx: ctx, 71 | cancel: cancel, 72 | internalHeaderC: make(chan *ethtypes.Header), 73 | wsConnectingCond: sync.NewCond(new(sync.Mutex)), 74 | } 75 | return sub 76 | } 77 | 78 | func (s *BlockSub) IsRunning() bool { 79 | return !s.stopped.Load() 80 | } 81 | 82 | // Subscribe is used to create a new subscription. 83 | func (s *BlockSub) Subscribe(ctx context.Context) Subscription { 84 | sub := NewSubscription(ctx) 85 | if s.stopped.Load() { 86 | sub.Unsubscribe() 87 | } else { 88 | go sub.run() 89 | s.subscriptions = append(s.subscriptions, &sub) 90 | } 91 | return sub 92 | } 93 | 94 | // Start starts polling and websocket threads. 95 | func (s *BlockSub) Start() (err error) { 96 | if s.stopped.Load() { 97 | return ErrStopped 98 | } 99 | 100 | go s.runListener() 101 | 102 | if s.ethNodeWebsocketURI != "" { 103 | err = s.startWebsocket(false) 104 | if err != nil { 105 | return err 106 | } 107 | } 108 | 109 | if s.ethNodeHTTPURI != "" { 110 | log.Info("BlockSub:Start - HTTP connecting...", "uri", s.ethNodeHTTPURI) 111 | s.httpClient, err = ethclient.Dial(s.ethNodeHTTPURI) 112 | if err != nil { // using an invalid port will NOT return an error here, only at polling 113 | return err 114 | } 115 | 116 | // Ensure that polling works 117 | err = s._pollNow() 118 | if err != nil { 119 | return err 120 | } 121 | 122 | log.Info("BlockSub:Start - HTTP connected", "uri", s.ethNodeHTTPURI) 123 | go s.runPoller() 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // Stop closes all subscriptions and stops the polling and websocket threads. 130 | func (s *BlockSub) Stop() { 131 | if s.stopped.Swap(true) { 132 | return 133 | } 134 | 135 | for _, sub := range s.subscriptions { 136 | sub.Unsubscribe() 137 | } 138 | 139 | s.cancel() 140 | } 141 | 142 | // Listens to internal headers and forwards them to the subscriber if the header has a greater blockNumber or different hash than the previous one. 143 | func (s *BlockSub) runListener() { 144 | for { 145 | select { 146 | case <-s.ctx.Done(): 147 | s.Stop() // ensures all subscribers are properly closed 148 | return 149 | 150 | case header := <-s.internalHeaderC: 151 | // use the new header if it's later or has a different hash than the previous known one 152 | if header.Number.Uint64() >= s.CurrentBlockNumber && header.Hash().Hex() != s.CurrentBlockHash { 153 | 154 | if s.EnableMetrics { 155 | setBlockNumber(header.Number.Uint64()) 156 | } 157 | s.CurrentHeader = header 158 | s.CurrentBlockNumber = header.Number.Uint64() 159 | s.CurrentBlockHash = header.Hash().Hex() 160 | 161 | // Send to each subscriber 162 | for _, sub := range s.subscriptions { 163 | if sub.stopped.Load() { 164 | continue 165 | } 166 | 167 | select { 168 | case sub.C <- header: 169 | default: 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | func (s *BlockSub) runPoller() { 178 | ch := time.After(s.PollTimeout) 179 | for { 180 | select { 181 | case <-s.ctx.Done(): 182 | return 183 | case <-ch: 184 | err := s._pollNow() 185 | if err != nil { 186 | log.Error("BlockSub: polling latest block failed", "err", err) 187 | } 188 | ch = time.After(s.PollTimeout) 189 | } 190 | } 191 | } 192 | 193 | func (s *BlockSub) _pollNow() error { 194 | header, err := s.httpClient.HeaderByNumber(s.ctx, nil) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | if s.DebugOutput { 200 | log.Debug("BlockSub: polled block", "number", header.Number.Uint64(), "hash", header.Hash().Hex()) 201 | } 202 | s.internalHeaderC <- header 203 | 204 | // Ensure websocket is still working (force a reconnect if it lags behind) 205 | if s.latestWsHeader != nil && s.latestWsHeader.Number.Uint64() < header.Number.Uint64()-2 { 206 | log.Warn("BlockSub: forcing websocket reconnect from polling", "wsBlockNum", s.latestWsHeader.Number.Uint64(), "pollBlockNum", header.Number.Uint64()) 207 | go s.startWebsocket(true) 208 | } 209 | 210 | return nil 211 | } 212 | 213 | // startWebsocket tries to establish a websocket connection to the node. If retryForever is true it will retry forever, until it is connected. 214 | // Also blocks if another instance is currently connecting. 215 | func (s *BlockSub) startWebsocket(retryForever bool) error { 216 | if isAlreadyConnecting := s.wsIsConnecting.Swap(true); isAlreadyConnecting { 217 | s.wsConnectingCond.L.Lock() 218 | s.wsConnectingCond.Wait() 219 | s.wsConnectingCond.L.Unlock() 220 | return nil 221 | } 222 | 223 | defer func() { 224 | s.wsIsConnecting.Store(false) 225 | s.wsConnectingCond.Broadcast() 226 | }() 227 | 228 | for { 229 | if s.wsClient != nil { 230 | s.wsClient.Close() 231 | } 232 | 233 | err := s._startWebsocket() 234 | if err != nil && retryForever { 235 | log.Error("BlockSub:startWebsocket failed, retrying...", "err", err) 236 | } else { 237 | return err 238 | } 239 | } 240 | } 241 | 242 | func (s *BlockSub) _startWebsocket() (err error) { 243 | log.Info("BlockSub:_startWebsocket - connecting...", "uri", s.ethNodeWebsocketURI) 244 | 245 | s.wsClient, err = ethclient.Dial(s.ethNodeWebsocketURI) 246 | if err != nil { 247 | return err 248 | } 249 | 250 | wsHeaderC := make(chan *ethtypes.Header) 251 | s.wsClientSub, err = s.wsClient.SubscribeNewHead(s.ctx, wsHeaderC) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | // Listen for headers and errors, and reconnect if needed 257 | go func() { 258 | timer := time.NewTimer(s.SubTimeout) 259 | 260 | for { 261 | select { 262 | case <-s.ctx.Done(): 263 | return 264 | 265 | case err := <-s.wsClientSub.Err(): 266 | if err == nil { // shutdown 267 | return 268 | } 269 | 270 | // reconnect 271 | log.Warn("BlockSub: headerSub failed, reconnect now", "err", err) 272 | go s.startWebsocket(true) 273 | return 274 | 275 | case <-timer.C: 276 | log.Warn("BlockSub: timeout, reconnect now", "timeout", s.SubTimeout) 277 | go s.startWebsocket(true) 278 | return 279 | 280 | case header := <-wsHeaderC: 281 | timer.Reset(s.SubTimeout) 282 | if s.DebugOutput { 283 | log.Debug("BlockSub: sub block", "number", header.Number.Uint64(), "hash", header.Hash().Hex()) 284 | } 285 | s.latestWsHeader = header 286 | s.internalHeaderC <- header 287 | } 288 | } 289 | }() 290 | 291 | log.Info("BlockSub:_startWebsocket - connected", "uri", s.ethNodeWebsocketURI) 292 | return nil 293 | } 294 | -------------------------------------------------------------------------------- /blocksub/metrics.go: -------------------------------------------------------------------------------- 1 | package blocksub 2 | 3 | import ( 4 | "github.com/VictoriaMetrics/metrics" 5 | ) 6 | 7 | var blockNumberGauge = metrics.NewGauge(`goutils_blocksub_latest_block_number`, nil) 8 | 9 | func setBlockNumber(blockNumber uint64) { 10 | blockNumberGauge.Set(float64(blockNumber)) 11 | } 12 | -------------------------------------------------------------------------------- /blocksub/subscription.go: -------------------------------------------------------------------------------- 1 | package blocksub 2 | 3 | import ( 4 | "context" 5 | 6 | ethtypes "github.com/ethereum/go-ethereum/core/types" 7 | "go.uber.org/atomic" 8 | ) 9 | 10 | // Subscription will push new headers to a subscriber until the context is done or Unsubscribe() is called, 11 | // at which point the subscription is stopped and the header channel closed. 12 | type Subscription struct { 13 | C chan *ethtypes.Header // Channel to receive the headers on. 14 | 15 | ctx context.Context 16 | cancel context.CancelFunc 17 | 18 | stopped atomic.Bool 19 | } 20 | 21 | func NewSubscription(ctx context.Context) Subscription { 22 | ctxWithCancel, cancel := context.WithCancel(ctx) 23 | return Subscription{ 24 | C: make(chan *ethtypes.Header), 25 | ctx: ctxWithCancel, 26 | cancel: cancel, 27 | } 28 | } 29 | 30 | func (sub *Subscription) run() { 31 | <-sub.ctx.Done() 32 | sub.Unsubscribe() 33 | } 34 | 35 | // Unsubscribe unsubscribes the notification and closes the header channel. 36 | // It can safely be called more than once. 37 | func (sub *Subscription) Unsubscribe() { 38 | if sub.stopped.Swap(true) { 39 | return 40 | } 41 | sub.cancel() 42 | close(sub.C) 43 | } 44 | 45 | func (sub *Subscription) Done() <-chan struct{} { 46 | return sub.ctx.Done() 47 | } 48 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | // Package cli implements various reusable utility functions for command-line interfaces. 2 | package cli 3 | 4 | import ( 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | // CheckErr panics if err is not nil 10 | func CheckErr(err error) { 11 | if err != nil { 12 | panic(err) 13 | } 14 | } 15 | 16 | // GetEnv returns the value of the environment variable named by key, or defaultValue if the environment variable doesn't exist 17 | func GetEnv(key, defaultValue string) string { 18 | if value, ok := os.LookupEnv(key); ok { 19 | return value 20 | } 21 | return defaultValue 22 | } 23 | 24 | // GetEnvInt returns the value of the environment variable named by key, or defaultValue if the environment variable 25 | // doesn't exist or is not a valid integer 26 | func GetEnvInt(key string, defaultValue int) int { 27 | if value, ok := os.LookupEnv(key); ok { 28 | val, err := strconv.Atoi(value) 29 | if err == nil { 30 | return val 31 | } 32 | } 33 | return defaultValue 34 | } 35 | -------------------------------------------------------------------------------- /envflag/envflag.go: -------------------------------------------------------------------------------- 1 | // Package envflag is a wrapper for stdlib's flag that adds the environment 2 | // variables as additional source of the values for flags. 3 | package envflag 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/flashbots/go-utils/truthy" 13 | ) 14 | 15 | // Bool is a convenience wrapper for boolean flag that picks its default value 16 | // from the environment variable. It returns error if the environment variable's 17 | // value can not be resolved into definitive `true` or `false`. 18 | func Bool(name string, defaultValue bool, usage string) (*bool, error) { 19 | var err error 20 | value := defaultValue 21 | env := flagToEnv(name) 22 | if raw := os.Getenv(env); raw != "" { 23 | if pValue, pErr := truthy.Is(raw); pErr == nil { 24 | value = pValue 25 | } else { 26 | err = fmt.Errorf("invalid boolean value \"%s\" for environment variable %s: %w", raw, env, pErr) 27 | } 28 | } 29 | return flag.Bool(name, value, usage+fmt.Sprintf(" (env \"%s\")", env)), err 30 | } 31 | 32 | // MustBool handles error (if any) returned by Bool according to the behaviour 33 | // configured by `flag.CommandLine.ErrorHandling()` by either ignoring it, 34 | // exiting the process with status code 2, or panicking. 35 | func MustBool(name string, defaultValue bool, usage string) *bool { 36 | res, err := Bool(name, defaultValue, usage) 37 | if err != nil { 38 | switch flag.CommandLine.ErrorHandling() { 39 | case flag.ContinueOnError: 40 | // continue 41 | case flag.ExitOnError: 42 | fmt.Fprintln(os.Stderr, err) 43 | os.Exit(2) 44 | case flag.PanicOnError: 45 | panic(err) 46 | } 47 | } 48 | if res == nil { // should never happen, guard added for NilAway 49 | panic(fmt.Sprintf("MustBool res for '%s' is nil", name)) 50 | } 51 | return res 52 | } 53 | 54 | // Int is a convenience wrapper for integer flag that picks its default value 55 | // from the environment variable. It returns error if the environment variable's 56 | // value can not be parsed into integer. 57 | func Int(name string, defaultValue int, usage string) (*int, error) { 58 | var err error 59 | value := defaultValue 60 | env := flagToEnv(name) 61 | if raw := os.Getenv(env); raw != "" { 62 | if pValue, pErr := strconv.Atoi(raw); pErr == nil { 63 | value = pValue 64 | } else { 65 | err = fmt.Errorf("invalid integer value \"%s\" for environment variable %s: %w", raw, env, pErr) 66 | } 67 | } 68 | return flag.Int(name, value, usage+fmt.Sprintf(" (env \"%s\")", env)), err 69 | } 70 | 71 | // MustInt handles error (if any) returned by Int according to the behaviour 72 | // configured by `flag.CommandLine.ErrorHandling()` by either ignoring it, 73 | // exiting the process with status code 2, or panicking. 74 | func MustInt(name string, defaultValue int, usage string) *int { 75 | res, err := Int(name, defaultValue, usage) 76 | if err != nil { 77 | switch flag.CommandLine.ErrorHandling() { 78 | case flag.ContinueOnError: 79 | // continue 80 | case flag.ExitOnError: 81 | fmt.Fprintln(os.Stderr, err) 82 | os.Exit(2) 83 | case flag.PanicOnError: 84 | panic(err) 85 | } 86 | } 87 | 88 | if res == nil { // should never happen, guard added for NilAway 89 | panic(fmt.Sprintf("MustInt res for '%s' is nil", name)) 90 | } 91 | 92 | return res 93 | } 94 | 95 | // String is a convenience wrapper for string flag that picks its default value 96 | // from the environment variable. 97 | func String(name, defaultValue, usage string) *string { 98 | value := defaultValue 99 | env := flagToEnv(name) 100 | if raw := os.Getenv(env); raw != "" { 101 | value = raw 102 | } 103 | return flag.String(name, value, usage+fmt.Sprintf(" (env \"%s\")", env)) 104 | } 105 | 106 | func flagToEnv(flag string) string { 107 | return strings.ToUpper( 108 | strings.ReplaceAll(flag, "-", "_"), 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /envflag/envflag_test.go: -------------------------------------------------------------------------------- 1 | package envflag_test 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "testing" 7 | 8 | "github.com/flashbots/go-utils/envflag" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBool(t *testing.T) { 13 | const name = "bool-var" 14 | const env = "BOOL_VAR" 15 | 16 | args := make([]string, len(os.Args)) 17 | copy(os.Args, args) 18 | defer func() { 19 | os.Args = make([]string, len(args)) 20 | copy(args, os.Args) 21 | }() 22 | 23 | { // cli: absent; env: absent; default: false 24 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 25 | os.Args = []string{"envflag.test"} 26 | os.Unsetenv(env) 27 | f := envflag.MustBool(name, false, "") 28 | assert.NotNil(t, f) 29 | flag.Parse() 30 | assert.False(t, *f) 31 | } 32 | { // cli: absent; env: absent; default: true 33 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 34 | os.Args = []string{"envflag.test"} 35 | os.Unsetenv(env) 36 | f := envflag.MustBool(name, true, "") 37 | assert.NotNil(t, f) 38 | flag.Parse() 39 | assert.True(t, *f) 40 | } 41 | { // cli: absent; env: false; default: false 42 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 43 | os.Args = []string{"envflag.test"} 44 | t.Setenv(env, "0") 45 | f := envflag.MustBool(name, false, "") 46 | assert.NotNil(t, f) 47 | flag.Parse() 48 | assert.False(t, *f) 49 | } 50 | { // cli: absent; env: false; default: true 51 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 52 | os.Args = []string{"envflag.test"} 53 | t.Setenv(env, "0") 54 | f := envflag.MustBool(name, true, "") 55 | assert.NotNil(t, f) 56 | flag.Parse() 57 | assert.False(t, *f) 58 | } 59 | { // cli: absent; env: true; default: false 60 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 61 | os.Args = []string{"envflag.test"} 62 | t.Setenv(env, "1") 63 | f := envflag.MustBool(name, false, "") 64 | assert.NotNil(t, f) 65 | flag.Parse() 66 | assert.True(t, *f) 67 | } 68 | { // cli: absent; env: true; default: true 69 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 70 | os.Args = []string{"envflag.test"} 71 | t.Setenv(env, "1") 72 | f := envflag.MustBool(name, true, "") 73 | assert.NotNil(t, f) 74 | flag.Parse() 75 | assert.True(t, *f) 76 | } 77 | 78 | { // cli: false; env: absent; default: false 79 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 80 | os.Args = []string{"envflag.test", "-" + name + "=false"} 81 | os.Unsetenv(env) 82 | f := envflag.MustBool(name, false, "") 83 | assert.NotNil(t, f) 84 | flag.Parse() 85 | assert.False(t, *f) 86 | } 87 | { // cli: false; env: absent; default: true 88 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 89 | os.Args = []string{"envflag.test", "-" + name + "=false"} 90 | os.Unsetenv(env) 91 | f := envflag.MustBool(name, true, "") 92 | assert.NotNil(t, f) 93 | flag.Parse() 94 | assert.False(t, *f) 95 | } 96 | { // cli: false; env: false; default: false 97 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 98 | os.Args = []string{"envflag.test", "-" + name + "=false"} 99 | t.Setenv(env, "0") 100 | f := envflag.MustBool(name, false, "") 101 | assert.NotNil(t, f) 102 | flag.Parse() 103 | assert.False(t, *f) 104 | } 105 | { // cli: false; env: false; default: true 106 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 107 | os.Args = []string{"envflag.test", "-" + name + "=false"} 108 | t.Setenv(env, "0") 109 | f := envflag.MustBool(name, true, "") 110 | assert.NotNil(t, f) 111 | flag.Parse() 112 | assert.False(t, *f) 113 | } 114 | { // cli: false; env: true; default: false 115 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 116 | os.Args = []string{"envflag.test", "-" + name + "=false"} 117 | t.Setenv(env, "1") 118 | f := envflag.MustBool(name, false, "") 119 | assert.NotNil(t, f) 120 | flag.Parse() 121 | assert.False(t, *f) 122 | } 123 | { // cli: false; env: true; default: true 124 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 125 | os.Args = []string{"envflag.test", "-" + name + "=false"} 126 | t.Setenv(env, "1") 127 | f := envflag.MustBool(name, true, "") 128 | assert.NotNil(t, f) 129 | flag.Parse() 130 | assert.False(t, *f) 131 | } 132 | 133 | { // cli: true; env: absent; default: false 134 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 135 | os.Args = []string{"envflag.test", "-" + name} 136 | os.Unsetenv(env) 137 | f := envflag.MustBool(name, false, "") 138 | assert.NotNil(t, f) 139 | flag.Parse() 140 | assert.True(t, *f) 141 | } 142 | { // cli: true; env: absent; default: true 143 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 144 | os.Args = []string{"envflag.test", "-" + name} 145 | os.Unsetenv(env) 146 | f := envflag.MustBool(name, true, "") 147 | assert.NotNil(t, f) 148 | flag.Parse() 149 | assert.True(t, *f) 150 | } 151 | { // cli: true; env: false; default: false 152 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 153 | os.Args = []string{"envflag.test", "-" + name} 154 | t.Setenv(env, "0") 155 | f := envflag.MustBool(name, false, "") 156 | assert.NotNil(t, f) 157 | flag.Parse() 158 | assert.True(t, *f) 159 | } 160 | { // cli: true; env: false; default: true 161 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 162 | os.Args = []string{"envflag.test", "-" + name} 163 | t.Setenv(env, "0") 164 | f := envflag.MustBool(name, true, "") 165 | assert.NotNil(t, f) 166 | flag.Parse() 167 | assert.True(t, *f) 168 | } 169 | { // cli: true; env: true; default: false 170 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 171 | os.Args = []string{"envflag.test", "-" + name} 172 | t.Setenv(env, "1") 173 | f := envflag.MustBool(name, false, "") 174 | assert.NotNil(t, f) 175 | flag.Parse() 176 | assert.True(t, *f) 177 | } 178 | { // cli: true; env: true; default: true 179 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 180 | os.Args = []string{"envflag.test", "-" + name} 181 | t.Setenv(env, "1") 182 | f := envflag.MustBool(name, true, "") 183 | assert.NotNil(t, f) 184 | flag.Parse() 185 | assert.True(t, *f) 186 | } 187 | } 188 | 189 | func TestInt(t *testing.T) { 190 | const name = "int-var" 191 | const env = "INT_VAR" 192 | 193 | args := make([]string, len(os.Args)) 194 | copy(os.Args, args) 195 | defer func() { 196 | os.Args = make([]string, len(args)) 197 | copy(args, os.Args) 198 | }() 199 | 200 | { // cli: absent; env: absent; default: 42 201 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 202 | os.Args = []string{"envflag.test"} 203 | os.Unsetenv(env) 204 | f := envflag.MustInt(name, 42, "") 205 | assert.NotNil(t, f) 206 | flag.Parse() 207 | assert.Equal(t, 42, *f) 208 | } 209 | { // cli: absent; env: 42; default: 0 210 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 211 | os.Args = []string{"envflag.test"} 212 | t.Setenv(env, "42") 213 | f := envflag.MustInt(name, 0, "") 214 | assert.NotNil(t, f) 215 | flag.Parse() 216 | assert.Equal(t, 42, *f) 217 | } 218 | { // cli: 42; env: 21; default: 0 219 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 220 | os.Args = []string{"envflag.test", "-" + name, "42"} 221 | t.Setenv(env, "21") 222 | f := envflag.MustInt(name, 0, "") 223 | assert.NotNil(t, f) 224 | flag.Parse() 225 | assert.Equal(t, 42, *f) 226 | } 227 | } 228 | 229 | func TestString(t *testing.T) { 230 | const name = "string-var" 231 | const env = "STRING_VAR" 232 | 233 | args := make([]string, len(os.Args)) 234 | copy(os.Args, args) 235 | defer func() { 236 | os.Args = make([]string, len(args)) 237 | copy(args, os.Args) 238 | }() 239 | 240 | { // cli: absent; env: absent; default: 42 241 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 242 | os.Args = []string{"envflag.test"} 243 | os.Unsetenv(env) 244 | f := envflag.String(name, "42", "") 245 | assert.NotNil(t, f) 246 | flag.Parse() 247 | assert.Equal(t, "42", *f) 248 | } 249 | { // cli: absent; env: 42; default: 0 250 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 251 | os.Args = []string{"envflag.test"} 252 | t.Setenv(env, "42") 253 | f := envflag.String(name, "0", "") 254 | assert.NotNil(t, f) 255 | flag.Parse() 256 | assert.Equal(t, "42", *f) 257 | } 258 | { // cli: 42; env: 21; default: 0 259 | flag.CommandLine = flag.NewFlagSet("test", flag.ContinueOnError) 260 | os.Args = []string{"envflag.test", "-" + name, "42"} 261 | t.Setenv(env, "21") 262 | f := envflag.String(name, "0", "") 263 | assert.NotNil(t, f) 264 | flag.Parse() 265 | assert.Equal(t, "42", *f) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /examples/blocksub/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log/slog" 7 | "os" 8 | 9 | "github.com/ethereum/go-ethereum/log" 10 | "github.com/flashbots/go-utils/blocksub" 11 | ) 12 | 13 | var ( 14 | httpURI = os.Getenv("ETH_HTTP") // usually port 8545 15 | wsURI = os.Getenv("ETH_WS") // usually port 8546 16 | logJSON = os.Getenv("LOG_JSON") == "1" 17 | logDebug = os.Getenv("DEBUG") == "1" 18 | ) 19 | 20 | func logSetup() { 21 | logLevel := log.LevelInfo 22 | if logDebug { 23 | logLevel = log.LevelDebug 24 | } 25 | 26 | output := io.Writer(os.Stderr) 27 | var handler slog.Handler = log.NewTerminalHandlerWithLevel(output, logLevel, true) 28 | if logJSON { 29 | handler = log.JSONHandler(output) 30 | } 31 | 32 | log.SetDefault(log.NewLogger(handler)) 33 | } 34 | 35 | func main() { 36 | logSetup() 37 | 38 | DemoSimpleSub(httpURI, wsURI) 39 | // DemoMultiSub(httpURI, wsURI) 40 | } 41 | 42 | func DemoSimpleSub(httpURI, wsURI string) { 43 | // Create and start a BlockSub instance 44 | blocksub := blocksub.NewBlockSub(context.Background(), httpURI, wsURI) 45 | blocksub.DebugOutput = true 46 | if err := blocksub.Start(); err != nil { 47 | log.Crit(err.Error()) 48 | } 49 | 50 | // Create a subscription to new headers 51 | sub := blocksub.Subscribe(context.Background()) 52 | for header := range sub.C { 53 | log.Info("new header", "number", header.Number.Uint64(), "hash", header.Hash().Hex()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/blocksub/multisub.go: -------------------------------------------------------------------------------- 1 | // Example for multiple subscribers 2 | package main 3 | 4 | import ( 5 | "context" 6 | "time" 7 | 8 | "github.com/ethereum/go-ethereum/log" 9 | "github.com/flashbots/go-utils/blocksub" 10 | ) 11 | 12 | func DemoMultiSub(httpURI, wsURI string) { 13 | // Create and start a BlockSub instance 14 | blocksub := blocksub.NewBlockSub(context.Background(), httpURI, wsURI) 15 | blocksub.DebugOutput = true 16 | if err := blocksub.Start(); err != nil { 17 | log.Crit(err.Error()) 18 | } 19 | 20 | // Create two regular subscriptions 21 | go listen(1, blocksub.Subscribe(context.Background())) 22 | go listen(2, blocksub.Subscribe(context.Background())) 23 | 24 | // Create a third subscription, which will be cancelled after 10 seconds 25 | ctx, cancel := context.WithCancel(context.Background()) 26 | go listen(3, blocksub.Subscribe(ctx)) 27 | time.Sleep(10 * time.Second) 28 | cancel() 29 | 30 | // // Wait 10 seconds and then stop the blocksub 31 | // time.Sleep(10 * time.Second) 32 | // blocksub.Stop() 33 | 34 | // Sleep forever 35 | select {} 36 | } 37 | 38 | func listen(id int, subscription blocksub.Subscription) { 39 | for { 40 | select { 41 | case <-subscription.Done(): 42 | log.Info("sub finished", "id", id) 43 | return 44 | case header := <-subscription.C: 45 | log.Info("new header", "id", id, "number", header.Number.Uint64(), "hash", header.Hash().Hex()) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/httplogger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "net/http" 7 | 8 | "github.com/ethereum/go-ethereum/log" 9 | "github.com/flashbots/go-utils/envflag" 10 | "github.com/flashbots/go-utils/httplogger" 11 | "github.com/flashbots/go-utils/logutils" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | var listenAddr = "localhost:8124" 16 | 17 | func HelloHandler(w http.ResponseWriter, r *http.Request) { 18 | w.Write([]byte("Hello, World!")) 19 | w.WriteHeader(http.StatusOK) 20 | } 21 | 22 | func ErrorHandler(w http.ResponseWriter, r *http.Request) { 23 | l := logutils.ZapFromRequest(r) 24 | l.Error("this is an error", zap.Error(errors.New("testError"))) 25 | http.Error(w, "this is an error", http.StatusInternalServerError) 26 | } 27 | 28 | func PanicHandler(w http.ResponseWriter, r *http.Request) { 29 | panic("foo!") 30 | } 31 | 32 | func main() { 33 | logLevel := envflag.String("log-level", "info", "Log level") 34 | logDev := envflag.MustBool("log-dev", false, "Log in development mode") 35 | flag.Parse() 36 | 37 | l := logutils.MustGetZapLogger( 38 | logutils.LogDevMode(*logDev), 39 | logutils.LogLevel(*logLevel), 40 | ) 41 | defer logutils.FlushZap(l) 42 | 43 | l.Info("Webserver running at " + listenAddr) 44 | 45 | mux := http.NewServeMux() 46 | mux.HandleFunc("/hello", HelloHandler) 47 | mux.HandleFunc("/error", ErrorHandler) 48 | mux.HandleFunc("/panic", PanicHandler) 49 | loggedRouter := httplogger.LoggingMiddlewareZap(l, mux) 50 | 51 | if err := http.ListenAndServe(listenAddr, loggedRouter); err != nil { 52 | log.Crit("webserver failed", "err", err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/rpcserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/flashbots/go-utils/rpcserver" 9 | ) 10 | 11 | var listenAddr = ":8080" 12 | 13 | func main() { 14 | handler, err := rpcserver.NewJSONRPCHandler( 15 | rpcserver.Methods{ 16 | "test_foo": HandleTestFoo, 17 | }, 18 | rpcserver.JSONRPCHandlerOpts{ 19 | ServerName: "public_server", 20 | GetResponseContent: []byte("Hello world"), 21 | }, 22 | ) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | // server 28 | server := &http.Server{ 29 | Addr: listenAddr, 30 | Handler: handler, 31 | } 32 | fmt.Println("Starting server.", "listenAddr:", listenAddr) 33 | if err := server.ListenAndServe(); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func HandleTestFoo(ctx context.Context) (string, error) { 39 | return "foo", nil 40 | } 41 | -------------------------------------------------------------------------------- /examples/send-multioperator-orderflow/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This example demonstrates sending a signed eth_sendRawTransaction request to a 4 | // multioperator builder node with a specific server certificate. 5 | 6 | import ( 7 | "context" 8 | "crypto/tls" 9 | "crypto/x509" 10 | "errors" 11 | "fmt" 12 | "net/http" 13 | 14 | "github.com/ethereum/go-ethereum/common/hexutil" 15 | "github.com/flashbots/go-utils/rpcclient" 16 | "github.com/flashbots/go-utils/rpctypes" 17 | "github.com/flashbots/go-utils/signature" 18 | ) 19 | 20 | var ( 21 | // Builder node nodeEndpoint and certificate 22 | nodeEndpoint = "https://127.0.0.1:443" 23 | nodeCertPEM = []byte("-----BEGIN CERTIFICATE-----\nMIIBlTCCATugAwIBAgIQeUQhWmrcFUOKnA/HpBPdODAKBggqhkjOPQQDAjAPMQ0w\nCwYDVQQKEwRBY21lMB4XDTI0MTExNDEyMTExM1oXDTI1MTExNDEyMTExM1owDzEN\nMAsGA1UEChMEQWNtZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJCl4R+DtNqu\nyPYd8a+Ppd4lSIEgKcyGz3Q6HOnZV3D96oxW03e92FBdKUkl5DLxTYo+837u44XL\n11OWmajjKzGjeTB3MA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcD\nATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTjt0S4lYkceJnonMJBEvwjezh3\nvDAgBgNVHREEGTAXgglsb2NhbGhvc3SHBDSuK4SHBH8AAAEwCgYIKoZIzj0EAwID\nSAAwRQIgOzm8ghnR4cKiE76siQ43Q4H2RzoJUmww3NyRVFkcp6oCIQDFZmuI+2tK\n1WlX3whjllaqr33K7kAa9ntihWfo+VB9zg==\n-----END CERTIFICATE-----\n") 24 | ignoreNodeCert = false // if set to true, the client will ignore the server certificate and connect to the endpoint without verifying it 25 | 26 | // Transaction and signing key 27 | rawTxHex = "0x02f8710183195414808503a1e38a30825208947804a60641a89c9c3a31ab5abea2a18c2b6b48408788c225841b2a9f80c080a0df68a9664190a59005ab6d6cc6b8e5a1e25604f546c36da0fd26ddd44d8f7d50a05b1bcfab22a3017cabb305884d081171e0f23340ae2a13c04eb3b0dd720a0552" 28 | signerPrivateKey = "0xaccc869c5c3cb397e4833d41b138d3528af6cc5ff4808bb85a1c2ce1c8f04007" 29 | ) 30 | 31 | func createTransportForSelfSignedCert(certPEM []byte) (*http.Transport, error) { 32 | certPool := x509.NewCertPool() 33 | if ok := certPool.AppendCertsFromPEM(certPEM); !ok { 34 | return nil, errors.New("failed to add certifcate to pool") 35 | } 36 | return &http.Transport{ 37 | TLSClientConfig: &tls.Config{ 38 | RootCAs: certPool, 39 | MinVersion: tls.VersionTLS12, 40 | }, 41 | }, nil 42 | } 43 | 44 | func exampleSendRawTx() (err error) { 45 | // Create a transport that verifies (or ignores) the server certificate 46 | var transport *http.Transport 47 | if ignoreNodeCert { 48 | transport = &http.Transport{ 49 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 50 | } 51 | } else { 52 | transport, err = createTransportForSelfSignedCert(nodeCertPEM) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | 58 | // Prepare the request signer 59 | requestSigner, err := signature.NewSignerFromHexPrivateKey(signerPrivateKey) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // Setup the RPC client 65 | client := rpcclient.NewClientWithOpts(nodeEndpoint, &rpcclient.RPCClientOpts{ 66 | HTTPClient: &http.Client{ 67 | Transport: transport, 68 | }, 69 | Signer: requestSigner, 70 | }) 71 | 72 | // Execute the eth_sendRawTransaction request 73 | rawTransaction := hexutil.MustDecode(rawTxHex) 74 | resp, err := client.Call(context.Background(), "eth_sendRawTransaction", rpctypes.EthSendRawTransactionArgs(rawTransaction)) 75 | if err != nil { 76 | return err 77 | } 78 | if resp != nil && resp.Error != nil { 79 | return fmt.Errorf("rpc error: %s", resp.Error.Error()) 80 | } 81 | return nil 82 | } 83 | 84 | func main() { 85 | err := exampleSendRawTx() 86 | if err != nil { 87 | panic(err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/tls-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // 4 | // This example demonstrates how to create a TLS certificate and key and serve it on a port. 5 | // 6 | // The certificate can be required by curl like this: 7 | // 8 | // curl --cacert cert.pem https://localhost:4433 9 | // 10 | 11 | import ( 12 | "crypto/tls" 13 | "fmt" 14 | "net/http" 15 | "os" 16 | "time" 17 | 18 | utils_tls "github.com/flashbots/go-utils/tls" 19 | ) 20 | 21 | // Configuration 22 | const listenAddr = ":4433" 23 | const certPath = "cert.pem" 24 | 25 | func main() { 26 | cert, key, err := utils_tls.GenerateTLS(time.Hour*24*265, []string{"localhost"}) 27 | if err != nil { 28 | panic(err) 29 | } 30 | fmt.Println("Generated TLS certificate and key:") 31 | fmt.Println(string(cert)) 32 | 33 | // write cert to file 34 | err = os.WriteFile(certPath, cert, 0644) 35 | if err != nil { 36 | panic(err) 37 | } 38 | fmt.Println("Wrote certificate to", certPath) 39 | 40 | certificate, err := tls.X509KeyPair(cert, key) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | mux := http.NewServeMux() 46 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 47 | // write certificate to response 48 | _, _ = w.Write(cert) 49 | }) 50 | 51 | srv := &http.Server{ 52 | Addr: listenAddr, 53 | Handler: mux, 54 | ReadHeaderTimeout: time.Second, 55 | TLSConfig: &tls.Config{ 56 | Certificates: []tls.Certificate{certificate}, 57 | MinVersion: tls.VersionTLS13, 58 | PreferServerCipherSuites: true, 59 | }, 60 | } 61 | 62 | fmt.Println("Starting HTTPS server", "addr", listenAddr) 63 | if err := srv.ListenAndServeTLS("", ""); err != nil { 64 | panic(err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flashbots/go-utils 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/VictoriaMetrics/metrics v1.35.1 7 | github.com/ethereum/go-ethereum v1.15.5 8 | github.com/goccy/go-json v0.10.5 9 | github.com/google/uuid v1.3.1 10 | github.com/sirupsen/logrus v1.9.3 11 | github.com/stretchr/testify v1.10.0 12 | go.uber.org/atomic v1.11.0 13 | go.uber.org/zap v1.25.0 14 | golang.org/x/crypto v0.32.0 15 | ) 16 | 17 | require ( 18 | github.com/Microsoft/go-winio v0.6.2 // indirect 19 | github.com/StackExchange/wmi v1.2.1 // indirect 20 | github.com/bits-and-blooms/bitset v1.17.0 // indirect 21 | github.com/consensys/bavard v0.1.22 // indirect 22 | github.com/consensys/gnark-crypto v0.14.0 // indirect 23 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect 24 | github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/deckarep/golang-set/v2 v2.6.0 // indirect 27 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 28 | github.com/ethereum/c-kzg-4844 v1.0.0 // indirect 29 | github.com/ethereum/go-verkle v0.2.2 // indirect 30 | github.com/go-ole/go-ole v1.3.0 // indirect 31 | github.com/gorilla/websocket v1.4.2 // indirect 32 | github.com/holiman/uint256 v1.3.2 // indirect 33 | github.com/mmcloughlin/addchain v0.4.0 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect 36 | github.com/supranational/blst v0.3.14 // indirect 37 | github.com/tklauser/go-sysconf v0.3.12 // indirect 38 | github.com/tklauser/numcpus v0.6.1 // indirect 39 | github.com/valyala/fastrand v1.1.0 // indirect 40 | github.com/valyala/histogram v1.2.0 // indirect 41 | go.uber.org/multierr v1.11.0 // indirect 42 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect 43 | golang.org/x/sync v0.10.0 // indirect 44 | golang.org/x/sys v0.29.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | rsc.io/tmplfunc v0.0.3 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= 2 | github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 3 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 4 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 5 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 6 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 7 | github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= 8 | github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= 9 | github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU= 10 | github.com/VictoriaMetrics/metrics v1.35.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= 11 | github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 12 | github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI= 16 | github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 17 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 18 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= 20 | github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= 21 | github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= 22 | github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= 23 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= 24 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= 25 | github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= 26 | github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= 27 | github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= 28 | github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 29 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= 30 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= 31 | github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A= 32 | github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= 33 | github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E= 34 | github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0= 35 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 36 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 37 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= 38 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= 39 | github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= 40 | github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= 41 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 43 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= 45 | github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= 46 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 47 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 48 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 49 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 50 | github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= 51 | github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= 52 | github.com/ethereum/go-ethereum v1.15.5 h1:Fo2TbBWC61lWVkFw9tsMoHCNX1ndpuaQBRJ8H6xLUPo= 53 | github.com/ethereum/go-ethereum v1.15.5/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo= 54 | github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= 55 | github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= 56 | github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= 57 | github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= 58 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 59 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 60 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 61 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 62 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 63 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 64 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 65 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 66 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 67 | github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= 68 | github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 69 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 70 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 71 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 72 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 73 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 74 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 75 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 76 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 77 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 78 | github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= 79 | github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= 80 | github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= 81 | github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= 82 | github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= 83 | github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= 84 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= 85 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 86 | github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= 87 | github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= 88 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 89 | github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 90 | github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= 91 | github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 92 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 93 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 94 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 95 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 96 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 97 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 98 | github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= 99 | github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= 100 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 101 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 102 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 103 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 104 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 105 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 106 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 107 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 108 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 109 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 110 | github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= 111 | github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= 112 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 113 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 114 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= 115 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 116 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 117 | github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= 118 | github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= 119 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 120 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 121 | github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= 122 | github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= 123 | github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= 124 | github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= 125 | github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= 126 | github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= 127 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 128 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 129 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 130 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 131 | github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= 132 | github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 133 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= 134 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 135 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 136 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 137 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 138 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 139 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 140 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 141 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 142 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 143 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 144 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 145 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 146 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 147 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= 148 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 149 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 150 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 151 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 152 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 153 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 154 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 155 | github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= 156 | github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 157 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 158 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 159 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 160 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 161 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 162 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 163 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 164 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 165 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 166 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 167 | github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= 168 | github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 169 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 170 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 171 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 172 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 173 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 174 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 175 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 176 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 177 | go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= 178 | go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= 179 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 180 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 181 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 182 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 183 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 184 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 185 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 191 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 192 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 193 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 194 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 195 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 196 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 197 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 198 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 199 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 200 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 201 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 202 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 203 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 204 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 205 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 206 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 207 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 208 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 209 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 210 | -------------------------------------------------------------------------------- /httplogger/httplogger.go: -------------------------------------------------------------------------------- 1 | // Package httplogger implements a middleware that logs the incoming HTTP request & its duration using go-ethereum-log or logrus 2 | package httplogger 3 | 4 | import ( 5 | "encoding/base64" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "runtime/debug" 10 | "time" 11 | 12 | "github.com/ethereum/go-ethereum/log" 13 | "github.com/flashbots/go-utils/logutils" 14 | "github.com/google/uuid" 15 | "github.com/sirupsen/logrus" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | // responseWriter is a minimal wrapper for http.ResponseWriter that allows the 20 | // written HTTP status code to be captured for logging. 21 | type responseWriter struct { 22 | http.ResponseWriter 23 | status int 24 | wroteHeader bool 25 | } 26 | 27 | func wrapResponseWriter(w http.ResponseWriter) *responseWriter { 28 | return &responseWriter{ResponseWriter: w} 29 | } 30 | 31 | func (rw *responseWriter) Status() int { 32 | return rw.status 33 | } 34 | 35 | func (rw *responseWriter) WriteHeader(code int) { 36 | if rw.wroteHeader { 37 | return 38 | } 39 | 40 | rw.status = code 41 | rw.ResponseWriter.WriteHeader(code) 42 | rw.wroteHeader = true 43 | } 44 | 45 | // LoggingMiddleware logs the incoming HTTP request & its duration. 46 | func LoggingMiddleware(next http.Handler) http.Handler { 47 | return http.HandlerFunc( 48 | func(w http.ResponseWriter, r *http.Request) { 49 | defer func() { 50 | if err := recover(); err != nil { 51 | w.WriteHeader(http.StatusInternalServerError) 52 | 53 | method := "" 54 | url := "" 55 | if r != nil { 56 | method = r.Method 57 | url = r.URL.EscapedPath() 58 | } 59 | 60 | log.Error(fmt.Sprintf("http request panic: %s %s", method, url), 61 | "err", err, 62 | "trace", string(debug.Stack()), 63 | ) 64 | } 65 | }() 66 | start := time.Now() 67 | wrapped := wrapResponseWriter(w) 68 | next.ServeHTTP(wrapped, r) 69 | log.Info(fmt.Sprintf("http: %s %s %d", r.Method, r.URL.EscapedPath(), wrapped.status), 70 | "status", wrapped.status, 71 | "method", r.Method, 72 | "path", r.URL.EscapedPath(), 73 | "duration", fmt.Sprintf("%f", time.Since(start).Seconds()), 74 | ) 75 | }, 76 | ) 77 | } 78 | 79 | // LoggingMiddlewareSlog logs the incoming HTTP request & its duration. 80 | func LoggingMiddlewareSlog(logger *slog.Logger, next http.Handler) http.Handler { 81 | return http.HandlerFunc( 82 | func(w http.ResponseWriter, r *http.Request) { 83 | defer func() { 84 | if err := recover(); err != nil { 85 | w.WriteHeader(http.StatusInternalServerError) 86 | 87 | method := "" 88 | url := "" 89 | if r != nil { 90 | method = r.Method 91 | url = r.URL.EscapedPath() 92 | } 93 | 94 | logger.Error(fmt.Sprintf("http request panic: %s %s", method, url), 95 | "err", err, 96 | "trace", string(debug.Stack()), 97 | "method", r.Method, 98 | ) 99 | } 100 | }() 101 | start := time.Now() 102 | wrapped := wrapResponseWriter(w) 103 | next.ServeHTTP(wrapped, r) 104 | logger.Info(fmt.Sprintf("http: %s %s %d", r.Method, r.URL.EscapedPath(), wrapped.status), 105 | "status", wrapped.status, 106 | "method", r.Method, 107 | "path", r.URL.EscapedPath(), 108 | "duration", fmt.Sprintf("%f", time.Since(start).Seconds()), 109 | "durationUs", fmt.Sprint(time.Since(start).Microseconds()), 110 | ) 111 | }, 112 | ) 113 | } 114 | 115 | // LoggingMiddlewareLogrus logs the incoming HTTP request & its duration. 116 | func LoggingMiddlewareLogrus(logger *logrus.Entry, next http.Handler) http.Handler { 117 | return http.HandlerFunc( 118 | func(w http.ResponseWriter, r *http.Request) { 119 | defer func() { 120 | if err := recover(); err != nil { 121 | w.WriteHeader(http.StatusInternalServerError) 122 | 123 | method := "" 124 | url := "" 125 | if r != nil { 126 | method = r.Method 127 | url = r.URL.EscapedPath() 128 | } 129 | 130 | logger.WithFields(logrus.Fields{ 131 | "err": err, 132 | "trace": string(debug.Stack()), 133 | "method": r.Method, 134 | }).Error(fmt.Sprintf("http request panic: %s %s", method, url)) 135 | } 136 | }() 137 | start := time.Now() 138 | wrapped := wrapResponseWriter(w) 139 | next.ServeHTTP(wrapped, r) 140 | logger.WithFields(logrus.Fields{ 141 | "status": wrapped.status, 142 | "method": r.Method, 143 | "path": r.URL.EscapedPath(), 144 | "duration": fmt.Sprintf("%f", time.Since(start).Seconds()), 145 | }).Info(fmt.Sprintf("http: %s %s %d", r.Method, r.URL.EscapedPath(), wrapped.status)) 146 | }, 147 | ) 148 | } 149 | 150 | // LoggingMiddlewareZap logs the incoming HTTP request & its duration. 151 | func LoggingMiddlewareZap(logger *zap.Logger, next http.Handler) http.Handler { 152 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 153 | // Generate request ID (`base64` to shorten its string representation) 154 | _uuid := [16]byte(uuid.New()) 155 | httpRequestID := base64.RawStdEncoding.EncodeToString(_uuid[:]) 156 | 157 | l := logger.With( 158 | zap.String("httpRequestID", httpRequestID), 159 | zap.String("logType", "activity"), 160 | ) 161 | r = logutils.RequestWithZap(r, l) 162 | 163 | // Handle panics 164 | defer func() { 165 | if msg := recover(); msg != nil { 166 | w.WriteHeader(http.StatusInternalServerError) 167 | var method, url string 168 | if r != nil { 169 | method = r.Method 170 | url = r.URL.EscapedPath() 171 | } 172 | l.Error("HTTP request handler panicked", 173 | zap.Any("error", msg), 174 | zap.String("method", method), 175 | zap.String("url", url), 176 | ) 177 | } 178 | }() 179 | 180 | start := time.Now() 181 | wrapped := wrapResponseWriter(w) 182 | next.ServeHTTP(w, r) 183 | 184 | // Passing request stats both in-message (for the human reader) 185 | // as well as inside the structured log (for the machine parser) 186 | logger.Info(fmt.Sprintf("%s: %s %s %d", r.URL.Scheme, r.Method, r.URL.EscapedPath(), wrapped.status), 187 | zap.Int("durationMs", int(time.Since(start).Milliseconds())), 188 | zap.Int("status", wrapped.status), 189 | zap.String("httpRequestID", httpRequestID), 190 | zap.String("logType", "access"), 191 | zap.String("method", r.Method), 192 | zap.String("path", r.URL.EscapedPath()), 193 | ) 194 | }) 195 | } 196 | -------------------------------------------------------------------------------- /jsonrpc/mockserver.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "sync" 8 | 9 | "github.com/goccy/go-json" 10 | 11 | "github.com/ethereum/go-ethereum/log" 12 | ) 13 | 14 | type MockJSONRPCServer struct { 15 | Handlers map[string]func(req *JSONRPCRequest) (interface{}, error) 16 | RequestCounter sync.Map 17 | server *httptest.Server 18 | URL string 19 | } 20 | 21 | func NewMockJSONRPCServer() *MockJSONRPCServer { 22 | s := &MockJSONRPCServer{ 23 | Handlers: make(map[string]func(req *JSONRPCRequest) (interface{}, error)), 24 | } 25 | s.server = httptest.NewServer(http.HandlerFunc(s.handleHTTPRequest)) 26 | s.URL = s.server.URL 27 | return s 28 | } 29 | 30 | func (s *MockJSONRPCServer) SetHandler(method string, handler func(req *JSONRPCRequest) (interface{}, error)) { 31 | s.Handlers[method] = handler 32 | } 33 | 34 | func (s *MockJSONRPCServer) handleHTTPRequest(w http.ResponseWriter, req *http.Request) { 35 | defer req.Body.Close() 36 | 37 | w.Header().Set("Content-Type", "application/json") 38 | testHeader := req.Header.Get("Test") 39 | w.Header().Set("Test", testHeader) 40 | 41 | returnError := func(id interface{}, err error) { 42 | res := JSONRPCResponse{ 43 | ID: id, 44 | Error: errorPayload(err), 45 | } 46 | 47 | if err := json.NewEncoder(w).Encode(res); err != nil { 48 | log.Error("error writing response", "err", err, "data", res) 49 | } 50 | } 51 | 52 | // Parse JSON RPC 53 | jsonReq := new(JSONRPCRequest) 54 | if err := json.NewDecoder(req.Body).Decode(jsonReq); err != nil { 55 | returnError(0, fmt.Errorf("failed to parse request body: %v", err)) 56 | return 57 | } 58 | 59 | jsonRPCHandler, found := s.Handlers[jsonReq.Method] 60 | if !found { 61 | returnError(jsonReq.ID, fmt.Errorf("no RPC method handler implemented for %s", jsonReq.Method)) 62 | return 63 | } 64 | 65 | s.IncrementRequestCounter(jsonReq.Method) 66 | 67 | rawRes, err := jsonRPCHandler(jsonReq) 68 | if err != nil { 69 | returnError(jsonReq.ID, err) 70 | return 71 | } 72 | 73 | w.WriteHeader(http.StatusOK) 74 | resBytes, err := json.Marshal(rawRes) 75 | if err != nil { 76 | log.Error("error marshalling rawRes", "err", err, "data", rawRes) 77 | return 78 | } 79 | 80 | res := NewJSONRPCResponse(jsonReq.ID, resBytes) 81 | if err := json.NewEncoder(w).Encode(res); err != nil { 82 | log.Error("error writing response 2", "err", err, "data", rawRes) 83 | return 84 | } 85 | } 86 | 87 | func (s *MockJSONRPCServer) IncrementRequestCounter(method string) { 88 | newCount := 0 89 | currentCount, ok := s.RequestCounter.Load(method) 90 | if ok { 91 | newCount = currentCount.(int) 92 | } 93 | s.RequestCounter.Store(method, newCount+1) 94 | } 95 | 96 | func (s *MockJSONRPCServer) GetRequestCount(method string) int { 97 | currentCount, ok := s.RequestCounter.Load(method) 98 | if ok { 99 | return currentCount.(int) 100 | } 101 | return 0 102 | } 103 | -------------------------------------------------------------------------------- /jsonrpc/mockserver_test.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestErrorResponse(t *testing.T) { 10 | server := NewMockJSONRPCServer() 11 | server.Handlers["eth_call"] = func(req *JSONRPCRequest) (interface{}, error) { 12 | return nil, &JSONRPCError{Code: 123, Message: "test"} 13 | } 14 | 15 | req := NewJSONRPCRequest(1, "eth_call", "0xabc") 16 | res, err := SendJSONRPCRequest(*req, server.URL) 17 | assert.Nil(t, err, err) 18 | assert.NotNil(t, res.Error) 19 | assert.Equal(t, 123, res.Error.Code) 20 | assert.Equal(t, "test", res.Error.Message) 21 | } 22 | 23 | func TestMockJSONRPCServer_IncrementRequestCounter(t *testing.T) { 24 | srv := NewMockJSONRPCServer() 25 | srv.RequestCounter.Store("EXISTING", 0) 26 | 27 | testCases := []struct { 28 | name string 29 | method string 30 | expectedRequestCount int 31 | }{ 32 | { 33 | name: "Existing value in map", 34 | method: "EXISTING", 35 | expectedRequestCount: 1, 36 | }, 37 | { 38 | name: "Non existing value in map", 39 | method: "UNKNOWN", 40 | expectedRequestCount: 1, 41 | }, 42 | } 43 | 44 | for _, tt := range testCases { 45 | t.Run(tt.name, func(t *testing.T) { 46 | srv.IncrementRequestCounter(tt.method) 47 | 48 | value, ok := srv.RequestCounter.Load(tt.method) 49 | 50 | assert.Equal(t, true, ok) 51 | assert.Equal(t, value, tt.expectedRequestCount) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /jsonrpc/request.go: -------------------------------------------------------------------------------- 1 | // Package jsonrpc is a minimal JSON-RPC implementation 2 | package jsonrpc 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "net/http" 8 | 9 | "github.com/goccy/go-json" 10 | ) 11 | 12 | type JSONRPCRequest struct { 13 | ID interface{} `json:"id"` 14 | Method string `json:"method"` 15 | Params []interface{} `json:"params"` 16 | Version string `json:"jsonrpc,omitempty"` 17 | } 18 | 19 | func NewJSONRPCRequest(id interface{}, method string, args interface{}) *JSONRPCRequest { 20 | return &JSONRPCRequest{ 21 | ID: id, 22 | Method: method, 23 | Params: []interface{}{args}, 24 | Version: "2.0", 25 | } 26 | } 27 | 28 | // SendJSONRPCRequest sends the request to URL and returns the general JsonRpcResponse, or an error (note: not the JSONRPCError) 29 | func SendJSONRPCRequest(req JSONRPCRequest, url string) (res *JSONRPCResponse, err error) { 30 | buf, err := json.Marshal(req) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | rawResp, err := http.Post(url, "application/json", bytes.NewBuffer(buf)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | res = new(JSONRPCResponse) 41 | if err := json.NewDecoder(rawResp.Body).Decode(res); err != nil { 42 | return nil, err 43 | } 44 | 45 | return res, nil 46 | } 47 | 48 | // SendNewJSONRPCRequest constructs a request and sends it to the URL 49 | func SendNewJSONRPCRequest(id interface{}, method string, args interface{}, url string) (res *JSONRPCResponse, err error) { 50 | req := NewJSONRPCRequest(id, method, args) 51 | return SendJSONRPCRequest(*req, url) 52 | } 53 | 54 | // SendJSONRPCRequestAndParseResult sends the request and decodes the response into the reply interface. If the JSON-RPC response 55 | // contains an Error property, the it's returned as this function's error. 56 | func SendJSONRPCRequestAndParseResult(req JSONRPCRequest, url string, reply interface{}) (err error) { 57 | res, err := SendJSONRPCRequest(req, url) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if res.Error != nil { 63 | return res.Error 64 | } 65 | 66 | if res.Result == nil { 67 | return errors.New("result is null") 68 | } 69 | 70 | return json.Unmarshal(res.Result, reply) 71 | } 72 | -------------------------------------------------------------------------------- /jsonrpc/request_test.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/goccy/go-json" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func setupMockServer() string { 12 | server := NewMockJSONRPCServer() 13 | server.Handlers["eth_call"] = func(req *JSONRPCRequest) (interface{}, error) { 14 | return "0x12345", nil 15 | } 16 | return server.URL 17 | } 18 | 19 | func TestSendJsonRpcRequest(t *testing.T) { 20 | addr := setupMockServer() 21 | 22 | req := NewJSONRPCRequest(1, "eth_call", "0xabc") 23 | res, err := SendJSONRPCRequest(*req, addr) 24 | assert.Nil(t, err, err) 25 | 26 | reply := new(string) 27 | err = json.Unmarshal(res.Result, reply) 28 | assert.Nil(t, err, err) 29 | assert.Equal(t, "0x12345", *reply) 30 | 31 | // Test an unknown RPC method 32 | req2 := NewJSONRPCRequest(2, "unknown", "foo") 33 | res2, err := SendJSONRPCRequest(*req2, addr) 34 | assert.Nil(t, err, err) 35 | assert.NotNil(t, res2.Error) 36 | } 37 | 38 | func TestSendJSONRPCRequestAndParseResult(t *testing.T) { 39 | addr := setupMockServer() 40 | 41 | req := NewJSONRPCRequest(1, "eth_call", "0xabc") 42 | res := new(string) 43 | err := SendJSONRPCRequestAndParseResult(*req, addr, res) 44 | assert.Nil(t, err, err) 45 | assert.Equal(t, "0x12345", *res) 46 | 47 | req2 := NewJSONRPCRequest(2, "unknown", "foo") 48 | res2 := new(string) 49 | err = SendJSONRPCRequestAndParseResult(*req2, addr, res2) 50 | assert.NotNil(t, err, err) 51 | } 52 | -------------------------------------------------------------------------------- /jsonrpc/response.go: -------------------------------------------------------------------------------- 1 | // Package jsonrpc is a minimal JSON-RPC implementation 2 | package jsonrpc 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/goccy/go-json" 8 | ) 9 | 10 | // As per JSON-RPC 2.0 Specification 11 | // https://www.jsonrpc.org/specification#error_object 12 | const ( 13 | ErrParse int = -32700 14 | ErrInvalidRequest int = -32600 15 | ErrMethodNotFound int = -32601 16 | ErrInvalidParams int = -32602 17 | ErrInternal int = -32603 18 | ) 19 | 20 | type JSONRPCResponse struct { 21 | ID interface{} `json:"id"` 22 | Result json.RawMessage `json:"result,omitempty"` 23 | Error *JSONRPCError `json:"error,omitempty"` 24 | Version string `json:"jsonrpc"` 25 | } 26 | 27 | // JSONRPCError as per spec: https://www.jsonrpc.org/specification#error_object 28 | type JSONRPCError struct { 29 | // A Number that indicates the error type that occurred. 30 | Code int `json:"code"` 31 | 32 | // A String providing a short description of the error. 33 | // The message SHOULD be limited to a concise single sentence. 34 | Message string `json:"message"` 35 | 36 | // A Primitive or Structured value that contains additional information about the error. 37 | Data interface{} `json:"data,omitempty"` /* optional */ 38 | } 39 | 40 | func (err *JSONRPCError) Error() string { 41 | if err.Message == "" { 42 | return fmt.Sprintf("json-rpc error %d", err.Code) 43 | } 44 | return err.Message 45 | } 46 | 47 | func (err *JSONRPCError) ErrorCode() int { 48 | return err.Code 49 | } 50 | 51 | func (err *JSONRPCError) ErrorData() interface{} { 52 | return err.Data 53 | } 54 | 55 | // Error wraps RPC errors, which contain an error code in addition to the message. 56 | type Error interface { 57 | Error() string // returns the message 58 | ErrorCode() int // returns the code 59 | } 60 | 61 | type DataError interface { 62 | Error() string // returns the message 63 | ErrorData() interface{} // returns the error data 64 | } 65 | 66 | func errorPayload(err error) *JSONRPCError { 67 | msg := &JSONRPCError{ 68 | Code: ErrInternal, 69 | Message: err.Error(), 70 | } 71 | ec, ok := err.(Error) 72 | if ok { 73 | msg.Code = ec.ErrorCode() 74 | } 75 | de, ok := err.(DataError) 76 | if ok { 77 | msg.Data = de.ErrorData() 78 | } 79 | return msg 80 | } 81 | 82 | func NewJSONRPCResponse(id interface{}, result json.RawMessage) *JSONRPCResponse { 83 | return &JSONRPCResponse{ 84 | ID: id, 85 | Result: result, 86 | Version: "2.0", 87 | } 88 | } 89 | 90 | func NewJSONRPCErrorResponse(id interface{}, code int, message string) *JSONRPCResponse { 91 | return &JSONRPCResponse{ 92 | ID: id, 93 | Error: &JSONRPCError{ 94 | Code: code, 95 | Message: message, 96 | }, 97 | Version: "2.0", 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /logutils/context.go: -------------------------------------------------------------------------------- 1 | // Package logutils implements helpers for logging. 2 | package logutils 3 | 4 | import ( 5 | "context" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type contextKey string 11 | 12 | const loggerContextKey contextKey = "logger" 13 | 14 | // ContextWithZap returns a copy of parent context injected with corresponding 15 | // zap logger. 16 | func ContextWithZap(parent context.Context, logger *zap.Logger) context.Context { 17 | return context.WithValue(parent, loggerContextKey, logger) 18 | } 19 | 20 | // ZapFromContext retrieves the zap logger passed with a context. 21 | func ZapFromContext(ctx context.Context) *zap.Logger { 22 | if l, found := ctx.Value(loggerContextKey).(*zap.Logger); found { 23 | return l 24 | } 25 | return zap.L() 26 | } 27 | -------------------------------------------------------------------------------- /logutils/flush.go: -------------------------------------------------------------------------------- 1 | package logutils 2 | 3 | import ( 4 | "errors" 5 | "html" 6 | "io/fs" 7 | "log" 8 | "time" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | // FlushZap triggers the Sync() on the logger. In case of an error, it will log it 14 | // using the logger from standard library. 15 | func FlushZap(logger *zap.Logger) { 16 | if err := logger.Sync(); err != nil { 17 | // Workaround for `inappropriate ioctl for device` or `invalid argument` errors 18 | // See: https://github.com/uber-go/zap/issues/880#issuecomment-731261906 19 | var pathErr *fs.PathError 20 | if errors.As(err, &pathErr) { 21 | if pathErr.Path == "/dev/stderr" && pathErr.Op == "sync" { 22 | return 23 | } 24 | } 25 | log.Printf( 26 | "{\"level\":\"error\",\"ts\":\"%s\",\"msg\":\"Failed to sync the logger\",\"error\":\"%s\"}\n", 27 | time.Now().Format(time.RFC3339), 28 | html.EscapeString(err.Error()), 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /logutils/getlogger.go: -------------------------------------------------------------------------------- 1 | package logutils 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | type loggerConfig struct { 9 | devMode bool 10 | level string 11 | } 12 | 13 | // LogConfigOption allows to fine-tune the configuration of the logger. 14 | type LogConfigOption = func(*loggerConfig) 15 | 16 | // LogDevMode tells the logger to work in the development mode. 17 | func LogDevMode(devMode bool) LogConfigOption { 18 | return func(lc *loggerConfig) { 19 | lc.devMode = devMode 20 | } 21 | } 22 | 23 | // LogLevel sets the desired level of logging. 24 | func LogLevel(level string) LogConfigOption { 25 | return func(lc *loggerConfig) { 26 | lc.level = level 27 | } 28 | } 29 | 30 | // GetZapLogger returns a logger created according to the provided options. In 31 | // case if anything goes wrong (for example if the log-level string can not be 32 | // parsed) it will return a logger (with configuration that is closest possible 33 | // to the desired one) and an error. 34 | func GetZapLogger(options ...LogConfigOption) (*zap.Logger, error) { 35 | cfg := &loggerConfig{ 36 | devMode: false, 37 | level: zap.InfoLevel.String(), 38 | } 39 | 40 | for _, o := range options { 41 | o(cfg) 42 | } 43 | 44 | var config zap.Config 45 | if cfg.devMode { 46 | config = zap.NewDevelopmentConfig() 47 | } else { 48 | config = zap.NewProductionConfig() 49 | } 50 | 51 | config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 52 | 53 | // Test the logger build as per the configuration we have so far 54 | // (we want to know if anything is wrong as early as possible) 55 | basicLogger, err := config.Build() 56 | if err != nil { 57 | return zap.L(), err // Return global logger for MustGetZapLogger sake 58 | } 59 | 60 | // Parse the log level 61 | level, err := zap.ParseAtomicLevel(cfg.level) 62 | if err != nil { 63 | return basicLogger, err // basicLogger is there already, so let's use it 64 | } 65 | config.Level = level 66 | 67 | // Build the final config of the logger 68 | finalLogger, err := config.Build() 69 | if err != nil { 70 | return basicLogger, err 71 | } 72 | 73 | return finalLogger, nil 74 | } 75 | 76 | // MustGetZapLogger is guaranteed to return a logger with configuration as close 77 | // as possible to the desired one. Any errors encountered in the process will be 78 | // logged as warnings with the resulting logger. 79 | func MustGetZapLogger(options ...LogConfigOption) *zap.Logger { 80 | l, err := GetZapLogger(options...) 81 | if l == nil { 82 | l = zap.L() 83 | } 84 | if err != nil { 85 | l.Warn("Error while building the logger", zap.Error(err)) 86 | } 87 | return l 88 | } 89 | -------------------------------------------------------------------------------- /logutils/httprequest.go: -------------------------------------------------------------------------------- 1 | package logutils 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | // RequestWithZap returns a shallow copy of parent request with context 10 | // being supplemented with corresponding zap logger. 11 | func RequestWithZap(parent *http.Request, logger *zap.Logger) *http.Request { 12 | return parent.WithContext( 13 | ContextWithZap(parent.Context(), logger), 14 | ) 15 | } 16 | 17 | // ZapFromRequest retrieves the zap logger passed with request's context. 18 | func ZapFromRequest(request *http.Request) *zap.Logger { 19 | return ZapFromContext(request.Context()) 20 | } 21 | -------------------------------------------------------------------------------- /logutils/levels.go: -------------------------------------------------------------------------------- 1 | package logutils 2 | 3 | import "go.uber.org/zap" 4 | 5 | var Levels = []string{ 6 | zap.DebugLevel.String(), 7 | zap.InfoLevel.String(), 8 | zap.WarnLevel.String(), 9 | zap.ErrorLevel.String(), 10 | zap.DPanicLevel.String(), 11 | zap.PanicLevel.String(), 12 | zap.FatalLevel.String(), 13 | } 14 | -------------------------------------------------------------------------------- /rpcclient/client.go: -------------------------------------------------------------------------------- 1 | // Package rpcclient is used to do jsonrpc calls with Flashbots request signatures (X-Flashbots-Signature header) 2 | // 3 | // Implemenation and interface is a slightly modified copy of https://github.com/ybbus/jsonrpc 4 | // The differences are: 5 | // * we handle case when Flashbots API returns errors incorrectly according to jsonrpc protocol (backwards compatibility) 6 | // * we don't support object params in the Call API. When you do Call with one object we set params to be [object] instead of object 7 | // * we can sign request body with ecdsa 8 | package rpcclient 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "errors" 14 | "fmt" 15 | "io" 16 | "net/http" 17 | "strconv" 18 | 19 | "github.com/goccy/go-json" 20 | 21 | "github.com/flashbots/go-utils/signature" 22 | ) 23 | 24 | const ( 25 | jsonrpcVersion = "2.0" 26 | ) 27 | 28 | // RPCClient sends JSON-RPC requests over HTTP to the provided JSON-RPC backend. 29 | // 30 | // RPCClient is created using the factory function NewClient(). 31 | type RPCClient interface { 32 | // Call is used to send a JSON-RPC request to the server endpoint. 33 | // 34 | // The spec states, that params can only be an array or an object, no primitive values. 35 | // We don't support object params in call interface and we always wrap params into array. 36 | // Use NewRequestWithObjectParam to create a request with object param. 37 | // 38 | // 1. no params: params field is omitted. e.g. Call(ctx, "getinfo") 39 | // 40 | // 2. single params primitive value: value is wrapped in array. e.g. Call(ctx, "getByID", 1423) 41 | // 42 | // 3. single params value array: array value is wrapped into array (use CallRaw to pass array to params without wrapping). e.g. Call(ctx, "storePerson", []*Person{&Person{Name: "Alex"}}) 43 | // 44 | // 4. single object or multiple params values: always wrapped in array. e.g. Call(ctx, "setDetails", "Alex, 35, "Germany", true) 45 | // 46 | // Examples: 47 | // Call(ctx, "getinfo") -> {"method": "getinfo"} 48 | // Call(ctx, "getPersonId", 123) -> {"method": "getPersonId", "params": [123]} 49 | // Call(ctx, "setName", "Alex") -> {"method": "setName", "params": ["Alex"]} 50 | // Call(ctx, "setMale", true) -> {"method": "setMale", "params": [true]} 51 | // Call(ctx, "setNumbers", []int{1, 2, 3}) -> {"method": "setNumbers", "params": [[1, 2, 3]]} 52 | // Call(ctx, "setNumbers", []int{1, 2, 3}...) -> {"method": "setNumbers", "params": [1, 2, 3]} 53 | // Call(ctx, "setNumbers", 1, 2, 3) -> {"method": "setNumbers", "params": [1, 2, 3]} 54 | // Call(ctx, "savePerson", &Person{Name: "Alex", Age: 35}) -> {"method": "savePerson", "params": [{"name": "Alex", "age": 35}]} 55 | // Call(ctx, "setPersonDetails", "Alex", 35, "Germany") -> {"method": "setPersonDetails", "params": ["Alex", 35, "Germany"}} 56 | // 57 | // for more information, see the examples or the unit tests 58 | Call(ctx context.Context, method string, params ...any) (*RPCResponse, error) 59 | 60 | // CallRaw is like Call() but without magic in the requests.Params field. 61 | // The RPCRequest object is sent exactly as you provide it. 62 | // See docs: NewRequest, RPCRequest 63 | // 64 | // It is recommended to first consider Call() and CallFor() 65 | CallRaw(ctx context.Context, request *RPCRequest) (*RPCResponse, error) 66 | 67 | // CallFor is a very handy function to send a JSON-RPC request to the server endpoint 68 | // and directly specify an object to store the response. 69 | // 70 | // out: will store the unmarshaled object, if request was successful. 71 | // should always be provided by references. can be nil even on success. 72 | // the behaviour is the same as expected from json.Unmarshal() 73 | // 74 | // method and params: see Call() function 75 | // 76 | // if the request was not successful (network, http error) or the rpc response returns an error, 77 | // an error is returned. if it was an JSON-RPC error it can be casted 78 | // to *RPCError. 79 | // 80 | CallFor(ctx context.Context, out any, method string, params ...any) error 81 | 82 | // CallBatch invokes a list of RPCRequests in a single batch request. 83 | // 84 | // Most convenient is to use the following form: 85 | // CallBatch(ctx, RPCRequests{ 86 | // NewRequest("myMethod1", 1, 2, 3), 87 | // NewRequest("myMethod2", "Test"), 88 | // }) 89 | // 90 | // You can create the []*RPCRequest array yourself, but it is not recommended and you should notice the following: 91 | // - field Params is sent as provided, so Params: 2 forms an invalid json (correct would be Params: []int{2}) 92 | // - you can use the helper function Params(1, 2, 3) to use the same format as in Call() 93 | // - field JSONRPC is overwritten and set to value: "2.0" 94 | // - field ID is overwritten and set incrementally and maps to the array position (e.g. requests[5].ID == 5) 95 | // 96 | // 97 | // Returns RPCResponses that is of type []*RPCResponse 98 | // - note that a list of RPCResponses can be received unordered so it can happen that: responses[i] != responses[i].ID 99 | // - RPCPersponses is enriched with helper functions e.g.: responses.HasError() returns true if one of the responses holds an RPCError 100 | CallBatch(ctx context.Context, requests RPCRequests) (RPCResponses, error) 101 | 102 | // CallBatchRaw invokes a list of RPCRequests in a single batch request. 103 | // It sends the RPCRequests parameter is it passed (no magic, no id autoincrement). 104 | // 105 | // Consider to use CallBatch() instead except you have some good reason not to. 106 | // 107 | // CallBatchRaw(ctx, RPCRequests{ 108 | // &RPCRequest{ 109 | // ID: 123, // this won't be replaced in CallBatchRaw 110 | // JSONRPC: "wrong", // this won't be replaced in CallBatchRaw 111 | // Method: "myMethod1", 112 | // Params: []int{1}, // there is no magic, be sure to only use array or object 113 | // }, 114 | // }) 115 | // 116 | // Returns RPCResponses that is of type []*RPCResponse 117 | // - note that a list of RPCResponses can be received unordered 118 | // - the id's must be mapped against the id's you provided 119 | // - RPCPersponses is enriched with helper functions e.g.: responses.HasError() returns true if one of the responses holds an RPCError 120 | CallBatchRaw(ctx context.Context, requests RPCRequests) (RPCResponses, error) 121 | } 122 | 123 | // RPCRequest represents a JSON-RPC request object. 124 | // 125 | // Method: string containing the method to be invoked 126 | // 127 | // Params: can be nil. if not must be an json array or object 128 | // 129 | // ID: may always be set to 0 (default can be changed) for single requests. Should be unique for every request in one batch request. 130 | // 131 | // JSONRPC: must always be set to "2.0" for JSON-RPC version 2.0 132 | // 133 | // See: http://www.jsonrpc.org/specification#request_object 134 | // 135 | // Most of the time you shouldn't create the RPCRequest object yourself. 136 | // The following functions do that for you: 137 | // Call(), CallFor(), NewRequest() 138 | // 139 | // If you want to create it yourself (e.g. in batch or CallRaw()) 140 | // you can potentially create incorrect rpc requests: 141 | // 142 | // request := &RPCRequest{ 143 | // Method: "myMethod", 144 | // Params: 2, <-- invalid since a single primitive value must be wrapped in an array 145 | // } 146 | // 147 | // correct: 148 | // 149 | // request := &RPCRequest{ 150 | // Method: "myMethod", 151 | // Params: []int{2}, 152 | // } 153 | type RPCRequest struct { 154 | Method string `json:"method"` 155 | Params any `json:"params,omitempty"` 156 | ID int `json:"id"` 157 | JSONRPC string `json:"jsonrpc"` 158 | } 159 | 160 | // NewRequest returns a new RPCRequest that can be created using the same convenient parameter syntax as Call() 161 | // 162 | // Default RPCRequest id is 0. If you want to use an id other than 0, use NewRequestWithID() or set the ID field of the returned RPCRequest manually. 163 | // 164 | // e.g. NewRequest("myMethod", "Alex", 35, true) 165 | func NewRequest(method string, params ...any) *RPCRequest { 166 | return NewRequestWithID(0, method, params...) 167 | } 168 | 169 | // NewRequestWithID returns a new RPCRequest that can be created using the same convenient parameter syntax as Call() 170 | // 171 | // e.g. NewRequestWithID(123, "myMethod", "Alex", 35, true) 172 | func NewRequestWithID(id int, method string, params ...any) *RPCRequest { 173 | // this code will omit "params" from the json output instead of having "params": null 174 | var newParams any 175 | if params != nil { 176 | newParams = params 177 | } 178 | return NewRequestWithObjectParam(id, method, newParams) 179 | } 180 | 181 | // NewRequestWithObjectParam returns a new RPCRequest that uses param object without wrapping it into array 182 | // 183 | // e.g. NewRequestWithID(struct{}{}) -> {"params": {}} 184 | func NewRequestWithObjectParam(id int, method string, params any) *RPCRequest { 185 | request := &RPCRequest{ 186 | ID: id, 187 | Method: method, 188 | Params: params, 189 | JSONRPC: jsonrpcVersion, 190 | } 191 | 192 | return request 193 | } 194 | 195 | // RPCResponse represents a JSON-RPC response object. 196 | // 197 | // Result: holds the result of the rpc call if no error occurred, nil otherwise. can be nil even on success. 198 | // 199 | // Error: holds an RPCError object if an error occurred. must be nil on success. 200 | // 201 | // ID: may always be 0 for single requests. is unique for each request in a batch call (see CallBatch()) 202 | // 203 | // JSONRPC: must always be set to "2.0" for JSON-RPC version 2.0 204 | // 205 | // See: http://www.jsonrpc.org/specification#response_object 206 | type RPCResponse struct { 207 | JSONRPC string `json:"jsonrpc"` 208 | Result any `json:"result,omitempty"` 209 | Error *RPCError `json:"error,omitempty"` 210 | ID int `json:"id"` 211 | } 212 | 213 | // RPCError represents a JSON-RPC error object if an RPC error occurred. 214 | // 215 | // Code holds the error code. 216 | // 217 | // Message holds a short error message. 218 | // 219 | // Data holds additional error data, may be nil. 220 | // 221 | // See: http://www.jsonrpc.org/specification#error_object 222 | type RPCError struct { 223 | Code int `json:"code"` 224 | Message string `json:"message"` 225 | Data any `json:"data,omitempty"` 226 | } 227 | 228 | // Error function is provided to be used as error object. 229 | func (e *RPCError) Error() string { 230 | return strconv.Itoa(e.Code) + ": " + e.Message 231 | } 232 | 233 | // HTTPError represents a error that occurred on HTTP level. 234 | // 235 | // An error of type HTTPError is returned when a HTTP error occurred (status code) 236 | // and the body could not be parsed to a valid RPCResponse object that holds a RPCError. 237 | // 238 | // Otherwise a RPCResponse object is returned with a RPCError field that is not nil. 239 | type HTTPError struct { 240 | Code int 241 | err error 242 | } 243 | 244 | // Error function is provided to be used as error object. 245 | func (e *HTTPError) Error() string { 246 | return e.err.Error() 247 | } 248 | 249 | type rpcClient struct { 250 | endpoint string 251 | httpClient *http.Client 252 | customHeaders map[string]string 253 | allowUnknownFields bool 254 | defaultRequestID int 255 | signer *signature.Signer 256 | rejectBrokenFlashbotsErrors bool 257 | } 258 | 259 | // RPCClientOpts can be provided to NewClientWithOpts() to change configuration of RPCClient. 260 | // 261 | // HTTPClient: provide a custom http.Client (e.g. to set a proxy, or tls options) 262 | // 263 | // CustomHeaders: provide custom headers, e.g. to set BasicAuth 264 | // 265 | // AllowUnknownFields: allows the rpc response to contain fields that are not defined in the rpc response specification. 266 | type RPCClientOpts struct { 267 | HTTPClient *http.Client 268 | CustomHeaders map[string]string 269 | AllowUnknownFields bool 270 | DefaultRequestID int 271 | 272 | // If Signer is set requset body will be signed and signature will be set in the X-Flashbots-Signature header 273 | Signer *signature.Signer 274 | // if true client will return error when server responds with errors like {"error": "text"} 275 | // otherwise this response will be converted to equivalent {"error": {"message": "text", "code": FlashbotsBrokenErrorResponseCode}} 276 | // Bad errors are always rejected for batch requests 277 | RejectBrokenFlashbotsErrors bool 278 | } 279 | 280 | // RPCResponses is of type []*RPCResponse. 281 | // This type is used to provide helper functions on the result list. 282 | type RPCResponses []*RPCResponse 283 | 284 | // AsMap returns the responses as map with response id as key. 285 | func (res RPCResponses) AsMap() map[int]*RPCResponse { 286 | resMap := make(map[int]*RPCResponse, 0) 287 | for _, r := range res { 288 | resMap[r.ID] = r 289 | } 290 | 291 | return resMap 292 | } 293 | 294 | // GetByID returns the response object of the given id, nil if it does not exist. 295 | func (res RPCResponses) GetByID(id int) *RPCResponse { 296 | for _, r := range res { 297 | if r.ID == id { 298 | return r 299 | } 300 | } 301 | 302 | return nil 303 | } 304 | 305 | // HasError returns true if one of the response objects has Error field != nil. 306 | func (res RPCResponses) HasError() bool { 307 | for _, res := range res { 308 | if res.Error != nil { 309 | return true 310 | } 311 | } 312 | return false 313 | } 314 | 315 | // RPCRequests is of type []*RPCRequest. 316 | // This type is used to provide helper functions on the request list. 317 | type RPCRequests []*RPCRequest 318 | 319 | // NewClient returns a new RPCClient instance with default configuration. 320 | // 321 | // endpoint: JSON-RPC service URL to which JSON-RPC requests are sent. 322 | func NewClient(endpoint string) RPCClient { 323 | return NewClientWithOpts(endpoint, nil) 324 | } 325 | 326 | // NewClientWithOpts returns a new RPCClient instance with custom configuration. 327 | // 328 | // endpoint: JSON-RPC service URL to which JSON-RPC requests are sent. 329 | // 330 | // opts: RPCClientOpts is used to provide custom configuration. 331 | func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient { 332 | rpcClient := &rpcClient{ 333 | endpoint: endpoint, 334 | httpClient: &http.Client{}, 335 | customHeaders: make(map[string]string), 336 | } 337 | 338 | if opts == nil { 339 | return rpcClient 340 | } 341 | 342 | if opts.HTTPClient != nil { 343 | rpcClient.httpClient = opts.HTTPClient 344 | } 345 | 346 | if opts.CustomHeaders != nil { 347 | for k, v := range opts.CustomHeaders { 348 | rpcClient.customHeaders[k] = v 349 | } 350 | } 351 | 352 | if opts.AllowUnknownFields { 353 | rpcClient.allowUnknownFields = true 354 | } 355 | 356 | rpcClient.defaultRequestID = opts.DefaultRequestID 357 | rpcClient.signer = opts.Signer 358 | rpcClient.rejectBrokenFlashbotsErrors = opts.RejectBrokenFlashbotsErrors 359 | 360 | return rpcClient 361 | } 362 | 363 | func (client *rpcClient) Call(ctx context.Context, method string, params ...any) (*RPCResponse, error) { 364 | request := NewRequestWithID(client.defaultRequestID, method, params...) 365 | return client.doCall(ctx, request) 366 | } 367 | 368 | func (client *rpcClient) CallRaw(ctx context.Context, request *RPCRequest) (*RPCResponse, error) { 369 | return client.doCall(ctx, request) 370 | } 371 | 372 | func (client *rpcClient) CallFor(ctx context.Context, out any, method string, params ...any) error { 373 | rpcResponse, err := client.Call(ctx, method, params...) 374 | if err != nil { 375 | return err 376 | } 377 | 378 | if rpcResponse.Error != nil { 379 | return rpcResponse.Error 380 | } 381 | 382 | return rpcResponse.GetObject(out) 383 | } 384 | 385 | func (client *rpcClient) CallBatch(ctx context.Context, requests RPCRequests) (RPCResponses, error) { 386 | if len(requests) == 0 { 387 | return nil, errors.New("empty request list") 388 | } 389 | 390 | for i, req := range requests { 391 | req.ID = i 392 | req.JSONRPC = jsonrpcVersion 393 | } 394 | 395 | return client.doBatchCall(ctx, requests) 396 | } 397 | 398 | func (client *rpcClient) CallBatchRaw(ctx context.Context, requests RPCRequests) (RPCResponses, error) { 399 | if len(requests) == 0 { 400 | return nil, errors.New("empty request list") 401 | } 402 | 403 | return client.doBatchCall(ctx, requests) 404 | } 405 | 406 | func (client *rpcClient) newRequest(ctx context.Context, req any) (*http.Request, error) { 407 | body, err := json.Marshal(req) 408 | if err != nil { 409 | return nil, err 410 | } 411 | 412 | request, err := http.NewRequestWithContext(ctx, "POST", client.endpoint, bytes.NewReader(body)) 413 | if err != nil { 414 | return nil, err 415 | } 416 | 417 | request.Header.Set("Content-Type", "application/json") 418 | request.Header.Set("Accept", "application/json") 419 | 420 | if client.signer != nil { 421 | signatureHeader, err := client.signer.Create(body) 422 | if err != nil { 423 | return nil, err 424 | } 425 | request.Header.Set(signature.HTTPHeader, signatureHeader) 426 | } 427 | 428 | // set default headers first, so that even content type and accept can be overwritten 429 | for k, v := range client.customHeaders { 430 | // check if header is "Host" since this will be set on the request struct itself 431 | if k == "Host" { 432 | request.Host = v 433 | } else { 434 | request.Header.Set(k, v) 435 | } 436 | } 437 | 438 | return request, nil 439 | } 440 | 441 | func (client *rpcClient) doCall(ctx context.Context, RPCRequest *RPCRequest) (*RPCResponse, error) { 442 | httpRequest, err := client.newRequest(ctx, RPCRequest) 443 | if err != nil { 444 | return nil, fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, client.endpoint, err) 445 | } 446 | httpResponse, err := client.httpClient.Do(httpRequest) 447 | if err != nil { 448 | return nil, fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.Redacted(), err) 449 | } 450 | defer httpResponse.Body.Close() 451 | 452 | body, err := io.ReadAll(httpResponse.Body) 453 | if err != nil { 454 | return nil, fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.Redacted(), err) 455 | } 456 | 457 | decodeJSONBody := func(v any) error { 458 | decoder := json.NewDecoder(bytes.NewReader(body)) 459 | if !client.allowUnknownFields { 460 | decoder.DisallowUnknownFields() 461 | } 462 | decoder.UseNumber() 463 | return decoder.Decode(v) 464 | } 465 | 466 | var ( 467 | rpcResponse *RPCResponse 468 | ) 469 | err = decodeJSONBody(&rpcResponse) 470 | 471 | // parsing error 472 | if err != nil { 473 | // if we have some http error, return it 474 | if httpResponse.StatusCode >= 400 { 475 | return nil, &HTTPError{ 476 | Code: httpResponse.StatusCode, 477 | err: fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %w", RPCRequest.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode, err), 478 | } 479 | } 480 | return nil, fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %w", RPCRequest.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode, err) 481 | } 482 | 483 | // response body empty 484 | if rpcResponse == nil { 485 | // if we have some http error, return it 486 | if httpResponse.StatusCode >= 400 { 487 | return nil, &HTTPError{ 488 | Code: httpResponse.StatusCode, 489 | err: fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode), 490 | } 491 | } 492 | return nil, fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode) 493 | } 494 | 495 | return rpcResponse, nil 496 | } 497 | 498 | func (client *rpcClient) doBatchCall(ctx context.Context, rpcRequest []*RPCRequest) ([]*RPCResponse, error) { 499 | httpRequest, err := client.newRequest(ctx, rpcRequest) 500 | if err != nil { 501 | return nil, fmt.Errorf("rpc batch call on %v: %w", client.endpoint, err) 502 | } 503 | httpResponse, err := client.httpClient.Do(httpRequest) 504 | if err != nil { 505 | return nil, fmt.Errorf("rpc batch call on %v: %w", httpRequest.URL.Redacted(), err) 506 | } 507 | defer httpResponse.Body.Close() 508 | 509 | var rpcResponses RPCResponses 510 | decoder := json.NewDecoder(httpResponse.Body) 511 | if !client.allowUnknownFields { 512 | decoder.DisallowUnknownFields() 513 | } 514 | decoder.UseNumber() 515 | err = decoder.Decode(&rpcResponses) 516 | 517 | // parsing error 518 | if err != nil { 519 | // if we have some http error, return it 520 | if httpResponse.StatusCode >= 400 { 521 | return nil, &HTTPError{ 522 | Code: httpResponse.StatusCode, 523 | err: fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %w", httpRequest.URL.Redacted(), httpResponse.StatusCode, err), 524 | } 525 | } 526 | return nil, fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %w", httpRequest.URL.Redacted(), httpResponse.StatusCode, err) 527 | } 528 | 529 | // response body empty 530 | if len(rpcResponses) == 0 { 531 | // if we have some http error, return it 532 | if httpResponse.StatusCode >= 400 { 533 | return nil, &HTTPError{ 534 | Code: httpResponse.StatusCode, 535 | err: fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.Redacted(), httpResponse.StatusCode), 536 | } 537 | } 538 | return nil, fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.Redacted(), httpResponse.StatusCode) 539 | } 540 | 541 | // if we have a response body, but also a http error, return both 542 | if httpResponse.StatusCode >= 400 { 543 | return rpcResponses, &HTTPError{ 544 | Code: httpResponse.StatusCode, 545 | err: fmt.Errorf("rpc batch call on %v status code: %v. check rpc responses for potential rpc error", httpRequest.URL.Redacted(), httpResponse.StatusCode), 546 | } 547 | } 548 | 549 | return rpcResponses, nil 550 | } 551 | 552 | // GetInt converts the rpc response to an int64 and returns it. 553 | // 554 | // If result was not an integer an error is returned. 555 | func (RPCResponse *RPCResponse) GetInt() (int64, error) { 556 | val, ok := RPCResponse.Result.(json.Number) 557 | if !ok { 558 | return 0, fmt.Errorf("could not parse int64 from %s", RPCResponse.Result) 559 | } 560 | 561 | i, err := val.Int64() 562 | if err != nil { 563 | return 0, err 564 | } 565 | 566 | return i, nil 567 | } 568 | 569 | // GetFloat converts the rpc response to float64 and returns it. 570 | // 571 | // If result was not an float64 an error is returned. 572 | func (RPCResponse *RPCResponse) GetFloat() (float64, error) { 573 | val, ok := RPCResponse.Result.(json.Number) 574 | if !ok { 575 | return 0, fmt.Errorf("could not parse float64 from %s", RPCResponse.Result) 576 | } 577 | 578 | f, err := val.Float64() 579 | if err != nil { 580 | return 0, err 581 | } 582 | 583 | return f, nil 584 | } 585 | 586 | // GetBool converts the rpc response to a bool and returns it. 587 | // 588 | // If result was not a bool an error is returned. 589 | func (RPCResponse *RPCResponse) GetBool() (bool, error) { 590 | val, ok := RPCResponse.Result.(bool) 591 | if !ok { 592 | return false, fmt.Errorf("could not parse bool from %s", RPCResponse.Result) 593 | } 594 | 595 | return val, nil 596 | } 597 | 598 | // GetString converts the rpc response to a string and returns it. 599 | // 600 | // If result was not a string an error is returned. 601 | func (RPCResponse *RPCResponse) GetString() (string, error) { 602 | val, ok := RPCResponse.Result.(string) 603 | if !ok { 604 | return "", fmt.Errorf("could not parse string from %s", RPCResponse.Result) 605 | } 606 | 607 | return val, nil 608 | } 609 | 610 | // GetObject converts the rpc response to an arbitrary type. 611 | // 612 | // The function works as you would expect it from json.Unmarshal() 613 | func (RPCResponse *RPCResponse) GetObject(toType any) error { 614 | js, err := json.Marshal(RPCResponse.Result) 615 | if err != nil { 616 | return err 617 | } 618 | 619 | err = json.Unmarshal(js, toType) 620 | if err != nil { 621 | return err 622 | } 623 | 624 | return nil 625 | } 626 | -------------------------------------------------------------------------------- /rpcserver/jsonrpc_server.go: -------------------------------------------------------------------------------- 1 | // Package rpcserver allows exposing functions like: 2 | // func Foo(context, int) (int, error) 3 | // as a JSON RPC methods 4 | // 5 | // This implementation is similar to the one in go-ethereum, but the idea is to eventually replace it as a default 6 | // JSON RPC server implementation in Flasbhots projects and for this we need to reimplement some of the quirks of existing API. 7 | package rpcserver 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "io" 13 | "log/slog" 14 | "net/http" 15 | "strings" 16 | "time" 17 | 18 | "github.com/goccy/go-json" 19 | 20 | "github.com/ethereum/go-ethereum/common" 21 | "github.com/flashbots/go-utils/signature" 22 | ) 23 | 24 | var ( 25 | // this are the only errors that are returned as http errors with http error codes 26 | errMethodNotAllowed = "only POST method is allowed" 27 | errWrongContentType = "header Content-Type must be application/json" 28 | errMarshalResponse = "failed to marshal response" 29 | 30 | CodeParseError = -32700 31 | CodeInvalidRequest = -32600 32 | CodeMethodNotFound = -32601 33 | CodeInvalidParams = -32602 34 | CodeInternalError = -32603 35 | CodeCustomError = -32000 36 | 37 | DefaultMaxRequestBodySizeBytes = 30 * 1024 * 1024 // 30mb 38 | ) 39 | 40 | const ( 41 | maxOriginIDLength = 255 42 | requestSizeThreshold = 50_000 43 | ) 44 | 45 | type ( 46 | highPriorityKey struct{} 47 | signerKey struct{} 48 | originKey struct{} 49 | sizeKey struct{} 50 | ) 51 | 52 | type jsonRPCRequest struct { 53 | JSONRPC string `json:"jsonrpc"` 54 | ID any `json:"id"` 55 | Method string `json:"method"` 56 | Params []json.RawMessage `json:"params"` 57 | } 58 | 59 | type jsonRPCResponse struct { 60 | JSONRPC string `json:"jsonrpc"` 61 | ID any `json:"id"` 62 | Result *json.RawMessage `json:"result,omitempty"` 63 | Error *jsonRPCError `json:"error,omitempty"` 64 | } 65 | 66 | type jsonRPCError struct { 67 | Code int `json:"code"` 68 | Message string `json:"message"` 69 | Data *any `json:"data,omitempty"` 70 | } 71 | 72 | type JSONRPCHandler struct { 73 | JSONRPCHandlerOpts 74 | methods map[string]methodHandler 75 | } 76 | 77 | type Methods map[string]any 78 | 79 | type JSONRPCHandlerOpts struct { 80 | // Logger, can be nil 81 | Log *slog.Logger 82 | // Server name. Used to separate logs and metrics when having multiple servers in one binary. 83 | ServerName string 84 | // Max size of the request payload 85 | MaxRequestBodySizeBytes int64 86 | // If true payload signature from X-Flashbots-Signature will be verified 87 | // Result can be extracted from the context using GetSigner 88 | VerifyRequestSignatureFromHeader bool 89 | // If true signer from X-Flashbots-Signature will be extracted without verifying signature 90 | // Result can be extracted from the context using GetSigner 91 | ExtractUnverifiedRequestSignatureFromHeader bool 92 | // If true high_prio header value will be extracted (true or false) 93 | // Result can be extracted from the context using GetHighPriority 94 | ExtractPriorityFromHeader bool 95 | // If true extract value from x-flashbots-origin header 96 | // Result can be extracted from the context using GetOrigin 97 | ExtractOriginFromHeader bool 98 | // GET response content 99 | GetResponseContent []byte 100 | // Custom handler for /readyz endpoint. If not nil then it is expected to write the response to the provided ResponseWriter. 101 | // If the custom handler returns an error, the error message is written to the ResponseWriter with a 500 status code. 102 | ReadyHandler func(w http.ResponseWriter, r *http.Request) error 103 | } 104 | 105 | // NewJSONRPCHandler creates JSONRPC http.Handler from the map that maps method names to method functions 106 | // each method function must: 107 | // - have context as a first argument 108 | // - return error as a last argument 109 | // - have argument types that can be unmarshalled from JSON 110 | // - have return types that can be marshalled to JSON 111 | func NewJSONRPCHandler(methods Methods, opts JSONRPCHandlerOpts) (*JSONRPCHandler, error) { 112 | if opts.MaxRequestBodySizeBytes == 0 { 113 | opts.MaxRequestBodySizeBytes = int64(DefaultMaxRequestBodySizeBytes) 114 | } 115 | 116 | m := make(map[string]methodHandler) 117 | for name, fn := range methods { 118 | method, err := getMethodTypes(fn) 119 | if err != nil { 120 | return nil, err 121 | } 122 | m[name] = method 123 | } 124 | return &JSONRPCHandler{ 125 | JSONRPCHandlerOpts: opts, 126 | methods: m, 127 | }, nil 128 | } 129 | 130 | func (h *JSONRPCHandler) writeJSONRPCResponse(w http.ResponseWriter, response jsonRPCResponse) { 131 | w.Header().Set("Content-Type", "application/json") 132 | if err := json.NewEncoder(w).Encode(response); err != nil { 133 | if h.Log != nil { 134 | h.Log.Error("failed to marshall response", slog.Any("error", err), slog.String("serverName", h.ServerName)) 135 | } 136 | http.Error(w, errMarshalResponse, http.StatusInternalServerError) 137 | incInternalErrors(h.ServerName) 138 | return 139 | } 140 | } 141 | 142 | func (h *JSONRPCHandler) writeJSONRPCError(w http.ResponseWriter, id any, code int, msg string) { 143 | res := jsonRPCResponse{ 144 | JSONRPC: "2.0", 145 | ID: id, 146 | Result: nil, 147 | Error: &jsonRPCError{ 148 | Code: code, 149 | Message: msg, 150 | Data: nil, 151 | }, 152 | } 153 | h.writeJSONRPCResponse(w, res) 154 | } 155 | 156 | func (h *JSONRPCHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 157 | startAt := time.Now() 158 | methodForMetrics := unknownMethodLabel 159 | bigRequest := false 160 | ctx := r.Context() 161 | 162 | defer func() { 163 | incRequestCount(methodForMetrics, h.ServerName, bigRequest) 164 | incRequestDuration(time.Since(startAt), methodForMetrics, h.ServerName, bigRequest) 165 | }() 166 | 167 | stepStartAt := time.Now() 168 | 169 | // Some GET requests are allowed 170 | if r.Method == http.MethodGet { 171 | if r.URL.Path == "/livez" { 172 | w.WriteHeader(http.StatusOK) 173 | return 174 | } else if r.URL.Path == "/readyz" { 175 | if h.JSONRPCHandlerOpts.ReadyHandler == nil { 176 | http.Error(w, "ready handler is not set", http.StatusNotFound) 177 | incIncorrectRequest(h.ServerName) 178 | return 179 | } else { 180 | // Handler is expected to write the Response 181 | err := h.JSONRPCHandlerOpts.ReadyHandler(w, r) 182 | if err != nil { 183 | http.Error(w, err.Error(), http.StatusInternalServerError) 184 | incInternalErrors(h.ServerName) 185 | } 186 | return 187 | } 188 | } else if len(h.GetResponseContent) > 0 { 189 | // Static response for all other GET requests 190 | w.WriteHeader(http.StatusOK) 191 | _, err := w.Write(h.GetResponseContent) 192 | if err != nil { 193 | http.Error(w, errMarshalResponse, http.StatusInternalServerError) 194 | incInternalErrors(h.ServerName) 195 | return 196 | } 197 | return 198 | } 199 | } 200 | 201 | // From here we only accept POST requests with JSON body 202 | if r.Method != http.MethodPost { 203 | http.Error(w, errMethodNotAllowed, http.StatusMethodNotAllowed) 204 | incIncorrectRequest(h.ServerName) 205 | return 206 | } 207 | 208 | if r.Header.Get("Content-Type") != "application/json" { 209 | http.Error(w, errWrongContentType, http.StatusUnsupportedMediaType) 210 | incIncorrectRequest(h.ServerName) 211 | return 212 | } 213 | 214 | r.Body = http.MaxBytesReader(w, r.Body, h.MaxRequestBodySizeBytes) 215 | body, err := io.ReadAll(r.Body) 216 | if err != nil { 217 | msg := fmt.Sprintf("request body is too big, max size: %d", h.MaxRequestBodySizeBytes) 218 | h.writeJSONRPCError(w, nil, CodeInvalidRequest, msg) 219 | incIncorrectRequest(h.ServerName) 220 | return 221 | } 222 | bodySize := len(body) 223 | ctx = context.WithValue(ctx, sizeKey{}, bodySize) 224 | 225 | bigRequest = bodySize > requestSizeThreshold 226 | defer func(size int) { 227 | incRequestSizeBytes(size, methodForMetrics, h.ServerName) 228 | }(bodySize) 229 | 230 | stepTime := time.Since(stepStartAt) 231 | defer func(stepTime time.Duration) { 232 | incRequestDurationStep(stepTime, methodForMetrics, h.ServerName, "io", bigRequest) 233 | }(stepTime) 234 | stepStartAt = time.Now() 235 | 236 | if h.VerifyRequestSignatureFromHeader { 237 | signatureHeader := r.Header.Get("x-flashbots-signature") 238 | signer, err := signature.Verify(signatureHeader, body) 239 | if err != nil { 240 | h.writeJSONRPCError(w, nil, CodeInvalidRequest, err.Error()) 241 | incIncorrectRequest(h.ServerName) 242 | return 243 | } 244 | ctx = context.WithValue(ctx, signerKey{}, signer) 245 | } 246 | 247 | // read request 248 | var req jsonRPCRequest 249 | if err := json.Unmarshal(body, &req); err != nil { 250 | h.writeJSONRPCError(w, nil, CodeParseError, err.Error()) 251 | incIncorrectRequest(h.ServerName) 252 | return 253 | } 254 | 255 | if req.JSONRPC != "2.0" { 256 | h.writeJSONRPCError(w, req.ID, CodeParseError, "invalid jsonrpc version") 257 | incIncorrectRequest(h.ServerName) 258 | return 259 | } 260 | if req.ID != nil { 261 | // id must be string or number 262 | switch req.ID.(type) { 263 | case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: 264 | default: 265 | h.writeJSONRPCError(w, req.ID, CodeParseError, "invalid id type") 266 | incIncorrectRequest(h.ServerName) 267 | return 268 | } 269 | } 270 | 271 | if h.ExtractPriorityFromHeader { 272 | highPriority := r.Header.Get("high_prio") == "true" 273 | ctx = context.WithValue(ctx, highPriorityKey{}, highPriority) 274 | } 275 | 276 | if h.ExtractUnverifiedRequestSignatureFromHeader { 277 | signature := r.Header.Get("x-flashbots-signature") 278 | if split := strings.Split(signature, ":"); len(split) > 0 { 279 | signer := common.HexToAddress(split[0]) 280 | ctx = context.WithValue(ctx, signerKey{}, signer) 281 | } 282 | } 283 | 284 | if h.ExtractOriginFromHeader { 285 | origin := r.Header.Get("x-flashbots-origin") 286 | if origin != "" { 287 | if len(origin) > maxOriginIDLength { 288 | h.writeJSONRPCError(w, req.ID, CodeInvalidRequest, "x-flashbots-origin header is too long") 289 | incIncorrectRequest(h.ServerName) 290 | return 291 | } 292 | ctx = context.WithValue(ctx, originKey{}, origin) 293 | } 294 | } 295 | 296 | // get method 297 | method, ok := h.methods[req.Method] 298 | if !ok { 299 | h.writeJSONRPCError(w, req.ID, CodeMethodNotFound, "method not found") 300 | incIncorrectRequest(h.ServerName) 301 | return 302 | } 303 | methodForMetrics = req.Method 304 | 305 | incRequestDurationStep(time.Since(stepStartAt), methodForMetrics, h.ServerName, "parse", bigRequest) 306 | stepStartAt = time.Now() 307 | 308 | // call method 309 | result, err := method.call(ctx, req.Params) 310 | if err != nil { 311 | h.writeJSONRPCError(w, req.ID, CodeCustomError, err.Error()) 312 | incRequestErrorCount(methodForMetrics, h.ServerName) 313 | incRequestDurationStep(time.Since(stepStartAt), methodForMetrics, h.ServerName, "call", bigRequest) 314 | return 315 | } 316 | 317 | incRequestDurationStep(time.Since(stepStartAt), methodForMetrics, h.ServerName, "call", bigRequest) 318 | stepStartAt = time.Now() 319 | 320 | marshaledResult, err := json.Marshal(result) 321 | if err != nil { 322 | h.writeJSONRPCError(w, req.ID, CodeInternalError, err.Error()) 323 | incInternalErrors(h.ServerName) 324 | 325 | incRequestDurationStep(time.Since(stepStartAt), methodForMetrics, h.ServerName, "response", bigRequest) 326 | return 327 | } 328 | 329 | // write response 330 | rawMessageResult := json.RawMessage(marshaledResult) 331 | res := jsonRPCResponse{ 332 | JSONRPC: "2.0", 333 | ID: req.ID, 334 | Result: &rawMessageResult, 335 | Error: nil, 336 | } 337 | h.writeJSONRPCResponse(w, res) 338 | 339 | incRequestDurationStep(time.Since(stepStartAt), methodForMetrics, h.ServerName, "response", bigRequest) 340 | } 341 | 342 | func GetHighPriority(ctx context.Context) bool { 343 | value, ok := ctx.Value(highPriorityKey{}).(bool) 344 | if !ok { 345 | return false 346 | } 347 | return value 348 | } 349 | 350 | func GetSigner(ctx context.Context) common.Address { 351 | value, ok := ctx.Value(signerKey{}).(common.Address) 352 | if !ok { 353 | return common.Address{} 354 | } 355 | return value 356 | } 357 | 358 | func GetOrigin(ctx context.Context) string { 359 | value, ok := ctx.Value(originKey{}).(string) 360 | if !ok { 361 | return "" 362 | } 363 | return value 364 | } 365 | 366 | func GetRequestSize(ctx context.Context) int { 367 | return ctx.Value(sizeKey{}).(int) 368 | } 369 | -------------------------------------------------------------------------------- /rpcserver/jsonrpc_server_test.go: -------------------------------------------------------------------------------- 1 | package rpcserver 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/flashbots/go-utils/rpcclient" 13 | "github.com/flashbots/go-utils/signature" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func testHandler(opts JSONRPCHandlerOpts) *JSONRPCHandler { 18 | var ( 19 | errorArg = -1 20 | errorOut = errors.New("custom error") //nolint:goerr113 21 | ) 22 | handlerMethod := func(ctx context.Context, arg1 int) (dummyStruct, error) { 23 | if arg1 == errorArg { 24 | return dummyStruct{}, errorOut 25 | } 26 | return dummyStruct{arg1}, nil 27 | } 28 | 29 | handler, err := NewJSONRPCHandler(map[string]interface{}{ 30 | "function": handlerMethod, 31 | }, opts) 32 | if err != nil { 33 | panic(err) 34 | } 35 | return handler 36 | } 37 | 38 | func TestHandler_ServeHTTP(t *testing.T) { 39 | handler := testHandler(JSONRPCHandlerOpts{}) 40 | 41 | testCases := map[string]struct { 42 | requestBody string 43 | expectedResponse string 44 | }{ 45 | "success": { 46 | requestBody: `{"jsonrpc":"2.0","id":1,"method":"function","params":[1]}`, 47 | expectedResponse: `{"jsonrpc":"2.0","id":1,"result":{"field":1}}`, 48 | }, 49 | "error": { 50 | requestBody: `{"jsonrpc":"2.0","id":1,"method":"function","params":[-1]}`, 51 | expectedResponse: `{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"custom error"}}`, 52 | }, 53 | "invalid json": { 54 | requestBody: `{"jsonrpc":"2.0","id":1,"method":"function","params":[1]`, 55 | expectedResponse: `{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"expected comma after object element"}}`, 56 | }, 57 | "method not found": { 58 | requestBody: `{"jsonrpc":"2.0","id":1,"method":"not_found","params":[1]}`, 59 | expectedResponse: `{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}`, 60 | }, 61 | "invalid params": { 62 | requestBody: `{"jsonrpc":"2.0","id":1,"method":"function","params":[1,2]}`, 63 | expectedResponse: `{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"too much arguments"}}`, // TODO: return correct code here 64 | }, 65 | "invalid params type": { 66 | requestBody: `{"jsonrpc":"2.0","id":1,"method":"function","params":["1"]}`, 67 | expectedResponse: `{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"json: cannot unmarshal number \" into Go value of type int"}}`, 68 | }, 69 | } 70 | 71 | for name, testCase := range testCases { 72 | t.Run(name, func(t *testing.T) { 73 | body := bytes.NewReader([]byte(testCase.requestBody)) 74 | request, err := http.NewRequest(http.MethodPost, "/", body) 75 | require.NoError(t, err) 76 | request.Header.Add("Content-Type", "application/json") 77 | 78 | rr := httptest.NewRecorder() 79 | 80 | handler.ServeHTTP(rr, request) 81 | require.Equal(t, http.StatusOK, rr.Code) 82 | 83 | require.JSONEq(t, testCase.expectedResponse, rr.Body.String()) 84 | }) 85 | } 86 | } 87 | 88 | func TestJSONRPCServerWithClient(t *testing.T) { 89 | handler := testHandler(JSONRPCHandlerOpts{}) 90 | httpServer := httptest.NewServer(handler) 91 | defer httpServer.Close() 92 | 93 | client := rpcclient.NewClient(httpServer.URL) 94 | 95 | var resp dummyStruct 96 | err := client.CallFor(context.Background(), &resp, "function", 123) 97 | require.NoError(t, err) 98 | require.Equal(t, 123, resp.Field) 99 | } 100 | 101 | func TestJSONRPCServerWithSignatureWithClient(t *testing.T) { 102 | handler := testHandler(JSONRPCHandlerOpts{VerifyRequestSignatureFromHeader: true}) 103 | httpServer := httptest.NewServer(handler) 104 | defer httpServer.Close() 105 | 106 | // first we do request without signature 107 | client := rpcclient.NewClient(httpServer.URL) 108 | resp, err := client.Call(context.Background(), "function", 123) 109 | require.NoError(t, err) 110 | require.Equal(t, "no signature provided", resp.Error.Message) 111 | 112 | // call with signature 113 | signer, err := signature.NewRandomSigner() 114 | require.NoError(t, err) 115 | client = rpcclient.NewClientWithOpts(httpServer.URL, &rpcclient.RPCClientOpts{ 116 | Signer: signer, 117 | }) 118 | 119 | var structResp dummyStruct 120 | err = client.CallFor(context.Background(), &structResp, "function", 123) 121 | require.NoError(t, err) 122 | require.Equal(t, 123, structResp.Field) 123 | } 124 | 125 | func TestJSONRPCServerDefaultLiveAndReady(t *testing.T) { 126 | handler := testHandler(JSONRPCHandlerOpts{}) 127 | httpServer := httptest.NewServer(handler) 128 | defer httpServer.Close() 129 | 130 | // /livez (200 by default) 131 | request, err := http.NewRequest(http.MethodGet, "/livez", nil) 132 | require.NoError(t, err) 133 | rr := httptest.NewRecorder() 134 | handler.ServeHTTP(rr, request) 135 | require.Equal(t, http.StatusOK, rr.Code) 136 | require.Equal(t, "", rr.Body.String()) 137 | 138 | // /readyz (404 by default) 139 | request, err = http.NewRequest(http.MethodGet, "/readyz", nil) 140 | require.NoError(t, err) 141 | rr = httptest.NewRecorder() 142 | handler.ServeHTTP(rr, request) 143 | require.Equal(t, http.StatusNotFound, rr.Code) 144 | } 145 | 146 | func TestJSONRPCServerReadyzOK(t *testing.T) { 147 | handler := testHandler(JSONRPCHandlerOpts{ 148 | ReadyHandler: func(w http.ResponseWriter, r *http.Request) error { 149 | w.WriteHeader(http.StatusOK) 150 | _, err := w.Write([]byte("ready")) 151 | return err 152 | }, 153 | }) 154 | httpServer := httptest.NewServer(handler) 155 | defer httpServer.Close() 156 | 157 | request, err := http.NewRequest(http.MethodGet, "/readyz", nil) 158 | require.NoError(t, err) 159 | rr := httptest.NewRecorder() 160 | 161 | handler.ServeHTTP(rr, request) 162 | require.Equal(t, http.StatusOK, rr.Code) 163 | require.Equal(t, "ready", rr.Body.String()) 164 | } 165 | 166 | func TestJSONRPCServerReadyzError(t *testing.T) { 167 | handler := testHandler(JSONRPCHandlerOpts{ 168 | ReadyHandler: func(w http.ResponseWriter, r *http.Request) error { 169 | return fmt.Errorf("not ready") 170 | }, 171 | }) 172 | httpServer := httptest.NewServer(handler) 173 | defer httpServer.Close() 174 | 175 | request, err := http.NewRequest(http.MethodGet, "/readyz", nil) 176 | require.NoError(t, err) 177 | rr := httptest.NewRecorder() 178 | 179 | handler.ServeHTTP(rr, request) 180 | require.Equal(t, http.StatusInternalServerError, rr.Code) 181 | fmt.Println(rr.Body.String()) 182 | require.Equal(t, "not ready\n", rr.Body.String()) 183 | } 184 | -------------------------------------------------------------------------------- /rpcserver/metrics.go: -------------------------------------------------------------------------------- 1 | package rpcserver 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/VictoriaMetrics/metrics" 8 | ) 9 | 10 | const ( 11 | // we use unknown method label for methods that server does not support because otherwise 12 | // users can create arbitrary number of metrics 13 | unknownMethodLabel = "unknown" 14 | 15 | // incremented when user made incorrect request 16 | incorrectRequestCounter = `goutils_rpcserver_incorrect_request_total{server_name="%s"}` 17 | 18 | // incremented when server has a bug (e.g. can't marshall response) 19 | internalErrorsCounter = `goutils_rpcserver_internal_errors_total{server_name="%s"}` 20 | 21 | // incremented when request comes in 22 | requestCountLabel = `goutils_rpcserver_request_count{method="%s",server_name="%s",is_big="%t"}` 23 | // incremented when handler method returns JSONRPC error 24 | errorCountLabel = `goutils_rpcserver_error_count{method="%s",server_name="%s"}` 25 | // total duration of the request 26 | requestDurationLabel = `goutils_rpcserver_request_duration_milliseconds{method="%s",server_name="%s",is_big="%t"}` 27 | // partial duration of the request 28 | requestDurationStepLabel = `goutils_rpcserver_request_step_duration_milliseconds{method="%s",server_name="%s",step="%s",is_big="%t"}` 29 | 30 | // request size in bytes 31 | requestSizeBytes = `goutils_rpcserver_request_size_bytes{method="%s",server_name="%s"}` 32 | ) 33 | 34 | func incRequestCount(method, serverName string, isBig bool) { 35 | l := fmt.Sprintf(requestCountLabel, method, serverName, isBig) 36 | metrics.GetOrCreateCounter(l).Inc() 37 | } 38 | 39 | func incIncorrectRequest(serverName string) { 40 | l := fmt.Sprintf(incorrectRequestCounter, serverName) 41 | metrics.GetOrCreateCounter(l).Inc() 42 | } 43 | 44 | func incRequestErrorCount(method, serverName string) { 45 | l := fmt.Sprintf(errorCountLabel, method, serverName) 46 | metrics.GetOrCreateCounter(l).Inc() 47 | } 48 | 49 | func incRequestDuration(duration time.Duration, method string, serverName string, isBig bool) { 50 | millis := float64(duration.Microseconds()) / 1000.0 51 | l := fmt.Sprintf(requestDurationLabel, method, serverName, isBig) 52 | metrics.GetOrCreateSummary(l).Update(millis) 53 | } 54 | 55 | func incInternalErrors(serverName string) { 56 | l := fmt.Sprintf(internalErrorsCounter, serverName) 57 | metrics.GetOrCreateCounter(l).Inc() 58 | } 59 | 60 | func incRequestDurationStep(duration time.Duration, method, serverName, step string, isBig bool) { 61 | millis := float64(duration.Microseconds()) / 1000.0 62 | l := fmt.Sprintf(requestDurationStepLabel, method, serverName, step, isBig) 63 | metrics.GetOrCreateSummary(l).Update(millis) 64 | } 65 | 66 | func incRequestSizeBytes(size int, method string, serverName string) { 67 | l := fmt.Sprintf(requestSizeBytes, method, serverName) 68 | metrics.GetOrCreateSummary(l).Update(float64(size)) 69 | } 70 | -------------------------------------------------------------------------------- /rpcserver/reflect.go: -------------------------------------------------------------------------------- 1 | package rpcserver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | 8 | "github.com/goccy/go-json" 9 | ) 10 | 11 | var ( 12 | ErrNotFunction = errors.New("not a function") 13 | ErrMustReturnError = errors.New("function must return error as a last return value") 14 | ErrMustHaveContext = errors.New("function must have context.Context as a first argument") 15 | ErrTooManyReturnValues = errors.New("too many return values") 16 | 17 | ErrTooMuchArguments = errors.New("too much arguments") 18 | ) 19 | 20 | type methodHandler struct { 21 | in []reflect.Type 22 | out []reflect.Type 23 | fn any 24 | } 25 | 26 | func getMethodTypes(fn interface{}) (methodHandler, error) { 27 | fnType := reflect.TypeOf(fn) 28 | if fnType.Kind() != reflect.Func { 29 | return methodHandler{}, ErrNotFunction 30 | } 31 | numIn := fnType.NumIn() 32 | in := make([]reflect.Type, numIn) 33 | for i := 0; i < numIn; i++ { 34 | in[i] = fnType.In(i) 35 | } 36 | // first input argument must be context.Context 37 | if numIn == 0 || in[0] != reflect.TypeOf((*context.Context)(nil)).Elem() { 38 | return methodHandler{}, ErrMustHaveContext 39 | } 40 | 41 | numOut := fnType.NumOut() 42 | out := make([]reflect.Type, numOut) 43 | for i := 0; i < numOut; i++ { 44 | out[i] = fnType.Out(i) 45 | } 46 | 47 | // function must contain error as a last return value 48 | if numOut == 0 || !out[numOut-1].Implements(reflect.TypeOf((*error)(nil)).Elem()) { 49 | return methodHandler{}, ErrMustReturnError 50 | } 51 | 52 | // function can return only one value 53 | if numOut > 2 { 54 | return methodHandler{}, ErrTooManyReturnValues 55 | } 56 | 57 | return methodHandler{in, out, fn}, nil 58 | } 59 | 60 | func (h methodHandler) call(ctx context.Context, params []json.RawMessage) (any, error) { 61 | args, err := extractArgumentsFromJSONparamsArray(h.in[1:], params) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | // prepend context.Context 67 | args = append([]reflect.Value{reflect.ValueOf(ctx)}, args...) 68 | 69 | // call function 70 | results := reflect.ValueOf(h.fn).Call(args) 71 | 72 | // check error 73 | var outError error 74 | if !results[len(results)-1].IsNil() { 75 | errVal, ok := results[len(results)-1].Interface().(error) 76 | if !ok { 77 | return nil, ErrMustReturnError 78 | } 79 | outError = errVal 80 | } 81 | 82 | if len(results) == 1 { 83 | return nil, outError 84 | } else { 85 | return results[0].Interface(), outError 86 | } 87 | } 88 | 89 | func extractArgumentsFromJSONparamsArray(in []reflect.Type, params []json.RawMessage) ([]reflect.Value, error) { 90 | if len(params) > len(in) { 91 | return nil, ErrTooMuchArguments 92 | } 93 | 94 | args := make([]reflect.Value, len(in)) 95 | for i, argType := range in { 96 | arg := reflect.New(argType) 97 | if i < len(params) { 98 | if err := json.Unmarshal(params[i], arg.Interface()); err != nil { 99 | return nil, err 100 | } 101 | } 102 | args[i] = arg.Elem() 103 | } 104 | return args, nil 105 | } 106 | -------------------------------------------------------------------------------- /rpcserver/reflect_test.go: -------------------------------------------------------------------------------- 1 | package rpcserver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/goccy/go-json" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type ctxKey string 15 | 16 | func rawParams(raw string) []json.RawMessage { 17 | var params []json.RawMessage 18 | err := json.Unmarshal([]byte(raw), ¶ms) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return params 23 | } 24 | 25 | func TestGetMethodTypes(t *testing.T) { 26 | funcWithTypes := func(ctx context.Context, arg1 int, arg2 float32) error { 27 | return nil 28 | } 29 | methodTypes, err := getMethodTypes(funcWithTypes) 30 | require.NoError(t, err) 31 | require.Equal(t, 3, len(methodTypes.in)) 32 | require.Equal(t, 1, len(methodTypes.out)) 33 | 34 | funcWithoutArgs := func(ctx context.Context) error { 35 | return nil 36 | } 37 | methodTypes, err = getMethodTypes(funcWithoutArgs) 38 | require.NoError(t, err) 39 | 40 | funcWithouCtx := func(arg1 int, arg2 float32) error { 41 | return nil 42 | } 43 | methodTypes, err = getMethodTypes(funcWithouCtx) 44 | require.ErrorIs(t, err, ErrMustHaveContext) 45 | 46 | funcWithouError := func(ctx context.Context, arg1 int, arg2 float32) (int, float32) { 47 | return 0, 0 48 | } 49 | methodTypes, err = getMethodTypes(funcWithouError) 50 | require.ErrorIs(t, err, ErrMustReturnError) 51 | 52 | funcWithTooManyReturnValues := func(ctx context.Context, arg1 int, arg2 float32) (int, float32, error) { 53 | return 0, 0, nil 54 | } 55 | methodTypes, err = getMethodTypes(funcWithTooManyReturnValues) 56 | require.ErrorIs(t, err, ErrTooManyReturnValues) 57 | } 58 | 59 | type dummyStruct struct { 60 | Field int `json:"field"` 61 | } 62 | 63 | func TestExtractArgumentsFromJSON(t *testing.T) { 64 | funcWithTypes := func(context.Context, int, float32, []int, dummyStruct) error { 65 | return nil 66 | } 67 | methodTypes, err := getMethodTypes(funcWithTypes) 68 | require.NoError(t, err) 69 | 70 | jsonArgs := rawParams(`[1, 2.0, [2, 3, 5], {"field": 11}]`) 71 | args, err := extractArgumentsFromJSONparamsArray(methodTypes.in[1:], jsonArgs) 72 | require.NoError(t, err) 73 | require.Equal(t, 4, len(args)) 74 | require.Equal(t, int(1), args[0].Interface()) 75 | require.Equal(t, float32(2.0), args[1].Interface()) 76 | require.Equal(t, []int{2, 3, 5}, args[2].Interface()) 77 | require.Equal(t, dummyStruct{Field: 11}, args[3].Interface()) 78 | 79 | funcWithoutArgs := func(context.Context) error { 80 | return nil 81 | } 82 | methodTypes, err = getMethodTypes(funcWithoutArgs) 83 | require.NoError(t, err) 84 | jsonArgs = rawParams(`[]`) 85 | args, err = extractArgumentsFromJSONparamsArray(methodTypes.in[1:], jsonArgs) 86 | require.NoError(t, err) 87 | require.Equal(t, 0, len(args)) 88 | } 89 | 90 | func TestCall_old(t *testing.T) { 91 | var ( 92 | errorArg = 0 93 | errorOut = errors.New("function error") //nolint:goerr113 94 | ) 95 | funcWithTypes := func(ctx context.Context, arg int) (dummyStruct, error) { 96 | value := ctx.Value(ctxKey("key")).(string) //nolint:forcetypeassert 97 | require.Equal(t, "value", value) 98 | 99 | if arg == errorArg { 100 | return dummyStruct{}, errorOut 101 | } 102 | return dummyStruct{arg}, nil 103 | } 104 | methodTypes, err := getMethodTypes(funcWithTypes) 105 | require.NoError(t, err) 106 | 107 | ctx := context.WithValue(context.Background(), ctxKey("key"), "value") 108 | 109 | jsonArgs := rawParams(`[1]`) 110 | result, err := methodTypes.call(ctx, jsonArgs) 111 | require.NoError(t, err) 112 | require.Equal(t, dummyStruct{1}, result) 113 | 114 | jsonArgs = rawParams(fmt.Sprintf(`[%d]`, errorArg)) 115 | result, err = methodTypes.call(ctx, jsonArgs) 116 | require.ErrorIs(t, err, errorOut) 117 | require.Equal(t, dummyStruct{}, result) 118 | } 119 | 120 | func TestCall(t *testing.T) { 121 | // for testing error return 122 | var ( 123 | errorArg = 0 124 | errorOut = errors.New("function error") //nolint:goerr113 125 | ) 126 | functionWithTypes := func(ctx context.Context, arg int) (dummyStruct, error) { 127 | // test context 128 | value := ctx.Value(ctxKey("key")).(string) //nolint:forcetypeassert 129 | require.Equal(t, "value", value) 130 | 131 | if arg == errorArg { 132 | return dummyStruct{}, errorOut 133 | } 134 | return dummyStruct{arg}, nil 135 | } 136 | functionNoArgs := func(ctx context.Context) (dummyStruct, error) { 137 | // test context 138 | value := ctx.Value(ctxKey("key")).(string) //nolint:forcetypeassert 139 | require.Equal(t, "value", value) 140 | 141 | return dummyStruct{1}, nil 142 | } 143 | functionNoArgsError := func(ctx context.Context) (dummyStruct, error) { 144 | // test context 145 | value := ctx.Value(ctxKey("key")).(string) //nolint:forcetypeassert 146 | require.Equal(t, "value", value) 147 | 148 | return dummyStruct{}, errorOut 149 | } 150 | functionNoReturn := func(ctx context.Context, arg int) error { 151 | // test context 152 | value := ctx.Value(ctxKey("key")).(string) //nolint:forcetypeassert 153 | require.Equal(t, "value", value) 154 | return nil 155 | } 156 | functonNoReturnError := func(ctx context.Context, arg int) error { 157 | // test context 158 | value := ctx.Value(ctxKey("key")).(string) //nolint:forcetypeassert 159 | require.Equal(t, "value", value) 160 | 161 | return errorOut 162 | } 163 | 164 | testCases := map[string]struct { 165 | function interface{} 166 | args string 167 | expectedValue interface{} 168 | expectedError error 169 | }{ 170 | "functionWithTypes": { 171 | function: functionWithTypes, 172 | args: `[1]`, 173 | expectedValue: dummyStruct{1}, 174 | expectedError: nil, 175 | }, 176 | "functionWithTypesError": { 177 | function: functionWithTypes, 178 | args: fmt.Sprintf(`[%d]`, errorArg), 179 | expectedValue: dummyStruct{}, 180 | expectedError: errorOut, 181 | }, 182 | "functionNoArgs": { 183 | function: functionNoArgs, 184 | args: `[]`, 185 | expectedValue: dummyStruct{1}, 186 | expectedError: nil, 187 | }, 188 | "functionNoArgsError": { 189 | function: functionNoArgsError, 190 | args: `[]`, 191 | expectedValue: dummyStruct{}, 192 | expectedError: errorOut, 193 | }, 194 | "functionNoReturn": { 195 | function: functionNoReturn, 196 | args: `[1]`, 197 | expectedValue: nil, 198 | expectedError: nil, 199 | }, 200 | "functionNoReturnError": { 201 | function: functonNoReturnError, 202 | args: `[1]`, 203 | expectedValue: nil, 204 | expectedError: errorOut, 205 | }, 206 | } 207 | 208 | for testName, testCase := range testCases { 209 | t.Run(testName, func(t *testing.T) { 210 | methodTypes, err := getMethodTypes(testCase.function) 211 | require.NoError(t, err) 212 | 213 | ctx := context.WithValue(context.Background(), ctxKey("key"), "value") 214 | 215 | result, err := methodTypes.call(ctx, rawParams(testCase.args)) 216 | if testCase.expectedError == nil { 217 | require.NoError(t, err) 218 | } else { 219 | require.ErrorIs(t, err, testCase.expectedError) 220 | } 221 | require.Equal(t, testCase.expectedValue, result) 222 | }) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /rpctypes/types.go: -------------------------------------------------------------------------------- 1 | // Package rpctypes implement types commonly used in the Flashbots codebase for receiving and senging requests 2 | package rpctypes 3 | 4 | import ( 5 | "bytes" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "errors" 9 | "hash" 10 | "math/big" 11 | "sort" 12 | 13 | "github.com/goccy/go-json" 14 | 15 | "github.com/ethereum/go-ethereum/common" 16 | "github.com/ethereum/go-ethereum/common/hexutil" 17 | "github.com/ethereum/go-ethereum/core/types" 18 | "github.com/google/uuid" 19 | "golang.org/x/crypto/sha3" 20 | ) 21 | 22 | // Note on optional Signer field: 23 | // * when receiving from Flashbots or other builders this field should be set 24 | // * otherwise its set from the request signature by orderflow proxy 25 | // in this case it can be empty! @should we prohibit that? 26 | 27 | // eth_SendBundle 28 | 29 | const ( 30 | BundleTxLimit = 100 31 | MevBundleTxLimit = 50 32 | MevBundleMaxDepth = 1 33 | BundleVersionV1 = "v1" 34 | BundleVersionV2 = "v2" 35 | ) 36 | 37 | var ( 38 | ErrBundleNoTxs = errors.New("bundle with no txs") 39 | ErrBundleTooManyTxs = errors.New("too many txs in bundle") 40 | ErrMevBundleUnmatchedTx = errors.New("mev bundle with unmatched tx") 41 | ErrMevBundleTooDeep = errors.New("mev bundle too deep") 42 | ErrUnsupportedBundleVersion = errors.New("unsupported bundle version") 43 | ) 44 | 45 | type EthSendBundleArgs struct { 46 | Txs []hexutil.Bytes `json:"txs"` 47 | BlockNumber *hexutil.Uint64 `json:"blockNumber"` 48 | MinTimestamp *uint64 `json:"minTimestamp,omitempty"` 49 | MaxTimestamp *uint64 `json:"maxTimestamp,omitempty"` 50 | RevertingTxHashes []common.Hash `json:"revertingTxHashes,omitempty"` 51 | ReplacementUUID *string `json:"replacementUuid,omitempty"` 52 | Version *string `json:"version,omitempty"` 53 | 54 | ReplacementNonce *uint64 `json:"replacementNonce,omitempty"` 55 | SigningAddress *common.Address `json:"signingAddress,omitempty"` // may or may not be respected depending on the context 56 | RefundIdentity *common.Address `json:"refundIdentity,omitempty"` // metadata field to improve redistribution ux 57 | 58 | DroppingTxHashes []common.Hash `json:"droppingTxHashes,omitempty"` 59 | UUID *string `json:"uuid,omitempty"` 60 | RefundPercent *uint64 `json:"refundPercent,omitempty"` 61 | RefundRecipient *common.Address `json:"refundRecipient,omitempty"` 62 | RefundTxHashes []string `json:"refundTxHashes,omitempty"` 63 | } 64 | 65 | const ( 66 | RevertModeAllow = "allow" 67 | RevertModeDrop = "drop" 68 | RevertModeFail = "fail" 69 | ) 70 | 71 | type MevBundleInclusion struct { 72 | BlockNumber hexutil.Uint64 `json:"block"` 73 | MaxBlock hexutil.Uint64 `json:"maxBlock"` 74 | } 75 | 76 | type MevBundleBody struct { 77 | Hash *common.Hash `json:"hash,omitempty"` 78 | Tx *hexutil.Bytes `json:"tx,omitempty"` 79 | Bundle *MevSendBundleArgs `json:"bundle,omitempty"` 80 | CanRevert bool `json:"canRevert,omitempty"` 81 | RevertMode string `json:"revertMode,omitempty"` 82 | } 83 | 84 | type MevBundleValidity struct { 85 | Refund []RefundConstraint `json:"refund,omitempty"` 86 | RefundConfig []RefundConfig `json:"refundConfig,omitempty"` 87 | } 88 | 89 | type RefundConstraint struct { 90 | BodyIdx int `json:"bodyIdx"` 91 | Percent int `json:"percent"` 92 | } 93 | 94 | type RefundConfig struct { 95 | Address common.Address `json:"address"` 96 | Percent int `json:"percent"` 97 | } 98 | 99 | type MevBundleMetadata struct { 100 | // Signer should be set by infra that verifies user signatures and not user 101 | Signer *common.Address `json:"signer,omitempty"` 102 | ReplacementNonce *int `json:"replacementNonce,omitempty"` 103 | // Used for cancelling. When true the only thing we care about is signer,replacement_nonce and RawShareBundle::replacement_uuid 104 | Cancelled *bool `json:"cancelled,omitempty"` 105 | } 106 | 107 | type MevSendBundleArgs struct { 108 | Version string `json:"version"` 109 | ReplacementUUID string `json:"replacementUuid,omitempty"` 110 | Inclusion MevBundleInclusion `json:"inclusion"` 111 | // when empty its considered cancel 112 | Body []MevBundleBody `json:"body"` 113 | Validity MevBundleValidity `json:"validity"` 114 | Metadata *MevBundleMetadata `json:"metadata,omitempty"` 115 | 116 | // must be empty 117 | Privacy *json.RawMessage `json:"privacy,omitempty"` 118 | } 119 | 120 | // eth_sendRawTransaction 121 | 122 | type EthSendRawTransactionArgs hexutil.Bytes 123 | 124 | func (tx EthSendRawTransactionArgs) MarshalText() ([]byte, error) { 125 | return hexutil.Bytes(tx).MarshalText() 126 | } 127 | 128 | func (tx *EthSendRawTransactionArgs) UnmarshalJSON(input []byte) error { 129 | return (*hexutil.Bytes)(tx).UnmarshalJSON(input) 130 | } 131 | 132 | func (tx *EthSendRawTransactionArgs) UnmarshalText(input []byte) error { 133 | return (*hexutil.Bytes)(tx).UnmarshalText(input) 134 | } 135 | 136 | // eth_cancelBundle 137 | 138 | type EthCancelBundleArgs struct { 139 | ReplacementUUID string `json:"replacementUuid"` 140 | SigningAddress *common.Address `json:"signingAddress"` 141 | } 142 | 143 | // bid_subsidiseBlock 144 | 145 | type BidSubsisideBlockArgs uint64 146 | 147 | /// unique key 148 | /// unique key is used to deduplicate requests, its will give different results then bundle uuid 149 | 150 | func newHash() hash.Hash { 151 | return sha256.New() 152 | } 153 | 154 | func uuidFromHash(h hash.Hash) uuid.UUID { 155 | version := 5 156 | s := h.Sum(nil) 157 | var uuid uuid.UUID 158 | copy(uuid[:], s) 159 | uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) 160 | uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant 161 | return uuid 162 | } 163 | 164 | func (b *EthSendBundleArgs) UniqueKey() uuid.UUID { 165 | blockNumber := uint64(0) 166 | if b.BlockNumber != nil { 167 | blockNumber = uint64(*b.BlockNumber) 168 | } 169 | hash := newHash() 170 | _ = binary.Write(hash, binary.LittleEndian, blockNumber) 171 | for _, tx := range b.Txs { 172 | _, _ = hash.Write(tx) 173 | } 174 | if b.MinTimestamp != nil { 175 | _ = binary.Write(hash, binary.LittleEndian, b.MinTimestamp) 176 | } 177 | if b.MaxTimestamp != nil { 178 | _ = binary.Write(hash, binary.LittleEndian, b.MaxTimestamp) 179 | } 180 | sort.Slice(b.RevertingTxHashes, func(i, j int) bool { 181 | return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) <= 0 182 | }) 183 | for _, txHash := range b.RevertingTxHashes { 184 | _, _ = hash.Write(txHash.Bytes()) 185 | } 186 | if b.ReplacementUUID != nil { 187 | _, _ = hash.Write([]byte(*b.ReplacementUUID)) 188 | } 189 | if b.ReplacementNonce != nil { 190 | _ = binary.Write(hash, binary.LittleEndian, *b.ReplacementNonce) 191 | } 192 | 193 | sort.Slice(b.DroppingTxHashes, func(i, j int) bool { 194 | return bytes.Compare(b.DroppingTxHashes[i][:], b.DroppingTxHashes[j][:]) <= 0 195 | }) 196 | for _, txHash := range b.DroppingTxHashes { 197 | _, _ = hash.Write(txHash.Bytes()) 198 | } 199 | if b.RefundPercent != nil { 200 | _ = binary.Write(hash, binary.LittleEndian, *b.RefundPercent) 201 | } 202 | 203 | if b.RefundRecipient != nil { 204 | _, _ = hash.Write(b.RefundRecipient.Bytes()) 205 | } 206 | for _, txHash := range b.RefundTxHashes { 207 | _, _ = hash.Write([]byte(txHash)) 208 | } 209 | 210 | if b.SigningAddress != nil { 211 | _, _ = hash.Write(b.SigningAddress.Bytes()) 212 | } 213 | return uuidFromHash(hash) 214 | } 215 | 216 | func (b *EthSendBundleArgs) Validate() (common.Hash, uuid.UUID, error) { 217 | blockNumber := uint64(0) 218 | if b.BlockNumber != nil { 219 | blockNumber = uint64(*b.BlockNumber) 220 | } 221 | if len(b.Txs) > BundleTxLimit { 222 | return common.Hash{}, uuid.Nil, ErrBundleTooManyTxs 223 | } 224 | // first compute keccak hash over the txs 225 | hasher := sha3.NewLegacyKeccak256() 226 | for _, rawTx := range b.Txs { 227 | var tx types.Transaction 228 | if err := tx.UnmarshalBinary(rawTx); err != nil { 229 | return common.Hash{}, uuid.Nil, err 230 | } 231 | hasher.Write(tx.Hash().Bytes()) 232 | } 233 | hashBytes := hasher.Sum(nil) 234 | 235 | if b.Version == nil || *b.Version == BundleVersionV1 { 236 | // then compute the uuid 237 | var buf []byte 238 | buf = binary.AppendVarint(buf, int64(blockNumber)) 239 | buf = append(buf, hashBytes...) 240 | sort.Slice(b.RevertingTxHashes, func(i, j int) bool { 241 | return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) <= 0 242 | }) 243 | for _, txHash := range b.RevertingTxHashes { 244 | buf = append(buf, txHash[:]...) 245 | } 246 | return common.BytesToHash(hashBytes), 247 | uuid.NewHash(sha256.New(), uuid.Nil, buf, 5), 248 | nil 249 | } 250 | 251 | if *b.Version == BundleVersionV2 { 252 | // blockNumber, default 0 253 | blockNumber := uint64(0) 254 | if b.BlockNumber != nil { 255 | blockNumber = uint64(*b.BlockNumber) 256 | } 257 | 258 | // minTimestamp, default 0 259 | minTimestamp := uint64(0) 260 | if b.MinTimestamp != nil { 261 | minTimestamp = *b.MinTimestamp 262 | } 263 | 264 | // maxTimestamp, default ^uint64(0) (i.e. 0xFFFFFFFFFFFFFFFF in Rust) 265 | maxTimestamp := ^uint64(0) 266 | if b.MaxTimestamp != nil { 267 | maxTimestamp = *b.MaxTimestamp 268 | } 269 | 270 | // Build up our buffer using variable-length encoding of the block 271 | // number, minTimestamp, maxTimestamp, #revertingTxHashes, #droppingTxHashes. 272 | var buf []byte 273 | buf = binary.AppendUvarint(buf, blockNumber) 274 | buf = binary.AppendUvarint(buf, minTimestamp) 275 | buf = binary.AppendUvarint(buf, maxTimestamp) 276 | buf = binary.AppendUvarint(buf, uint64(len(b.RevertingTxHashes))) 277 | buf = binary.AppendUvarint(buf, uint64(len(b.DroppingTxHashes))) 278 | 279 | // Append the main txs keccak hash (already computed in hashBytes). 280 | buf = append(buf, hashBytes...) 281 | 282 | // Sort revertingTxHashes and append them. 283 | sort.Slice(b.RevertingTxHashes, func(i, j int) bool { 284 | return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) < 0 285 | }) 286 | for _, h := range b.RevertingTxHashes { 287 | buf = append(buf, h[:]...) 288 | } 289 | 290 | // Sort droppingTxHashes and append them. 291 | sort.Slice(b.DroppingTxHashes, func(i, j int) bool { 292 | return bytes.Compare(b.DroppingTxHashes[i][:], b.DroppingTxHashes[j][:]) < 0 293 | }) 294 | for _, h := range b.DroppingTxHashes { 295 | buf = append(buf, h[:]...) 296 | } 297 | 298 | // If a "refund" is present (analogous to the Rust code), we push: 299 | // refundPercent (1 byte) 300 | // refundRecipient (20 bytes, if an Ethereum address) 301 | // #refundTxHashes (varint) 302 | // each refundTxHash (32 bytes) 303 | // NOTE: The Rust code uses a single byte for `refund.percent`, 304 | // so we do the same here 305 | if b.RefundPercent != nil && *b.RefundPercent != 0 { 306 | if len(b.Txs) == 0 { 307 | // Bundle with not txs can't be refund-recipient 308 | return common.Hash{}, uuid.Nil, ErrBundleNoTxs 309 | } 310 | 311 | // We only keep the low 8 bits of RefundPercent (mimicking Rust's `buff.push(u8)`). 312 | buf = append(buf, byte(*b.RefundPercent)) 313 | 314 | refundRecipient := b.RefundRecipient 315 | if refundRecipient == nil { 316 | var tx types.Transaction 317 | if err := tx.UnmarshalBinary(b.Txs[0]); err != nil { 318 | return common.Hash{}, uuid.Nil, err 319 | } 320 | from, err := types.Sender(types.LatestSignerForChainID(big.NewInt(1)), &tx) 321 | if err != nil { 322 | return common.Hash{}, uuid.Nil, err 323 | } 324 | refundRecipient = &from 325 | } 326 | bts := [20]byte(*refundRecipient) 327 | // RefundRecipient is a common.Address, which is 20 bytes in geth. 328 | buf = append(buf, bts[:]...) 329 | 330 | var refundTxHashes []common.Hash 331 | for _, rth := range b.RefundTxHashes { 332 | // decode from hex 333 | refundTxHashes = append(refundTxHashes, common.HexToHash(rth)) 334 | } 335 | 336 | if len(refundTxHashes) == 0 { 337 | var lastTx types.Transaction 338 | if err := lastTx.UnmarshalBinary(b.Txs[len(b.Txs)-1]); err != nil { 339 | return common.Hash{}, uuid.Nil, err 340 | } 341 | refundTxHashes = []common.Hash{lastTx.Hash()} 342 | } 343 | 344 | // #refundTxHashes 345 | buf = binary.AppendUvarint(buf, uint64(len(refundTxHashes))) 346 | 347 | sort.Slice(refundTxHashes, func(i, j int) bool { 348 | return bytes.Compare(refundTxHashes[i][:], refundTxHashes[j][:]) < 0 349 | }) 350 | for _, h := range refundTxHashes { 351 | buf = append(buf, h[:]...) 352 | } 353 | } 354 | 355 | // Now produce a UUID from `buf` using SHA-256 in the same way the Rust code calls 356 | // `Self::uuid_from_buffer(buff)` (which is effectively a UUIDv5 but with SHA-256). 357 | finalUUID := uuid.NewHash(sha256.New(), uuid.Nil, buf, 5) 358 | 359 | // Return the main txs keccak hash as well as the computed UUID 360 | return common.BytesToHash(hashBytes), finalUUID, nil 361 | } 362 | 363 | return common.Hash{}, uuid.Nil, ErrUnsupportedBundleVersion 364 | 365 | } 366 | 367 | func (b *MevSendBundleArgs) UniqueKey() uuid.UUID { 368 | hash := newHash() 369 | uniqueKeyMevSendBundle(b, hash) 370 | return uuidFromHash(hash) 371 | } 372 | 373 | func uniqueKeyMevSendBundle(b *MevSendBundleArgs, hash hash.Hash) { 374 | hash.Write([]byte(b.ReplacementUUID)) 375 | _ = binary.Write(hash, binary.LittleEndian, b.Inclusion.BlockNumber) 376 | _ = binary.Write(hash, binary.LittleEndian, b.Inclusion.MaxBlock) 377 | for _, body := range b.Body { 378 | if body.Bundle != nil { 379 | uniqueKeyMevSendBundle(body.Bundle, hash) 380 | } else if body.Tx != nil { 381 | hash.Write(*body.Tx) 382 | } 383 | // body.Hash should not occur at this point 384 | if body.CanRevert { 385 | hash.Write([]byte{1}) 386 | } else { 387 | hash.Write([]byte{0}) 388 | } 389 | hash.Write([]byte(body.RevertMode)) 390 | } 391 | _, _ = hash.Write(b.Metadata.Signer.Bytes()) 392 | } 393 | 394 | func (b *MevSendBundleArgs) Validate() (common.Hash, error) { 395 | // only cancell call can be without txs 396 | // cancell call must have ReplacementUUID set 397 | if len(b.Body) == 0 && b.ReplacementUUID == "" { 398 | return common.Hash{}, ErrBundleNoTxs 399 | } 400 | return hashMevSendBundle(0, b) 401 | } 402 | 403 | func hashMevSendBundle(level int, b *MevSendBundleArgs) (common.Hash, error) { 404 | if level > MevBundleMaxDepth { 405 | return common.Hash{}, ErrMevBundleTooDeep 406 | } 407 | hasher := sha3.NewLegacyKeccak256() 408 | for _, body := range b.Body { 409 | if body.Hash != nil { 410 | return common.Hash{}, ErrMevBundleUnmatchedTx 411 | } else if body.Bundle != nil { 412 | innerHash, err := hashMevSendBundle(level+1, body.Bundle) 413 | if err != nil { 414 | return common.Hash{}, err 415 | } 416 | hasher.Write(innerHash.Bytes()) 417 | } else if body.Tx != nil { 418 | tx := new(types.Transaction) 419 | if err := tx.UnmarshalBinary(*body.Tx); err != nil { 420 | return common.Hash{}, err 421 | } 422 | hasher.Write(tx.Hash().Bytes()) 423 | } 424 | } 425 | return common.BytesToHash(hasher.Sum(nil)), nil 426 | } 427 | 428 | func (tx *EthSendRawTransactionArgs) UniqueKey() uuid.UUID { 429 | hash := newHash() 430 | _, _ = hash.Write(*tx) 431 | return uuidFromHash(hash) 432 | } 433 | 434 | func (b *EthCancelBundleArgs) UniqueKey() uuid.UUID { 435 | hash := newHash() 436 | _, _ = hash.Write([]byte(b.ReplacementUUID)) 437 | _, _ = hash.Write(b.SigningAddress.Bytes()) 438 | return uuidFromHash(hash) 439 | } 440 | 441 | func (b *BidSubsisideBlockArgs) UniqueKey() uuid.UUID { 442 | hash := newHash() 443 | _ = binary.Write(hash, binary.LittleEndian, uint64(*b)) 444 | return uuidFromHash(hash) 445 | } 446 | -------------------------------------------------------------------------------- /rpctypes/types_test.go: -------------------------------------------------------------------------------- 1 | package rpctypes 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/goccy/go-json" 8 | 9 | "github.com/ethereum/go-ethereum/common/hexutil" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestEthSendBundleArgsValidate(t *testing.T) { 14 | // from https://github.com/flashbots/rbuilder/blob/develop/crates/rbuilder/src/primitives/serialize.rs#L607 15 | inputs := []struct { 16 | Payload json.RawMessage 17 | ExpectedHash string 18 | ExpectedUUID string 19 | ExpectedUniqueKey string 20 | }{ 21 | { 22 | Payload: []byte(`{ 23 | "blockNumber": "0x1136F1F", 24 | "txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"], 25 | "revertingTxHashes": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] 26 | }`), 27 | ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc", 28 | ExpectedUUID: "d9a3ae52-79a2-5ce9-a687-e2aa4183d5c6", 29 | }, 30 | { 31 | Payload: []byte(`{ 32 | "blockNumber": "0x1136F1F", 33 | "txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"], 34 | "revertingTxHashes": ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"] 35 | }`), 36 | ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc", 37 | ExpectedUUID: "d9a3ae52-79a2-5ce9-a687-e2aa4183d5c6", 38 | }, 39 | { 40 | Payload: []byte(`{ 41 | "blockNumber": "0xA136F1F", 42 | "txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"], 43 | "revertingTxHashes": [] 44 | }`), 45 | ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc", 46 | ExpectedUUID: "5d5bf52c-ac3f-57eb-a3e9-fc01b18ca516", 47 | }, 48 | { 49 | Payload: []byte(`{ 50 | "txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"], 51 | "revertingTxHashes": [] 52 | }`), 53 | ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc", 54 | ExpectedUUID: "e9ced844-16d5-5884-8507-db9338950c5c", 55 | }, 56 | { 57 | Payload: []byte(`{ 58 | "blockNumber": "0x0", 59 | "txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"], 60 | "revertingTxHashes": [] 61 | }`), 62 | ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc", 63 | ExpectedUUID: "e9ced844-16d5-5884-8507-db9338950c5c", 64 | }, 65 | { 66 | Payload: []byte(`{ 67 | "blockNumber": null, 68 | "txs": ["0x02f9037b018203cd8405f5e1008503692da370830388ba943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad8780e531581b77c4b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064f390d300000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000080e531581b77c400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000b5ea574dd8f2b735424dfc8c4e16760fc44a931b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c001a0a9ea84ad107d335afd5e5d2ddcc576f183be37386a9ac6c9d4469d0329c22e87a06a51ea5a0809f43bf72d0156f1db956da3a9f3da24b590b7eed01128ff84a2c1"], 69 | "revertingTxHashes": [] 70 | }`), 71 | ExpectedHash: "0xcf3c567aede099e5455207ed81c4884f72a4c0c24ddca331163a335525cd22cc", 72 | ExpectedUUID: "e9ced844-16d5-5884-8507-db9338950c5c", 73 | }, 74 | // different empty bundles have the same uuid, they must have different unique key 75 | { 76 | Payload: []byte(`{ 77 | "replacementUuid": "e9ced844-16d5-5884-8507-db9338950c5c" 78 | }`), 79 | ExpectedHash: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", 80 | ExpectedUUID: "35718fe4-5d24-51c8-93bf-9c961d7c3ea3", 81 | ExpectedUniqueKey: "1655edd0-29a6-5372-a19b-1ddedda14b20", 82 | }, 83 | { 84 | Payload: []byte(`{ 85 | "replacementUuid": "35718fe4-5d24-51c8-93bf-9c961d7c3ea3" 86 | }`), 87 | ExpectedHash: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", 88 | ExpectedUUID: "35718fe4-5d24-51c8-93bf-9c961d7c3ea3", 89 | ExpectedUniqueKey: "3c718cb9-3f6c-5dc0-9d99-264dafc0b4e9", 90 | }, 91 | { 92 | Payload: []byte(` { 93 | "version": "v2", 94 | "txs": [ 95 | "0x02f86b83aa36a780800982520894f24a01ae29dec4629dfb4170647c4ed4efc392cd861ca62a4c95b880c080a07d37bb5a4da153a6fbe24cf1f346ef35748003d1d0fc59cf6c17fb22d49e42cea02c231ac233220b494b1ad501c440c8b1a34535cdb8ca633992d6f35b14428672" 96 | ], 97 | "blockNumber": "0x0", 98 | "minTimestamp": 123, 99 | "maxTimestamp": 1234, 100 | "revertingTxHashes": ["0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"], 101 | "droppingTxHashes": ["0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], 102 | "refundPercent": 1, 103 | "refundRecipient": "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5", 104 | "refundTxHashes": ["0x75662ab9cb6d1be7334723db5587435616352c7e581a52867959ac24006ac1fe"] 105 | }`), 106 | ExpectedHash: "0xee3996920364173b0990f92cf6fbeb8a4ab832fe5549c1b728ac44aee0160f02", 107 | ExpectedUUID: "e2bdb8cd-9473-5a1b-b425-57fa7ecfe2c1", 108 | ExpectedUniqueKey: "a54c1e8f-936f-5868-bded-f5138c60b34a", 109 | }, 110 | { 111 | Payload: []byte(` { 112 | "version": "v2", 113 | "txs": [ 114 | "0x02f90408018303f1d4808483ab318e8304485c94a69babef1ca67a37ffaf7a485dfff3382056e78c8302be00b9014478e111f60000000000000000000000007f0f35bbf44c8343d14260372c469b331491567b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4f4ff52950000000000000000000000000000000000000000000000000be75df44ebec5390000000000000000000000000000000000000000000036404c073ad050000000000000000000000000000000000000000000003e91fd871e8a6021ca93d911920000000000000000000000000000000000000000000000000000e91615b961030000000000000000000000000000000000000000000000000000000067eaa0b7ff8000000000000000000000000000000000000000000000000000000001229300000000000000000000000000000000000000000000000000000000f90253f9018394919fa96e88d67499339577fa202345436bcdaf79f9016ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513782a000000000000000000000000000000000000000000000000000000000000000a1a0bfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513785a00000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000a0a00000000000000000000000000000000000000000000000000000000000000002a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513783a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513784a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004f85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a060802b93a9ac49b8c74d6ade12cf6235f4ac8c52c84fd39da757d0b2d720d76fa075245230289a9f0bf73a6c59aef6651b98b3833a62a3c0bd9ab6b0dec8ed4d8fd6947f0f35bbf44c8343d14260372c469b331491567bc0f85994d533a949740bb3306d119cc777fa900ba034cd52f842a07a7ff188ddb962db42160fb3fb573f4af0ebe1a1d6b701f1f1464b5ea43f7638a03d4653d86fe510221a71cfd2b1168b2e9af3e71339c63be5f905dabce97ee61f01a0c9d68ec80949077b6c28d45a6bf92727bc49d705d201bff8c62956201f5d3a81a036b7b953d7385d8fab8834722b7c66eea4a02a66434fc4f38ebfe8f5218a87b0" 115 | ], 116 | "blockNumber": "0x0", 117 | "minTimestamp": 123, 118 | "maxTimestamp": 1234, 119 | "refundPercent": 20, 120 | "refundRecipient": "0xFF82BF5238637B7E5E345888BaB9cd99F5Ebe331", 121 | "refundTxHashes": ["0xffd9f02004350c16b312fd14ccc828f587c3c49ad3e9293391a398cc98c1a373"] 122 | }`), 123 | ExpectedHash: "0x90551b655a8a5b424064e802c0ec2daae864d8b786a788c2c6f9d7902feb42d2", 124 | ExpectedUUID: "e785c7c0-8bfa-508e-9c3f-cb24f1638de3", 125 | ExpectedUniqueKey: "fb7bff94-6f0d-5030-ab69-33adf953b742", 126 | }, 127 | { 128 | Payload: []byte(` { 129 | "version": "v2", 130 | "txs": [ 131 | "0x02f90408018303f1d4808483ab318e8304485c94a69babef1ca67a37ffaf7a485dfff3382056e78c8302be00b9014478e111f60000000000000000000000007f0f35bbf44c8343d14260372c469b331491567b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4f4ff52950000000000000000000000000000000000000000000000000be75df44ebec5390000000000000000000000000000000000000000000036404c073ad050000000000000000000000000000000000000000000003e91fd871e8a6021ca93d911920000000000000000000000000000000000000000000000000000e91615b961030000000000000000000000000000000000000000000000000000000067eaa0b7ff8000000000000000000000000000000000000000000000000000000001229300000000000000000000000000000000000000000000000000000000f90253f9018394919fa96e88d67499339577fa202345436bcdaf79f9016ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513782a000000000000000000000000000000000000000000000000000000000000000a1a0bfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513785a00000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000a0a00000000000000000000000000000000000000000000000000000000000000002a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513783a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513784a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004f85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a060802b93a9ac49b8c74d6ade12cf6235f4ac8c52c84fd39da757d0b2d720d76fa075245230289a9f0bf73a6c59aef6651b98b3833a62a3c0bd9ab6b0dec8ed4d8fd6947f0f35bbf44c8343d14260372c469b331491567bc0f85994d533a949740bb3306d119cc777fa900ba034cd52f842a07a7ff188ddb962db42160fb3fb573f4af0ebe1a1d6b701f1f1464b5ea43f7638a03d4653d86fe510221a71cfd2b1168b2e9af3e71339c63be5f905dabce97ee61f01a0c9d68ec80949077b6c28d45a6bf92727bc49d705d201bff8c62956201f5d3a81a036b7b953d7385d8fab8834722b7c66eea4a02a66434fc4f38ebfe8f5218a87b0" 132 | ], 133 | "blockNumber": "0x0", 134 | "minTimestamp": 123, 135 | "maxTimestamp": 1234, 136 | "refundPercent": 20, 137 | "refundRecipient": "0xFF82BF5238637B7E5E345888BaB9cd99F5Ebe331" 138 | }`), 139 | ExpectedHash: "0x90551b655a8a5b424064e802c0ec2daae864d8b786a788c2c6f9d7902feb42d2", 140 | ExpectedUUID: "e785c7c0-8bfa-508e-9c3f-cb24f1638de3", 141 | ExpectedUniqueKey: "", 142 | }, 143 | { 144 | Payload: []byte(` { 145 | "version": "v2", 146 | "txs": [ 147 | "0x02f90408018303f1d4808483ab318e8304485c94a69babef1ca67a37ffaf7a485dfff3382056e78c8302be00b9014478e111f60000000000000000000000007f0f35bbf44c8343d14260372c469b331491567b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4f4ff52950000000000000000000000000000000000000000000000000be75df44ebec5390000000000000000000000000000000000000000000036404c073ad050000000000000000000000000000000000000000000003e91fd871e8a6021ca93d911920000000000000000000000000000000000000000000000000000e91615b961030000000000000000000000000000000000000000000000000000000067eaa0b7ff8000000000000000000000000000000000000000000000000000000001229300000000000000000000000000000000000000000000000000000000f90253f9018394919fa96e88d67499339577fa202345436bcdaf79f9016ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513782a000000000000000000000000000000000000000000000000000000000000000a1a0bfd358e93f18da3ed276c3afdbdba00b8f0b6008a03476a6a86bd6320ee6938ba0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513785a00000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000a0a00000000000000000000000000000000000000000000000000000000000000002a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513783a0bf1c200cc6dee22da7e010c51ff8e5210da52f1c78d2171dbb5d4f739e513784a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004f85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a060802b93a9ac49b8c74d6ade12cf6235f4ac8c52c84fd39da757d0b2d720d76fa075245230289a9f0bf73a6c59aef6651b98b3833a62a3c0bd9ab6b0dec8ed4d8fd6947f0f35bbf44c8343d14260372c469b331491567bc0f85994d533a949740bb3306d119cc777fa900ba034cd52f842a07a7ff188ddb962db42160fb3fb573f4af0ebe1a1d6b701f1f1464b5ea43f7638a03d4653d86fe510221a71cfd2b1168b2e9af3e71339c63be5f905dabce97ee61f01a0c9d68ec80949077b6c28d45a6bf92727bc49d705d201bff8c62956201f5d3a81a036b7b953d7385d8fab8834722b7c66eea4a02a66434fc4f38ebfe8f5218a87b0" 148 | ], 149 | "blockNumber": "0x0", 150 | "minTimestamp": 123, 151 | "maxTimestamp": 1234, 152 | "refundPercent": 20 153 | }`), 154 | ExpectedHash: "0x90551b655a8a5b424064e802c0ec2daae864d8b786a788c2c6f9d7902feb42d2", 155 | ExpectedUUID: "e785c7c0-8bfa-508e-9c3f-cb24f1638de3", 156 | ExpectedUniqueKey: "", 157 | }, 158 | { 159 | Payload: []byte(`{ 160 | "version": "v2", 161 | "txs": [ 162 | "0x02f86b83aa36a780800982520894f24a01ae29dec4629dfb4170647c4ed4efc392cd861ca62a4c95b880c080a07d37bb5a4da153a6fbe24cf1f346ef35748003d1d0fc59cf6c17fb22d49e42cea02c231ac233220b494b1ad501c440c8b1a34535cdb8ca633992d6f35b14428672" 163 | ], 164 | "blockNumber": "0x0", 165 | "revertingTxHashes": [] 166 | }`), 167 | ExpectedHash: "0xee3996920364173b0990f92cf6fbeb8a4ab832fe5549c1b728ac44aee0160f02", 168 | ExpectedUUID: "22dc6bf0-9a12-5a76-9bbd-98ab77423415", 169 | ExpectedUniqueKey: "", 170 | }, 171 | } 172 | 173 | for i, input := range inputs { 174 | t.Run(fmt.Sprintf("inout-%d", i), func(t *testing.T) { 175 | bundle := &EthSendBundleArgs{} 176 | require.NoError(t, json.Unmarshal(input.Payload, bundle)) 177 | hash, uuid, err := bundle.Validate() 178 | uniqueKey := bundle.UniqueKey() 179 | require.NoError(t, err) 180 | require.Equal(t, input.ExpectedHash, hash.Hex()) 181 | require.Equal(t, input.ExpectedUUID, uuid.String()) 182 | if input.ExpectedUniqueKey != "" { 183 | require.Equal(t, input.ExpectedUniqueKey, uniqueKey.String()) 184 | } 185 | }) 186 | } 187 | } 188 | 189 | func TestMevSendBundleArgsValidate(t *testing.T) { 190 | // From: https://github.com/flashbots/rbuilder/blob/91f7a2c22eaeaf6c44e28c0bda98a2a0d566a6cb/crates/rbuilder/src/primitives/serialize.rs#L700 191 | // NOTE: I had to dump the hash in a debugger to get the expected hash since the test above uses a computed hash 192 | raw := []byte(`{ 193 | "version": "v0.1", 194 | "inclusion": { 195 | "block": "0x1" 196 | }, 197 | "body": [ 198 | { 199 | "bundle": { 200 | "version": "v0.1", 201 | "inclusion": { 202 | "block": "0x1" 203 | }, 204 | "body": [ 205 | { 206 | "tx": "0x02f86b0180843b9aca00852ecc889a0082520894c87037874aed04e51c29f582394217a0a2b89d808080c080a0a463985c616dd8ee17d7ef9112af4e6e06a27b071525b42182fe7b0b5c8b4925a00af5ca177ffef2ff28449292505d41be578bebb77110dfc09361d2fb56998260", 207 | "canRevert": true 208 | }, 209 | { 210 | "tx": "0x02f8730180843b9aca00852ecc889a008288b894c10000000000000000000000000000000000000088016345785d8a000080c001a07c8890151fed9a826f241d5a37c84062ebc55ca7f5caef4683dcda6ac99dbffba069108de72e4051a764f69c51a6b718afeff4299107963a5d84d5207b2d6932a4" 211 | } 212 | ], 213 | "validity": { 214 | "refund": [ 215 | { 216 | "bodyIdx": 0, 217 | "percent": 90 218 | } 219 | ], 220 | "refundConfig": [ 221 | { 222 | "address": "0x3e7dfb3e26a16e3dbf6dfeeff8a5ae7a04f73aad", 223 | "percent": 100 224 | } 225 | ] 226 | } 227 | } 228 | }, 229 | { 230 | "tx": "0x02f8730101843b9aca00852ecc889a008288b894c10000000000000000000000000000000000000088016345785d8a000080c001a0650c394d77981e46be3d8cf766ecc435ec3706375baed06eb9bef21f9da2828da064965fdf88b91575cd74f20301649c9d011b234cefb6c1761cc5dd579e4750b1" 231 | } 232 | ], 233 | "validity": { 234 | "refund": [ 235 | { 236 | "bodyIdx": 0, 237 | "percent": 80 238 | } 239 | ] 240 | }, 241 | "metadata": { 242 | "signer": "0x4696595f68034b47BbEc82dB62852B49a8EE7105" 243 | } 244 | }`) 245 | 246 | bundle := &MevSendBundleArgs{} 247 | require.NoError(t, json.Unmarshal(raw, bundle)) 248 | hash, err := bundle.Validate() 249 | require.NoError(t, err) 250 | require.Equal(t, "0x3b1994ad123d089f978074cfa197811b644e43b2b44b4c4710614f3a30ee0744", hash.Hex()) 251 | } 252 | 253 | func TestEthsendRawTransactionArgsJSON(t *testing.T) { 254 | data := hexutil.MustDecode("0x1234") 255 | 256 | rawTransaction := EthSendRawTransactionArgs(data) 257 | 258 | out, err := json.Marshal(rawTransaction) 259 | require.NoError(t, err) 260 | 261 | require.Equal(t, `"0x1234"`, string(out)) 262 | 263 | var roundtripRawTransaction EthSendRawTransactionArgs 264 | err = json.Unmarshal(out, &roundtripRawTransaction) 265 | require.NoError(t, err) 266 | require.Equal(t, rawTransaction, roundtripRawTransaction) 267 | } 268 | -------------------------------------------------------------------------------- /signature/signature.go: -------------------------------------------------------------------------------- 1 | // Package signature provides functionality for interacting with the X-Flashbots-Signature header. 2 | package signature 3 | 4 | import ( 5 | "crypto/ecdsa" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/ethereum/go-ethereum/accounts" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/common/hexutil" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | ) 15 | 16 | // HTTPHeader is the name of the X-Flashbots-Signature header. 17 | const HTTPHeader = "X-Flashbots-Signature" 18 | 19 | var ( 20 | ErrNoSignature = errors.New("no signature provided") 21 | ErrInvalidSignature = errors.New("invalid signature provided") 22 | ) 23 | 24 | // Verify takes a X-Flashbots-Signature header and a body and verifies that the signature is valid for the body. 25 | // It returns the signing address if the signature is valid or an error if the signature is invalid. 26 | func Verify(header string, body []byte) (common.Address, error) { 27 | if header == "" { 28 | return common.Address{}, ErrNoSignature 29 | } 30 | 31 | parsedSignerStr, parsedSignatureStr, found := strings.Cut(header, ":") 32 | if !found { 33 | return common.Address{}, fmt.Errorf("%w: missing separator", ErrInvalidSignature) 34 | } 35 | 36 | parsedSignature, err := hexutil.Decode(parsedSignatureStr) 37 | if err != nil || len(parsedSignature) == 0 { 38 | return common.Address{}, fmt.Errorf("%w: %w", ErrInvalidSignature, err) 39 | } 40 | 41 | if parsedSignature[len(parsedSignature)-1] >= 27 { 42 | parsedSignature[len(parsedSignature)-1] -= 27 43 | } 44 | if parsedSignature[len(parsedSignature)-1] > 1 { 45 | return common.Address{}, fmt.Errorf("%w: invalid recovery id", ErrInvalidSignature) 46 | } 47 | 48 | hashedBody := crypto.Keccak256Hash(body).Hex() 49 | messageHash := accounts.TextHash([]byte(hashedBody)) 50 | recoveredPublicKeyBytes, err := crypto.Ecrecover(messageHash, parsedSignature) 51 | if err != nil { 52 | return common.Address{}, fmt.Errorf("%w: %w", ErrInvalidSignature, err) 53 | } 54 | 55 | recoveredPublicKey, err := crypto.UnmarshalPubkey(recoveredPublicKeyBytes) 56 | if err != nil { 57 | return common.Address{}, fmt.Errorf("%w: %w", ErrInvalidSignature, err) 58 | } 59 | recoveredSigner := crypto.PubkeyToAddress(*recoveredPublicKey) 60 | 61 | // case-insensitive equality check 62 | parsedSigner := common.HexToAddress(parsedSignerStr) 63 | if recoveredSigner.Cmp(parsedSigner) != 0 { 64 | return common.Address{}, fmt.Errorf("%w: signing address mismatch", ErrInvalidSignature) 65 | } 66 | 67 | signatureNoRecoverID := parsedSignature[:len(parsedSignature)-1] // remove recovery id 68 | if !crypto.VerifySignature(recoveredPublicKeyBytes, messageHash, signatureNoRecoverID) { 69 | return common.Address{}, fmt.Errorf("%w: %w", ErrInvalidSignature, err) 70 | } 71 | 72 | return recoveredSigner, nil 73 | } 74 | 75 | type Signer struct { 76 | privateKey *ecdsa.PrivateKey 77 | address common.Address 78 | hexAddress string 79 | } 80 | 81 | func NewSigner(privateKey *ecdsa.PrivateKey) Signer { 82 | address := crypto.PubkeyToAddress(privateKey.PublicKey) 83 | return Signer{ 84 | privateKey: privateKey, 85 | hexAddress: address.Hex(), 86 | address: address, 87 | } 88 | } 89 | 90 | // NewSignerFromHexPrivateKey creates new signer from 0x-prefixed hex-encoded private key 91 | func NewSignerFromHexPrivateKey(hexPrivateKey string) (*Signer, error) { 92 | privateKeyBytes, err := hexutil.Decode(hexPrivateKey) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | privateKey, err := crypto.ToECDSA(privateKeyBytes) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | signer := NewSigner(privateKey) 103 | return &signer, nil 104 | } 105 | 106 | func NewRandomSigner() (*Signer, error) { 107 | privateKey, err := crypto.GenerateKey() 108 | if err != nil { 109 | return nil, err 110 | } 111 | signer := NewSigner(privateKey) 112 | return &signer, nil 113 | } 114 | 115 | func (s *Signer) Address() common.Address { 116 | return s.address 117 | } 118 | 119 | // Create takes a body and a private key and returns a X-Flashbots-Signature header value. 120 | // The header value can be included in a HTTP request to sign the body. 121 | func (s *Signer) Create(body []byte) (string, error) { 122 | signature, err := crypto.Sign( 123 | accounts.TextHash([]byte(hexutil.Encode(crypto.Keccak256(body)))), 124 | s.privateKey, 125 | ) 126 | if err != nil { 127 | return "", err 128 | } 129 | // To maintain compatibility with the EVM `ecrecover` precompile, the recovery ID in the last 130 | // byte is encoded as v = 27/28 instead of 0/1. This also ensures we generate the same signatures as other 131 | // popular libraries like ethers.js, and tooling like `cast wallet sign` and MetaMask. 132 | // 133 | // See: 134 | // - Yellow Paper, Appendix E & F. https://ethereum.github.io/yellowpaper/paper.pdf 135 | // - https://www.evm.codes/precompiled (ecrecover is the 1st precompile at 0x01) 136 | // 137 | if signature[len(signature)-1] < 27 { 138 | signature[len(signature)-1] += 27 139 | } 140 | 141 | header := fmt.Sprintf("%s:%s", s.hexAddress, hexutil.Encode(signature)) 142 | return header, nil 143 | } 144 | -------------------------------------------------------------------------------- /signature/signature_test.go: -------------------------------------------------------------------------------- 1 | package signature_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ethereum/go-ethereum/accounts" 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/common/hexutil" 10 | "github.com/ethereum/go-ethereum/crypto" 11 | "github.com/flashbots/go-utils/signature" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | // TestSignatureVerify tests the signature verification function. 16 | func TestSignatureVerify(t *testing.T) { 17 | // For most of these test cases, we first need to generate a signature 18 | privateKey, err := crypto.GenerateKey() 19 | require.NoError(t, err) 20 | 21 | signerAddress := crypto.PubkeyToAddress(privateKey.PublicKey) 22 | body := fmt.Sprintf( 23 | `{"jsonrpc":"2.0","method":"eth_getTransactionCount","params":["%s","pending"],"id":1}`, 24 | signerAddress, 25 | ) 26 | 27 | sig, err := crypto.Sign( 28 | accounts.TextHash([]byte(hexutil.Encode(crypto.Keccak256([]byte(body))))), 29 | privateKey, 30 | ) 31 | require.NoError(t, err) 32 | 33 | header := fmt.Sprintf("%s:%s", signerAddress, hexutil.Encode(sig)) 34 | 35 | t.Run("header is empty", func(t *testing.T) { 36 | _, err := signature.Verify("", []byte{}) 37 | require.ErrorIs(t, err, signature.ErrNoSignature) 38 | }) 39 | 40 | t.Run("header is valid", func(t *testing.T) { 41 | verifiedAddress, err := signature.Verify(header, []byte(body)) 42 | require.NoError(t, err) 43 | require.Equal(t, signerAddress, verifiedAddress) 44 | }) 45 | 46 | t.Run("header is invalid", func(t *testing.T) { 47 | _, err := signature.Verify("invalid", []byte(body)) 48 | require.ErrorIs(t, err, signature.ErrInvalidSignature) 49 | }) 50 | 51 | t.Run("header has extra bytes", func(t *testing.T) { 52 | _, err := signature.Verify(header+"deadbeef", []byte(body)) 53 | require.ErrorIs(t, err, signature.ErrInvalidSignature) 54 | }) 55 | 56 | t.Run("header has missing bytes", func(t *testing.T) { 57 | _, err := signature.Verify(header[:len(header)-8], []byte(body)) 58 | require.ErrorIs(t, err, signature.ErrInvalidSignature) 59 | }) 60 | 61 | t.Run("body is empty", func(t *testing.T) { 62 | _, err := signature.Verify(header, []byte{}) 63 | require.ErrorIs(t, err, signature.ErrInvalidSignature) 64 | }) 65 | 66 | t.Run("body is invalid", func(t *testing.T) { 67 | _, err := signature.Verify(header, []byte(`{}`)) 68 | require.ErrorIs(t, err, signature.ErrInvalidSignature) 69 | }) 70 | 71 | t.Run("body has extra bytes", func(t *testing.T) { 72 | _, err := signature.Verify(header, []byte(body+"...")) 73 | require.ErrorIs(t, err, signature.ErrInvalidSignature) 74 | }) 75 | 76 | t.Run("body has missing bytes", func(t *testing.T) { 77 | _, err := signature.Verify(header, []byte(body[:len(body)-8])) 78 | require.ErrorIs(t, err, signature.ErrInvalidSignature) 79 | }) 80 | } 81 | 82 | // TestVerifySignatureFromMetaMask ensures that a signature generated by MetaMask 83 | // can be verified by this package. 84 | func TestVerifySignatureFromMetaMask(t *testing.T) { 85 | // Source: use the "Sign Message" feature in Etherscan 86 | // to sign the keccak256 hash of `Hello` 87 | // Published to https://etherscan.io/verifySig/255560 88 | messageHash := crypto.Keccak256Hash([]byte("Hello")).Hex() 89 | require.Equal(t, `0x06b3dfaec148fb1bb2b066f10ec285e7c9bf402ab32aa78a5d38e34566810cd2`, messageHash) 90 | signerAddress := common.HexToAddress(`0x4bE0Cd2553356b4ABb8b6a1882325dAbC8d3013D`) 91 | signatureHash := `0xbf36915334f8fa93894cd54d491c31a89dbf917e9a4402b2779b73d21ecf46e36ff07db2bef6d10e92c99a02c1c5ea700b0b674dfa5d3ce9220822a7ebcc17101b` 92 | header := signerAddress.Hex() + ":" + signatureHash 93 | verifiedAddress, err := signature.Verify( 94 | header, 95 | []byte(`Hello`), 96 | ) 97 | require.NoError(t, err) 98 | require.Equal(t, signerAddress, verifiedAddress) 99 | } 100 | 101 | // TestVerifySignatureFromCast ensures that the signature generated by the `cast` CLI 102 | // can be verified by this package. 103 | func TestVerifySignatureFromCast(t *testing.T) { 104 | // Source: use `cast wallet sign` in the `cast` CLI 105 | // to sign the keccak256 hash of `Hello`: 106 | // `cast wallet sign --interactive $(cast from-utf8 $(cast keccak Hello))` 107 | // NOTE: The call to from-utf8 is required as cast wallet sign 108 | // interprets inputs with a leading 0x as a byte array, not a string. 109 | // Published to https://etherscan.io/verifySig/255562 110 | messageHash := crypto.Keccak256Hash([]byte("Hello")).Hex() 111 | require.Equal(t, `0x06b3dfaec148fb1bb2b066f10ec285e7c9bf402ab32aa78a5d38e34566810cd2`, messageHash) 112 | signerAddress := common.HexToAddress(`0x2485Aaa7C5453e04658378358f5E028150Dc7606`) 113 | signatureHash := `0xff2aa92eb8d8c2ca04f1755a4ddbff4bda6a5c9cefc8b706d5d8a21d3aa6fe7a20d3ec062fb5a4c1656fd2c14a8b33ca378b830d9b6168589bfee658e83745cc1b` 114 | header := signerAddress.Hex() + ":" + signatureHash 115 | verifiedAddress, err := signature.Verify( 116 | header, 117 | []byte(`Hello`), 118 | ) 119 | require.NoError(t, err) 120 | require.Equal(t, signerAddress, verifiedAddress) 121 | } 122 | 123 | // TestSignatureCreateAndVerify uses a randomly generated private key 124 | // to create a signature and then verifies it. 125 | func TestSignatureCreateAndVerify(t *testing.T) { 126 | signer, err := signature.NewRandomSigner() 127 | require.NoError(t, err) 128 | 129 | signerAddress := signer.Address() 130 | body := fmt.Sprintf( 131 | `{"jsonrpc":"2.0","method":"eth_getTransactionCount","params":["%s","pending"],"id":1}`, 132 | signerAddress, 133 | ) 134 | 135 | header, err := signer.Create([]byte(body)) 136 | require.NoError(t, err) 137 | 138 | verifiedAddress, err := signature.Verify(header, []byte(body)) 139 | require.NoError(t, err) 140 | require.Equal(t, signerAddress, verifiedAddress) 141 | } 142 | 143 | // TestSignatureCreateCompareToCastAndEthers uses a static private key 144 | // and compares the signature generated by this package to the signatures 145 | // generated by the `cast` CLI and ethers.js. 146 | func TestSignatureCreateCompareToCastAndEthers(t *testing.T) { 147 | // This purposefully uses the already highly compromised keypair from the go-ethereum book: 148 | // https://goethereumbook.org/transfer-eth/ 149 | // privateKey = fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19 150 | signer, err := signature.NewSignerFromHexPrivateKey("0xfad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19") 151 | require.NoError(t, err) 152 | 153 | address := signer.Address() 154 | body := []byte("Hello") 155 | 156 | // I generated the signature using the cast CLI: 157 | // 158 | // cast wallet sign --private-key fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19 $(cast from-utf8 $(cast keccak Hello)) 159 | // 160 | // (As mentioned above, the call to from-utf8 is required as cast wallet 161 | // sign interprets inputs with a leading 0x as a byte array, not a string.) 162 | // 163 | // As well as the following ethers script: 164 | // 165 | // import { Wallet } from "ethers"; 166 | // import { id } from 'ethers/lib/utils' 167 | // var w = new Wallet("0xfad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19") 168 | // `${ await w.getAddress() }:${ await w.signMessage(id("Hello")) }` 169 | //'0x96216849c49358B10257cb55b28eA603c874b05E:0x1446053488f02d460c012c84c4091cd5054d98c6cfca01b65f6c1a72773e80e60b8a4931aeee7ed18ce3adb45b2107e8c59e25556c1f871a8334e30e5bddbed21c' 170 | 171 | expectedSignature := "0x1446053488f02d460c012c84c4091cd5054d98c6cfca01b65f6c1a72773e80e60b8a4931aeee7ed18ce3adb45b2107e8c59e25556c1f871a8334e30e5bddbed21c" 172 | expectedAddress := common.HexToAddress("0x96216849c49358B10257cb55b28eA603c874b05E") 173 | expectedHeader := fmt.Sprintf("%s:%s", expectedAddress, expectedSignature) 174 | require.Equal(t, expectedAddress, address) 175 | 176 | header, err := signer.Create(body) 177 | require.NoError(t, err) 178 | require.Equal(t, expectedHeader, header) 179 | 180 | verifiedAddress, err := signature.Verify(header, body) 181 | require.NoError(t, err) 182 | require.Equal(t, expectedAddress, verifiedAddress) 183 | } 184 | 185 | func BenchmarkSignatureCreate(b *testing.B) { 186 | signer, err := signature.NewRandomSigner() 187 | require.NoError(b, err) 188 | 189 | body := []byte("Hello") 190 | 191 | b.ResetTimer() 192 | for i := 0; i < b.N; i++ { 193 | _, err := signer.Create(body) 194 | require.NoError(b, err) 195 | } 196 | } 197 | 198 | // benchmark signature verification 199 | func BenchmarkSignatureVerify(b *testing.B) { 200 | signer, err := signature.NewRandomSigner() 201 | require.NoError(b, err) 202 | 203 | body := "body" 204 | header, err := signer.Create([]byte(body)) 205 | require.NoError(b, err) 206 | 207 | b.ResetTimer() 208 | for i := 0; i < b.N; i++ { 209 | _, err := signature.Verify(header, []byte(body)) 210 | require.NoError(b, err) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all"] 2 | # checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023"] 3 | initialisms = ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] 4 | http_status_code_whitelist = ["200", "400", "404", "500"] 5 | -------------------------------------------------------------------------------- /tls/tls_generate.go: -------------------------------------------------------------------------------- 1 | // Package tls provides utilities for generating self-signed TLS certificates. 2 | package tls 3 | 4 | import ( 5 | "bytes" 6 | "crypto/ecdsa" 7 | "crypto/elliptic" 8 | "crypto/rand" 9 | "crypto/x509" 10 | "crypto/x509/pkix" 11 | "encoding/pem" 12 | "math/big" 13 | "net" 14 | "os" 15 | "time" 16 | ) 17 | 18 | // GetOrGenerateTLS tries to load a TLS certificate and key from the given paths, and if that fails, 19 | // it generates a new self-signed certificate and key and saves it. 20 | func GetOrGenerateTLS(certPath, certKeyPath string, validFor time.Duration, hosts []string) (cert, key []byte, err error) { 21 | // Check if the certificate and key files exist 22 | _, err1 := os.Stat(certPath) 23 | _, err2 := os.Stat(certKeyPath) 24 | if os.IsNotExist(err1) || os.IsNotExist(err2) { 25 | // If either file does not exist, generate a new certificate and key 26 | cert, key, err = GenerateTLS(validFor, hosts) 27 | if err != nil { 28 | return nil, nil, err 29 | } 30 | // Save the generated certificate and key to the specified paths 31 | err = os.WriteFile(certPath, cert, 0644) 32 | if err != nil { 33 | return nil, nil, err 34 | } 35 | err = os.WriteFile(certKeyPath, key, 0600) 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | return cert, key, nil 40 | } 41 | 42 | // The files exist, read them 43 | cert, err = os.ReadFile(certPath) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | key, err = os.ReadFile(certKeyPath) 48 | if err != nil { 49 | return nil, nil, err 50 | } 51 | 52 | return cert, key, nil 53 | } 54 | 55 | // GenerateTLS generated a TLS certificate and key. 56 | // based on https://go.dev/src/crypto/tls/generate_cert.go 57 | // - `hosts`: a list of ip / dns names to include in the certificate 58 | func GenerateTLS(validFor time.Duration, hosts []string) (cert, key []byte, err error) { 59 | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 60 | if err != nil { 61 | return nil, nil, err 62 | } 63 | keyUsage := x509.KeyUsageDigitalSignature 64 | 65 | notBefore := time.Now() 66 | notAfter := notBefore.Add(validFor) 67 | 68 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 69 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 70 | if err != nil { 71 | return nil, nil, err 72 | } 73 | 74 | template := x509.Certificate{ 75 | SerialNumber: serialNumber, 76 | Subject: pkix.Name{ 77 | Organization: []string{"Acme"}, 78 | }, 79 | NotBefore: notBefore, 80 | NotAfter: notAfter, 81 | 82 | KeyUsage: keyUsage, 83 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 84 | BasicConstraintsValid: true, 85 | } 86 | 87 | for _, h := range hosts { 88 | if ip := net.ParseIP(h); ip != nil { 89 | template.IPAddresses = append(template.IPAddresses, ip) 90 | } else { 91 | template.DNSNames = append(template.DNSNames, h) 92 | } 93 | } 94 | 95 | // certificate is its own CA 96 | template.IsCA = true 97 | template.KeyUsage |= x509.KeyUsageCertSign 98 | 99 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | var certOut bytes.Buffer 105 | if err = pem.Encode(&certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { 106 | return nil, nil, err 107 | } 108 | cert = certOut.Bytes() 109 | 110 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 111 | if err != nil { 112 | return nil, nil, err 113 | } 114 | 115 | var keyOut bytes.Buffer 116 | err = pem.Encode(&keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) 117 | if err != nil { 118 | return nil, nil, err 119 | } 120 | key = keyOut.Bytes() 121 | return cert, key, nil 122 | } 123 | -------------------------------------------------------------------------------- /truthy/truthy.go: -------------------------------------------------------------------------------- 1 | // Package truthy implements helpers to test the truthy-ness of the values. 2 | package truthy 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var isTruthy = map[string]bool{ 10 | // truthy 11 | "1": true, 12 | "t": true, 13 | "true": true, 14 | "y": true, 15 | "yes": true, 16 | // non-truthy 17 | "": false, 18 | "0": false, 19 | "f": false, 20 | "false": false, 21 | "n": false, 22 | "no": false, 23 | } 24 | 25 | // Is returns `false` if the argument sounds like "false" (empty string, "0", 26 | // "f", "false", and so on), and `true` otherwise. 27 | func Is(val string) (bool, error) { 28 | if res, known := isTruthy[strings.ToLower(val)]; known { 29 | return res, nil 30 | } 31 | return false, fmt.Errorf("can not resolve truthy-ness of \"%s\"", val) 32 | } 33 | 34 | // TrueOnError returns true if err is not nil, otherwise it returns res. 35 | func TrueOnError(res bool, err error) bool { 36 | if err != nil { 37 | return true 38 | } 39 | return res 40 | } 41 | 42 | // FalseOnError returns false if err is not nil, otherwise it returns res. 43 | func FalseOnError(res bool, err error) bool { 44 | if err != nil { 45 | return false 46 | } 47 | return res 48 | } 49 | -------------------------------------------------------------------------------- /truthy/truthy_test.go: -------------------------------------------------------------------------------- 1 | package truthy_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/flashbots/go-utils/truthy" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestIs(t *testing.T) { 12 | { // truthy values 13 | for _, y := range []string{ 14 | "1", 15 | "t", 16 | "T", 17 | "true", 18 | "True", 19 | "Y", 20 | "yes", 21 | } { 22 | assert.True( 23 | t, 24 | truthy.FalseOnError(truthy.Is(y)), 25 | fmt.Sprintf("Value '%s' must render as truthy", y), 26 | ) 27 | } 28 | } 29 | { // falsy values 30 | for _, n := range []string{ 31 | "", 32 | "0", 33 | "f", 34 | "F", 35 | "false", 36 | "False", 37 | "N", 38 | "no", 39 | } { 40 | assert.False( 41 | t, 42 | truthy.TrueOnError(truthy.Is(n)), 43 | fmt.Sprintf("Value '%s' must render as falsy", n), 44 | ) 45 | } 46 | } 47 | } 48 | --------------------------------------------------------------------------------