├── .github
├── CODEOWNERS
├── pull_request_template.md
└── workflows
│ └── test.yml
├── .gitignore
├── .idea
├── .gitignore
├── modules.xml
├── rpc-endpoint.iml
└── vcs.xml
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── adapters
├── flashbots
│ ├── signature.go
│ └── signature_test.go
└── webfile
│ ├── fetcher.go
│ └── fetcher_test.go
├── application
├── builder_info.go
└── rpc_cache.go
├── cmd
├── mockbackend
│ └── main.go
├── server
│ └── main.go
└── txdecoder
│ └── main.go
├── database
├── mem_store.go
├── mock_store.go
├── postgres_store.go
├── store.go
└── types.go
├── docker-compose.yaml
├── docs
├── metamask.md
└── testing.md
├── go.mod
├── go.sum
├── server
├── configuration.go
├── fingerprint.go
├── fingerprint_test.go
├── http_client.go
├── ofacblacklist.go
├── ofacblacklist_test.go
├── redisstate.go
├── redisstate_test.go
├── request_handler.go
├── request_intercepts.go
├── request_processor.go
├── request_processor_test.go
├── request_record.go
├── request_record_test.go
├── request_response.go
├── request_sendrawtx.go
├── server.go
├── url_params.go
├── url_params_test.go
├── util.go
└── whitelist.go
├── sql
├── psql
│ ├── 001_initial.schema.down.sql
│ ├── 001_initial.schema.up.sql
│ ├── 002_add_fast.down.sql
│ ├── 002_add_fast.up.sql
│ ├── 003_alter_is_blocked.down.sql
│ └── 003_alter_is_blocked.up.sql
└── redshift
│ ├── 001_initial.schema.down.sql
│ ├── 001_initial.schema.up.sql
│ ├── 002_add_fast.down.sql
│ ├── 002_add_fast.up.sql
│ ├── 003_alter_is_blocked.down.sql
│ └── 003_alter_is_blocked.up.sql
├── staticcheck.conf
├── tests
└── e2e_test.go
├── testutils
├── mock_rpcbackend.go
├── mock_txapibackend.go
├── rpctesthelpers.go
└── transactions.go
└── types
└── types.go
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These owners will be the default owners for everything in
2 | # the repo. Unless a later match takes precedence,
3 | # they will be requested for review when someone opens a pull request.
4 | * @dvush @Wazzymandias @TymKh @metachris
5 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## 📝 Summary
2 |
3 |
4 |
5 | ## ⛱ Motivation and Context
6 |
7 |
8 |
9 | ## 📚 References
10 |
11 |
12 |
13 | ---
14 |
15 | ## ✅ I have run these commands
16 |
17 | * [ ] `make lint`
18 | * [ ] `make test`
19 | * [ ] `go mod tidy`
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
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.22
18 | id: go
19 |
20 | - name: Check out code into the Go module directory
21 | uses: actions/checkout@v2
22 |
23 | - name: Run unit tests and generate the coverage report
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.22
34 | id: go
35 |
36 | - name: Check out code into the Go module directory
37 | uses: actions/checkout@v2
38 |
39 | - name: Install staticcheck
40 | run: go install honnef.co/go/tools/cmd/staticcheck@2024.1.1
41 |
42 | - name: Lint
43 | run: make lint
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /rpc-endpoint
3 | /tmp
4 | /log*.txt
5 | /run*.sh
6 | .idea/*
7 | /.vscode
8 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/rpc-endpoint.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement writing an
63 | email to leo@flashbots.net or contacting elopio#8526 in
64 | [Discord](https://discord.com/invite/7hvTycdNcK).
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.22-alpine as builder
2 | WORKDIR /build
3 | ADD . /build
4 | RUN apk add --no-cache gcc musl-dev linux-headers git make
5 | RUN --mount=type=cache,target=/root/.cache/go-build make build-for-docker
6 |
7 | FROM alpine:latest
8 | WORKDIR /app
9 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
10 | COPY --from=builder /build/rpc-endpoint /app/rpc-endpoint
11 | ENV LISTEN_ADDR=":8080"
12 | EXPOSE 8080
13 | CMD ["/app/rpc-endpoint"]
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 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 | .PHONY: all build test clean lint cover cover-html up down
2 |
3 | APP_NAME := rpc-endpoint
4 | GOPATH := $(if $(GOPATH),$(GOPATH),~/go)
5 | GIT_VER := $(shell git describe --tags --always --dirty="-dev")
6 | PACKAGES := $(shell go list -mod=readonly ./...)
7 |
8 | all: clean build
9 |
10 | build:
11 | CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.version=${GIT_VER}" -v -o ${APP_NAME} cmd/server/main.go
12 |
13 | clean:
14 | rm -rf ${APP_NAME} build/
15 |
16 | test:
17 | go test ./...
18 |
19 | gofmt:
20 | gofmt -w ./
21 |
22 | lint:
23 | go fmt -mod=readonly $(PACKAGES)
24 | go vet ./...
25 | staticcheck ./...
26 |
27 | cover:
28 | go test -coverpkg=github.com/flashbots/rpc-endpoint/server,github.com/flashbots/rpc-endpoint/types,github.com/flashbots/rpc-endpoint/utils -coverprofile=/tmp/go-rpcendpoint.cover.tmp ./...
29 | go tool cover -func /tmp/go-rpcendpoint.cover.tmp
30 | unlink /tmp/go-rpcendpoint.cover.tmp
31 |
32 | cover-html:
33 | go test -coverpkg=github.com/flashbots/rpc-endpoint/server,github.com/flashbots/rpc-endpoint/types,github.com/flashbots/rpc-endpoint/utils -coverprofile=/tmp/go-rpcendpoint.cover.tmp ./...
34 | go tool cover -html=/tmp/go-rpcendpoint.cover.tmp
35 | unlink /tmp/go-rpcendpoint.cover.tmp
36 |
37 | up:
38 | docker-compose up -d
39 |
40 | down:
41 | docker-compose down -v
42 |
43 | build-for-docker:
44 | CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.version=${GIT_VER}" -v -o ${APP_NAME} cmd/server/main.go
45 |
46 | docker-image:
47 | DOCKER_BUILDKIT=1 docker build --platform linux/amd64 . -t ${IMAGE_REGISTRY_ACCOUNT}/${APP_NAME}:${GIT_VER}
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Flashbots Protect RPC Endpoint
2 |
3 | [](https://github.com/flashbots/rpc-endpoint/actions?query=workflow%3A%22Test%22)
4 | [](https://github.com/RichardLitt/standard-readme)
5 | [](https://discord.gg/7hvTycdNcK)
6 |
7 | This repository contains code for a server which can be used as an RPC endpoint in popular Ethereum wallets.
8 |
9 | The endpoint is live at **https://rpc.flashbots.net/**
10 |
11 | It does two basic things:
12 | - It receives JSON-RPC requests, proxies those to a node, and responds with the result of the proxied request.
13 | - On receiving an `eth_sendRawTransaction` call which has data and is not on whitelisted methods, the call is sent to the Flashbots relay as a private transaction, and submitted as bundles for up to 25 blocks.
14 |
15 | There are a few key benefits to using the Flashbots RPC endpoint:
16 |
17 | - Frontrunning protection: your transaction will not be seen by hungry sandwich bots in the public mempool.
18 | - No failed transactions: your transaction will only be mined if it doesn't include any reverts, so you don't pay for failed transactions. Note: your transaction could be uncled, emitted to the mempool, and then included on-chain.
19 | - Priority in blocks: transactions sent via Flashbots are mined at the top of blocks, giving them priority.
20 |
21 | Privacy notice: rpc-endpoint does not track, store or log any kind of user information (i.e. IP, location, etc.).
22 |
23 | ## Transaction Status Check
24 |
25 | If a transaction is sent to the Flashbots relay instead of the public mempool, you cannot see the status on Etherscan or other explorers. Flashbots provides a Protect Transaction API to get the status of these private transactions: **https://protect.flashbots.net/**
26 |
27 | ## Usage
28 |
29 | To send your transactions through the Flashbots Protect RPC please refer to the [quick-start guide](https://docs.flashbots.net/flashbots-protect/rpc/quick-start/).
30 |
31 | To run the server, run the following command:
32 |
33 | ```bash
34 | go run cmd/server/main.go -redis REDIS_URL -signingKey ETH_PRIVATE_KEY -proxy PROXY_URL
35 |
36 | # For development, you can use built-in redis and create a random signing key
37 | go run cmd/server/main.go -redis dev -signingKey dev -proxy PROXY_URL
38 |
39 | # You can use the DEBUG_DONT_SEND_RAWTX to skip sending transactions anywhere (useful for local testing):
40 | DEBUG_DONT_SEND_RAWTX=1 go run cmd/server/main.go -redis dev -signingKey dev -proxy PROXY_URL
41 | ```
42 |
43 | Example Single request:
44 |
45 | ```bash
46 | curl localhost:9000 -f -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", false],"id":1}'
47 | ```
48 |
49 | ## Contributing
50 |
51 | [Flashbots](https://flashbots.net) is a research and development collective working on mitigating the negative externalities of decentralized economies. We contribute with the larger free software community to illuminate the dark forest.
52 |
53 | You are welcome here <3.
54 |
55 | - If you want to join us, come and say hi in our [Discord chat](https://discord.gg/7hvTycdNcK).
56 | - If you have a question, feedback or a bug report for this project, please [open a new Issue](https://github.com/flashbots/rpc-endpoint/issues).
57 | - We ask you to be nice. Read our [code of conduct](CODE_OF_CONDUCT.md).
58 |
59 | **Send a pull request**
60 |
61 | - Your proposed changes should be first described and discussed in an issue.
62 | - Every pull request should be small and represent a single change. If the problem is complicated, split it in multiple issues and pull requests.
63 | - Every pull request should be covered by unit/e2e tests.
64 |
65 | We appreciate your contributions <3
66 |
67 | Please install [`staticcheck`](https://staticcheck.io/) (`go install honnef.co/go/tools/cmd/staticcheck@master`) and run `make lint` before submitting a PR.
68 |
69 | ## Security
70 |
71 | If you find a security vulnerability on this project or any other initiative related to Flashbots, please let us know sending an email to security@flashbots.net.
72 |
73 | ## License
74 |
75 | The code in this project is free software under the [MIT license](LICENSE).
76 |
77 | ---
78 |
79 | Made with ☀️ by the ⚡🤖 collective.
80 |
--------------------------------------------------------------------------------
/adapters/flashbots/signature.go:
--------------------------------------------------------------------------------
1 | // Package flashbots provides methods for parsing the X-Flashbots-Signature header.
2 | package flashbots
3 |
4 | import (
5 | "errors"
6 | "fmt"
7 | "strings"
8 |
9 | "github.com/ethereum/go-ethereum/accounts"
10 | "github.com/ethereum/go-ethereum/common/hexutil"
11 | "github.com/ethereum/go-ethereum/crypto"
12 | )
13 |
14 | var (
15 | ErrNoSignature = errors.New("no signature provided")
16 | ErrInvalidSignature = errors.New("invalid signature provided")
17 | )
18 |
19 | func ParseSignature(header string, body []byte) (signingAddress string, err error) {
20 | if header == "" {
21 | return "", ErrNoSignature
22 | }
23 |
24 | splitSig := strings.Split(header, ":")
25 | if len(splitSig) != 2 {
26 | return "", ErrInvalidSignature
27 | }
28 |
29 | return VerifySignature(body, splitSig[0], splitSig[1])
30 | }
31 |
32 | func VerifySignature(body []byte, signingAddressStr, signatureStr string) (signingAddress string, err error) {
33 | signature, err := hexutil.Decode(signatureStr)
34 | if err != nil || len(signature) == 0 {
35 | return "", fmt.Errorf("%w: %w", ErrInvalidSignature, err)
36 | }
37 |
38 | if signature[len(signature)-1] >= 27 {
39 | signature[len(signature)-1] -= 27
40 | }
41 |
42 | hashedBody := crypto.Keccak256Hash(body).Hex()
43 | messageHash := accounts.TextHash([]byte(hashedBody))
44 | signaturePublicKeyBytes, err := crypto.Ecrecover(messageHash, signature)
45 | if err != nil {
46 | return "", fmt.Errorf("%w: %w", ErrInvalidSignature, err)
47 | }
48 |
49 | publicKey, err := crypto.UnmarshalPubkey(signaturePublicKeyBytes)
50 | if err != nil {
51 | return "", fmt.Errorf("%w: %w", ErrInvalidSignature, err)
52 | }
53 | signaturePubkey := *publicKey
54 | signaturePubKeyAddress := crypto.PubkeyToAddress(signaturePubkey).Hex()
55 |
56 | // case-insensitive equality check
57 | if !strings.EqualFold(signaturePubKeyAddress, signingAddressStr) {
58 | return "", fmt.Errorf("%w: signing address mismatch", ErrInvalidSignature)
59 | }
60 |
61 | signatureNoRecoverID := signature[:len(signature)-1] // remove recovery id
62 | if !crypto.VerifySignature(signaturePublicKeyBytes, messageHash, signatureNoRecoverID) {
63 | return "", fmt.Errorf("%w: %w", ErrInvalidSignature, err)
64 | }
65 |
66 | return signaturePubKeyAddress, nil
67 | }
68 |
--------------------------------------------------------------------------------
/adapters/flashbots/signature_test.go:
--------------------------------------------------------------------------------
1 | package flashbots_test
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/ethereum/go-ethereum/accounts"
9 | "github.com/ethereum/go-ethereum/common/hexutil"
10 | "github.com/ethereum/go-ethereum/crypto"
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/flashbots/rpc-endpoint/adapters/flashbots"
14 | )
15 |
16 | func TestParseSignature(t *testing.T) {
17 |
18 | // For most of these test cases, we first need to generate a signature
19 | privateKey, err := crypto.GenerateKey()
20 | require.NoError(t, err)
21 |
22 | address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex()
23 | body := fmt.Sprintf(
24 | `{"jsonrpc":"2.0","method":"eth_getTransactionCount","params":["%s","pending"],"id":1}`,
25 | address,
26 | )
27 |
28 | signature, err := crypto.Sign(
29 | accounts.TextHash([]byte(hexutil.Encode(crypto.Keccak256([]byte(body))))),
30 | privateKey,
31 | )
32 | require.NoError(t, err)
33 |
34 | header := fmt.Sprintf("%s:%s", address, hexutil.Encode(signature))
35 |
36 | t.Run("header is empty", func(t *testing.T) {
37 | _, err := flashbots.ParseSignature("", []byte{})
38 | require.ErrorIs(t, err, flashbots.ErrNoSignature)
39 | })
40 |
41 | t.Run("header is valid", func(t *testing.T) {
42 | signingAddress, err := flashbots.ParseSignature(header, []byte(body))
43 | require.NoError(t, err)
44 | require.Equal(t, address, signingAddress)
45 | })
46 |
47 | t.Run("header is invalid", func(t *testing.T) {
48 | _, err := flashbots.ParseSignature("invalid", []byte(body))
49 | require.ErrorIs(t, err, flashbots.ErrInvalidSignature)
50 | })
51 |
52 | t.Run("header has extra bytes", func(t *testing.T) {
53 | _, err := flashbots.ParseSignature(header+"deadbeef", []byte(body))
54 | require.ErrorIs(t, err, flashbots.ErrInvalidSignature)
55 | })
56 |
57 | t.Run("header has missing bytes", func(t *testing.T) {
58 | _, err := flashbots.ParseSignature(header[:len(header)-8], []byte(body))
59 | require.ErrorIs(t, err, flashbots.ErrInvalidSignature)
60 | })
61 |
62 | t.Run("body is empty", func(t *testing.T) {
63 | _, err := flashbots.ParseSignature(header, []byte{})
64 | require.ErrorIs(t, err, flashbots.ErrInvalidSignature)
65 | })
66 |
67 | t.Run("body is invalid", func(t *testing.T) {
68 | _, err := flashbots.ParseSignature(header, []byte(`{}`))
69 | require.ErrorIs(t, err, flashbots.ErrInvalidSignature)
70 | })
71 |
72 | t.Run("body has extra bytes", func(t *testing.T) {
73 | _, err := flashbots.ParseSignature(header, []byte(body+"..."))
74 | require.ErrorIs(t, err, flashbots.ErrInvalidSignature)
75 | })
76 |
77 | t.Run("body has missing bytes", func(t *testing.T) {
78 | _, err := flashbots.ParseSignature(header, []byte(body[:len(body)-8]))
79 | require.ErrorIs(t, err, flashbots.ErrInvalidSignature)
80 | })
81 | }
82 |
83 | func TestVerifySignatureFromMetaMask(t *testing.T) {
84 | // Source: use the "Sign Message" feature in Etherscan
85 | // to sign the keccak256 hash of `Hello`
86 | // Published to https://etherscan.io/verifySig/255560
87 | messageHash := crypto.Keccak256Hash([]byte("Hello")).Hex()
88 | require.Equal(t, `0x06b3dfaec148fb1bb2b066f10ec285e7c9bf402ab32aa78a5d38e34566810cd2`, messageHash)
89 | address := `0x4bE0Cd2553356b4ABb8b6a1882325dAbC8d3013D`
90 | signatureHash := `0xbf36915334f8fa93894cd54d491c31a89dbf917e9a4402b2779b73d21ecf46e36ff07db2bef6d10e92c99a02c1c5ea700b0b674dfa5d3ce9220822a7ebcc17101b`
91 | header := address + ":" + signatureHash
92 | signingAddress, err := flashbots.ParseSignature(
93 | header,
94 | []byte(`Hello`),
95 | )
96 | require.NoError(t, err)
97 | require.Equal(t, strings.ToLower(address), strings.ToLower(signingAddress))
98 | }
99 |
100 | func TestVerifySignatureCast(t *testing.T) {
101 | // Source: use `cast wallet sign` in the `cast` CLI
102 | // to sign the keccak256 hash of `Hello`:
103 | // `cast wallet sign --interactive $(cast from-utf8 $(cast keccak Hello))`
104 | // NOTE: The call to from-utf8 is required as cast wallet sign
105 | // interprets inputs with a leading 0x as a byte array, not a string.
106 | // Published to https://etherscan.io/verifySig/255562
107 | messageHash := crypto.Keccak256Hash([]byte("Hello")).Hex()
108 | require.Equal(t, `0x06b3dfaec148fb1bb2b066f10ec285e7c9bf402ab32aa78a5d38e34566810cd2`, messageHash)
109 | address := `0x2485Aaa7C5453e04658378358f5E028150Dc7606`
110 | signatureHash := `0xff2aa92eb8d8c2ca04f1755a4ddbff4bda6a5c9cefc8b706d5d8a21d3aa6fe7a20d3ec062fb5a4c1656fd2c14a8b33ca378b830d9b6168589bfee658e83745cc1b`
111 | header := address + ":" + signatureHash
112 | signingAddress, err := flashbots.ParseSignature(
113 | header,
114 | []byte(`Hello`),
115 | )
116 | require.NoError(t, err)
117 | require.Equal(t, strings.ToLower(address), strings.ToLower(signingAddress))
118 | }
119 |
--------------------------------------------------------------------------------
/adapters/webfile/fetcher.go:
--------------------------------------------------------------------------------
1 | package webfile
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | var ErrRequest = fmt.Errorf("request failed")
11 |
12 | //https://raw.githubusercontent.com/flashbots/dowg/main/builder-registrations.json
13 |
14 | type Fetcher struct {
15 | url string
16 | cl http.Client
17 | }
18 |
19 | func NewFetcher(url string) *Fetcher {
20 | return &Fetcher{url: url, cl: http.Client{}}
21 | }
22 |
23 | func (f *Fetcher) Fetch(ctx context.Context) ([]byte, error) {
24 | //execute http request and load bytes
25 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, f.url, nil)
26 | if err != nil {
27 | return nil, err
28 | }
29 | resp, err := f.cl.Do(httpReq)
30 | if err != nil {
31 | return nil, err
32 | }
33 | bts, err := io.ReadAll(resp.Body)
34 | resp.Body.Close()
35 | if err != nil {
36 | return nil, err
37 | }
38 | if resp.StatusCode >= 400 {
39 | return nil, fmt.Errorf("err: %w status code %d", ErrRequest, resp.StatusCode)
40 | }
41 | return bts, nil
42 | }
43 |
--------------------------------------------------------------------------------
/adapters/webfile/fetcher_test.go:
--------------------------------------------------------------------------------
1 | package webfile
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "testing"
8 | )
9 |
10 | func TestFetch(t *testing.T) {
11 | f := Fetcher{
12 | url: "https://raw.githubusercontent.com/flashbots/dowg/main/builder-registrations.json",
13 | cl: http.Client{},
14 | }
15 | bts, err := f.Fetch(context.Background())
16 | if err != nil {
17 | panic(err)
18 | }
19 |
20 | fmt.Println(string(bts))
21 | }
22 |
--------------------------------------------------------------------------------
/application/builder_info.go:
--------------------------------------------------------------------------------
1 | package application
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "strings"
7 | "time"
8 |
9 | "github.com/ethereum/go-ethereum/log"
10 | )
11 |
12 | type BuilderInfo struct {
13 | Name string `json:"name"`
14 | RPC string `json:"rpc"`
15 | SupportedApis []string `json:"supported-apis"`
16 | }
17 | type Fetcher interface {
18 | Fetch(ctx context.Context) ([]byte, error)
19 | }
20 | type BuilderInfoService struct {
21 | fetcher Fetcher
22 | builderInfos []BuilderInfo
23 | }
24 |
25 | func StartBuilderInfoService(ctx context.Context, fetcher Fetcher, fetchInterval time.Duration) (*BuilderInfoService, error) {
26 | bis := BuilderInfoService{
27 | fetcher: fetcher,
28 | }
29 | if fetcher != nil {
30 | err := bis.fetchBuilderInfo(ctx)
31 | if err != nil {
32 | return nil, err
33 | }
34 | go bis.syncLoop(fetchInterval)
35 |
36 | }
37 | return &bis, nil
38 | }
39 | func (bis *BuilderInfoService) Builders() []BuilderInfo {
40 | return bis.builderInfos
41 | }
42 |
43 | func (bis *BuilderInfoService) BuilderNames() []string {
44 | var names = make([]string, 0, len(bis.builderInfos))
45 | for _, builderInfo := range bis.builderInfos {
46 | names = append(names, strings.ToLower(builderInfo.Name))
47 | }
48 | return names
49 | }
50 |
51 | func (bis *BuilderInfoService) syncLoop(fetchInterval time.Duration) {
52 | ticker := time.NewTicker(fetchInterval)
53 | for range ticker.C {
54 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
55 | err := bis.fetchBuilderInfo(ctx)
56 | if err != nil {
57 | //TODO: probably panic on multiple consequent errors, though it's not critical in nature
58 | log.Error("failed to fetch builder info", "err", err)
59 | }
60 | cancel()
61 | }
62 | }
63 |
64 | func (bis *BuilderInfoService) fetchBuilderInfo(ctx context.Context) error {
65 | bts, err := bis.fetcher.Fetch(ctx)
66 | if err != nil {
67 | return err
68 | }
69 | var builderInfos []BuilderInfo
70 | err = json.Unmarshal(bts, &builderInfos)
71 | if err != nil {
72 | return err
73 | }
74 | bis.builderInfos = builderInfos
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/application/rpc_cache.go:
--------------------------------------------------------------------------------
1 | package application
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/flashbots/rpc-endpoint/types"
8 | )
9 |
10 | type value struct {
11 | data *types.JsonRpcResponse
12 | timestamp int64
13 | }
14 | type RpcCache struct {
15 | mu sync.Mutex
16 | cache map[string]value
17 | ttl int64
18 | }
19 |
20 | func NewRpcCache(ttl int64) *RpcCache {
21 | return &RpcCache{
22 | cache: make(map[string]value),
23 | ttl: ttl,
24 | mu: sync.Mutex{},
25 | }
26 | }
27 |
28 | func (rc *RpcCache) Get(key string) (*types.JsonRpcResponse, bool) {
29 | rc.mu.Lock()
30 | defer rc.mu.Unlock()
31 | v, ok := rc.cache[key]
32 | if !ok {
33 | return nil, false
34 | }
35 | if time.Now().Unix()-v.timestamp > rc.ttl {
36 | delete(rc.cache, key)
37 | return nil, false
38 | }
39 | return v.data, ok
40 | }
41 |
42 | func (rc *RpcCache) Set(key string, data *types.JsonRpcResponse) {
43 | rc.mu.Lock()
44 | defer rc.mu.Unlock()
45 | rc.cache[key] = value{
46 | data: data,
47 | timestamp: time.Now().Unix(),
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/cmd/mockbackend/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/flashbots/rpc-endpoint/testutils"
8 | )
9 |
10 | func main() {
11 | port := 8090
12 | http.HandleFunc("/", testutils.RpcBackendHandler)
13 | fmt.Printf("rpc backend listening on localhost:%d\n", port)
14 | http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil)
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "flag"
6 | "os"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/ethereum/go-ethereum/crypto"
11 | "github.com/ethereum/go-ethereum/log"
12 | "github.com/flashbots/rpc-endpoint/database"
13 | "github.com/flashbots/rpc-endpoint/server"
14 | )
15 |
16 | var (
17 | version = "dev" // is set during build process
18 |
19 | // defaults
20 | defaultDebug = os.Getenv("DEBUG") == "1"
21 | defaultLogJSON = os.Getenv("LOG_JSON") == "1"
22 | defaultListenAddress = "127.0.0.1:9000"
23 | defaultDrainAddress = "127.0.0.1:9001"
24 | defaultDrainSeconds = 60
25 | defaultProxyUrl = "http://127.0.0.1:8545"
26 | defaultProxyTimeoutSeconds = 10
27 | defaultRelayUrl = "https://relay.flashbots.net"
28 | defaultRedisUrl = "localhost:6379"
29 | defaultServiceName = os.Getenv("SERVICE_NAME")
30 | defaultFetchInfoIntervalSeconds = 600
31 | defaultRpcTTLCacheSeconds = 300
32 | defaultMempoolRPC = os.Getenv("DEFAULT_MEMPOOL_RPC")
33 |
34 | // cli flags
35 | versionPtr = flag.Bool("version", false, "just print the program version")
36 | listenAddress = flag.String("listen", getEnvAsStrOrDefault("LISTEN_ADDR", defaultListenAddress), "Listen address")
37 | drainAddress = flag.String("drain", getEnvAsStrOrDefault("DRAIN_ADDR", defaultDrainAddress), "Drain address")
38 | drainSeconds = flag.Int("drainSeconds", getEnvAsIntOrDefault("DRAIN_SECONDS", defaultDrainSeconds), "seconds to wait for graceful shutdown")
39 | fetchIntervalSeconds = flag.Int("fetchIntervalSeconds", getEnvAsIntOrDefault("FETCH_INFO_INTERVAL_SECONDS", defaultFetchInfoIntervalSeconds), "seconds between builder info fetches")
40 | ttlCacheSeconds = flag.Int("ttlCacheSeconds", getEnvAsIntOrDefault("TTL_CACHE_SECONDS", defaultRpcTTLCacheSeconds), "seconds to cache static requests")
41 | builderInfoSource = flag.String("builderInfoSource", getEnvAsStrOrDefault("BUILDER_INFO_SOURCE", ""), "URL for json source of actual builder info")
42 | proxyUrl = flag.String("proxy", getEnvAsStrOrDefault("PROXY_URL", defaultProxyUrl), "URL for default JSON-RPC proxy target (eth node, Infura, etc.)")
43 | proxyTimeoutSeconds = flag.Int("proxyTimeoutSeconds", getEnvAsIntOrDefault("PROXY_TIMEOUT_SECONDS", defaultProxyTimeoutSeconds), "proxy client timeout in seconds")
44 | redisUrl = flag.String("redis", getEnvAsStrOrDefault("REDIS_URL", defaultRedisUrl), "URL for Redis (use 'dev' to use integrated in-memory redis)")
45 | relayUrl = flag.String("relayUrl", getEnvAsStrOrDefault("RELAY_URL", defaultRelayUrl), "URL for relay")
46 | relaySigningKey = flag.String("signingKey", os.Getenv("RELAY_SIGNING_KEY"), "Signing key for relay requests")
47 | psqlDsn = flag.String("psql", os.Getenv("POSTGRES_DSN"), "Postgres DSN")
48 | debugPtr = flag.Bool("debug", defaultDebug, "print debug output")
49 | logJSONPtr = flag.Bool("logJSON", defaultLogJSON, "log in JSON")
50 | serviceName = flag.String("serviceName", defaultServiceName, "name of the service which will be used in the logs")
51 | )
52 |
53 | func main() {
54 | var key *ecdsa.PrivateKey
55 | var err error
56 |
57 | flag.Parse()
58 |
59 | logLevel := log.LvlInfo
60 | if *debugPtr {
61 | logLevel = log.LvlDebug
62 | }
63 | termHandler := log.NewTerminalHandlerWithLevel(os.Stderr, logLevel, false)
64 | rLogger := log.NewLogger(termHandler)
65 | if *logJSONPtr {
66 | // NOTE(tymurkh): unfortunately seems to be no way to pass LogLevel in jsonHandler, so level will always be info
67 | rLogger = log.NewLogger(log.JSONHandler(os.Stderr))
68 | }
69 |
70 | log.SetDefault(rLogger)
71 |
72 | logger := log.New()
73 | if *serviceName != "" {
74 | logger = logger.New("service", *serviceName)
75 | }
76 | // Perhaps print only the version
77 | if *versionPtr {
78 | logger.Info("rpc-endpoint", "version", version)
79 | return
80 | }
81 |
82 | logger.Info("Init rpc-endpoint", "version", version)
83 |
84 | if *relaySigningKey == "" {
85 | logger.Crit("Cannot use the relay without a signing key.")
86 | }
87 |
88 | pkHex := strings.Replace(*relaySigningKey, "0x", "", 1)
89 | if pkHex == "dev" {
90 | logger.Info("Creating a new dev signing key...")
91 | key, err = crypto.GenerateKey()
92 | } else {
93 | key, err = crypto.HexToECDSA(pkHex)
94 | }
95 |
96 | if err != nil {
97 | logger.Crit("Error with relay signing key", "error", err)
98 | }
99 |
100 | // Setup database
101 | var db database.Store
102 | if *psqlDsn == "" {
103 | db = database.NewMockStore()
104 | } else {
105 | db = database.NewPostgresStore(*psqlDsn)
106 | }
107 | // Start the endpoint
108 | s, err := server.NewRpcEndPointServer(server.Configuration{
109 | DB: db,
110 | DrainAddress: *drainAddress,
111 | DrainSeconds: *drainSeconds,
112 | ListenAddress: *listenAddress,
113 | Logger: logger,
114 | ProxyTimeoutSeconds: *proxyTimeoutSeconds,
115 | ProxyUrl: *proxyUrl,
116 | RedisUrl: *redisUrl,
117 | RelaySigningKey: key,
118 | RelayUrl: *relayUrl,
119 | Version: version,
120 | BuilderInfoSource: *builderInfoSource,
121 | FetchInfoInterval: *fetchIntervalSeconds,
122 | TTLCacheSeconds: int64(*ttlCacheSeconds),
123 | DefaultMempoolRPC: defaultMempoolRPC,
124 | })
125 | if err != nil {
126 | logger.Crit("Server init error", "error", err)
127 | }
128 | logger.Info("Starting rpc-endpoint...", "relayUrl", *relayUrl, "proxyUrl", *proxyUrl)
129 | s.Start()
130 | }
131 |
132 | func getEnvAsStrOrDefault(key string, defaultValue string) string {
133 | ret := os.Getenv(key)
134 | if ret == "" {
135 | ret = defaultValue
136 | }
137 | return ret
138 | }
139 |
140 | func getEnvAsIntOrDefault(name string, defaultValue int) int {
141 | if valueStr, exists := os.LookupEnv(name); exists {
142 | if value, err := strconv.Atoi(valueStr); err == nil {
143 | return value
144 | }
145 | }
146 | return defaultValue
147 | }
148 |
--------------------------------------------------------------------------------
/cmd/txdecoder/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/flashbots/rpc-endpoint/server"
8 | )
9 |
10 | var rawTx = ""
11 |
12 | func main() {
13 | tx, err := server.GetTx(rawTx)
14 | if err != nil {
15 | log.Fatal(err)
16 | }
17 |
18 | txFrom, err := server.GetSenderFromRawTx(tx)
19 | if err != nil {
20 | log.Fatal(err)
21 | }
22 |
23 | txHash := tx.Hash().Hex()
24 |
25 | fmt.Println("rawTx:", rawTx)
26 | fmt.Println("Hash: ", txHash)
27 | fmt.Println("From: ", txFrom)
28 | fmt.Println("To: ", tx.To())
29 | fmt.Println("Nonce:", tx.Nonce())
30 | }
31 |
--------------------------------------------------------------------------------
/database/mem_store.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "github.com/google/uuid"
5 | "sync"
6 | )
7 |
8 | type memStore struct {
9 | Requests map[uuid.UUID]RequestEntry
10 | EthSendRawTxs map[uuid.UUID][]*EthSendRawTxEntry
11 | mutex sync.Mutex
12 | }
13 |
14 | func NewMemStore() *memStore {
15 | return &memStore{
16 | Requests: make(map[uuid.UUID]RequestEntry),
17 | EthSendRawTxs: make(map[uuid.UUID][]*EthSendRawTxEntry),
18 | mutex: sync.Mutex{},
19 | }
20 | }
21 |
22 | func (m *memStore) SaveRequestEntry(entry RequestEntry) error {
23 | m.mutex.Lock()
24 | defer m.mutex.Unlock()
25 | m.Requests[entry.Id] = entry
26 | return nil
27 | }
28 |
29 | func (m *memStore) SaveRawTxEntries(entries []*EthSendRawTxEntry) error {
30 | if len(entries) != 0 {
31 | m.mutex.Lock()
32 | defer m.mutex.Unlock()
33 | m.EthSendRawTxs[entries[0].RequestId] = entries
34 | }
35 | return nil
36 | }
37 |
--------------------------------------------------------------------------------
/database/mock_store.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | type mockStore struct{}
4 |
5 | func NewMockStore() Store {
6 | return &mockStore{}
7 | }
8 |
9 | func (m *mockStore) SaveRequestEntry(entry RequestEntry) error {
10 | return nil
11 | }
12 | func (m *mockStore) SaveRawTxEntries(entries []*EthSendRawTxEntry) error {
13 | return nil
14 | }
15 |
--------------------------------------------------------------------------------
/database/postgres_store.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/jmoiron/sqlx"
8 | _ "github.com/lib/pq"
9 | )
10 |
11 | const (
12 | connTimeOut = 10 * time.Second
13 | )
14 |
15 | type postgresStore struct {
16 | DB *sqlx.DB
17 | }
18 |
19 | func NewPostgresStore(dsn string) *postgresStore {
20 | db := sqlx.MustConnect("postgres", dsn)
21 | db.DB.SetMaxOpenConns(50)
22 | db.DB.SetMaxIdleConns(10)
23 | db.DB.SetConnMaxIdleTime(0)
24 | return &postgresStore{
25 | DB: db,
26 | }
27 | }
28 |
29 | func (d *postgresStore) Close() {
30 | d.DB.Close()
31 | }
32 |
33 | func (d *postgresStore) SaveRequestEntry(entry RequestEntry) error {
34 | query := `INSERT INTO rpc_endpoint_requests
35 | (id, received_at, request_duration_ms, is_batch_request, num_request_in_batch, http_method, http_url, http_query_param, http_response_status, ip_hash, origin, host, error) VALUES (:id, :received_at, :request_duration_ms, :is_batch_request, :num_request_in_batch, :http_method, :http_url, :http_query_param, :http_response_status, :ip_hash, :origin, :host, :error)`
36 | ctx, cancel := context.WithTimeout(context.Background(), connTimeOut)
37 | defer cancel()
38 | _, err := d.DB.NamedExecContext(ctx, query, entry)
39 | return err
40 | }
41 |
42 | func (d *postgresStore) SaveRawTxEntries(entries []*EthSendRawTxEntry) error {
43 | query := `INSERT INTO rpc_endpoint_eth_send_raw_txs (id, request_id, is_on_oafc_list, is_white_hat_bundle_collection, white_hat_bundle_id, is_cancel_tx, needs_front_running_protection, was_sent_to_relay, was_sent_to_mempool, is_blocked, error, error_code, tx_raw, tx_hash, tx_from, tx_to, tx_nonce, tx_data, tx_smart_contract_method,fast) VALUES (:id, :request_id, :is_on_oafc_list, :is_white_hat_bundle_collection, :white_hat_bundle_id, :is_cancel_tx, :needs_front_running_protection, :was_sent_to_relay, :was_sent_to_mempool, :is_blocked, :error, :error_code, :tx_raw, :tx_hash, :tx_from, :tx_to, :tx_nonce, :tx_data, :tx_smart_contract_method, :fast)`
44 | ctx, cancel := context.WithTimeout(context.Background(), connTimeOut)
45 | defer cancel()
46 | _, err := d.DB.NamedExecContext(ctx, query, entries)
47 | return err
48 | }
49 |
--------------------------------------------------------------------------------
/database/store.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | type Store interface {
4 | SaveRequestEntry(entry RequestEntry) error
5 | SaveRawTxEntries(entries []*EthSendRawTxEntry) error
6 | }
7 |
--------------------------------------------------------------------------------
/database/types.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/uuid"
7 | )
8 |
9 | // RequestEntry to store each request
10 | type RequestEntry struct {
11 | Id uuid.UUID `db:"id"`
12 | ReceivedAt time.Time `db:"received_at"`
13 | InsertedAt time.Time `db:"inserted_at"`
14 | RequestDurationMs int64 `db:"request_duration_ms"`
15 | IsBatchRequest bool `db:"is_batch_request"`
16 | NumRequestInBatch int `db:"num_request_in_batch"`
17 | HttpMethod string `db:"http_method"`
18 | HttpUrl string `db:"http_url"`
19 | HttpQueryParam string `db:"http_query_param"`
20 | HttpResponseStatus int `db:"http_response_status"`
21 | Origin string `db:"origin"`
22 | Host string `db:"host"`
23 | Error string `db:"error"`
24 | }
25 |
26 | // EthSendRawTxEntry to store each eth_sendRawTransaction calls
27 | type EthSendRawTxEntry struct {
28 | Id uuid.UUID `db:"id"`
29 | RequestId uuid.UUID `db:"request_id"` // id from RequestEntry table
30 | InsertedAt time.Time `db:"inserted_at"`
31 | IsOnOafcList bool `db:"is_on_oafc_list"`
32 | IsWhiteHatBundleCollection bool `db:"is_white_hat_bundle_collection"`
33 | WhiteHatBundleId string `db:"white_hat_bundle_id"`
34 | IsCancelTx bool `db:"is_cancel_tx"`
35 | NeedsFrontRunningProtection bool `db:"needs_front_running_protection"`
36 | WasSentToRelay bool `db:"was_sent_to_relay"`
37 | WasSentToMempool bool `db:"was_sent_to_mempool"`
38 | IsBlocked bool `db:"is_blocked"`
39 | Error string `db:"error"`
40 | ErrorCode int `db:"error_code"`
41 | TxRaw string `db:"tx_raw"`
42 | TxHash string `db:"tx_hash"`
43 | TxFrom string `db:"tx_from"`
44 | TxTo string `db:"tx_to"`
45 | TxNonce int `db:"tx_nonce"`
46 | TxData string `db:"tx_data"`
47 | TxSmartContractMethod string `db:"tx_smart_contract_method"`
48 | Fast bool `db:"fast"` // If set, fast preference gets called
49 | }
50 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | redis:
4 | image: redis:6.2-alpine
5 | restart: always
6 | ports:
7 | - '6379:6379'
8 | database:
9 | image: postgres:latest
10 | container_name:
11 | pg-rpc-endpoint
12 | environment:
13 | - POSTGRES_PASSWORD=postgres
14 | - POSTGRES_USER=postgres
15 | - POSTGRES_DB=test
16 | healthcheck:
17 | test: [ "CMD", "pg_isready", "-q", "-d", "test", "-U", "postgres" ]
18 | interval: 3s
19 | timeout: 3s
20 | retries: 5
21 | ports:
22 | - "5432:5432"
23 | volumes:
24 | - db-data:/var/lib/postgresql/data
25 | migrate:
26 | image: migrate/migrate
27 | volumes:
28 | - ./sql/psql:/migrations
29 | command: [ "-path", "/migrations", "-database", "postgres://postgres:postgres@pg-rpc-endpoint:5432/test?sslmode=disable", "up" ]
30 | links:
31 | - database
32 | depends_on:
33 | database:
34 | condition: service_healthy
35 | volumes:
36 | db-data:
--------------------------------------------------------------------------------
/docs/metamask.md:
--------------------------------------------------------------------------------
1 | On sending a transaction:
2 |
3 | * MetaMask continuously checks for status with `eth_getTransactionReceipt`, blocking the UI in "tx pending" state.
4 | * The TX never makes it to the mempool if it failed (not enough gas, reverts, etc.)
5 | * MetaMask doesn't know about it and keeps resubmitting transactions (all ~20 sec)
6 |
7 | On `eth_getTransactionReceipt`, we check whether the private tx failed with the private-tx-api. If the TX failed, we return a wrong nonce for `eth_getTransactionCount` to
8 | unblock the MetaMask UI (see also https://github.com/MetaMask/metamask-extension/issues/10914).
9 |
10 | Note: MetaMask needs to receive a wrong nonce exactly 4 times, after which it will put the transaction into "Dropped" state.
11 |
12 | There's a helper to debug the MetaMask behaviour: set the `DEBUG_DONT_SEND_RAWTX` environment variable to `1`, and transactions won't be sent at all. The Metamask fix flow
13 | will be triggered immediately (i.e. next `getTransactionReceipt` call starts the nonce fix without waiting for tx to expire).
14 |
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | To test locally, you can use the environment variable `DEBUG_DONT_SEND_RAWTX` to skip sending transactions.
2 |
3 | This allows you to trigger the Metamask failure flow immediately.
4 |
5 | You can use https://matcha.xyz to easily create transactions that should go to the relay.
6 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/flashbots/rpc-endpoint
2 |
3 | go 1.22.0
4 |
5 | toolchain go1.23.6
6 |
7 | require (
8 | github.com/alicebob/miniredis v2.5.0+incompatible
9 | github.com/cespare/xxhash/v2 v2.3.0
10 | github.com/ethereum/go-ethereum v1.15.2
11 | github.com/go-redis/redis/v8 v8.11.5
12 | github.com/google/uuid v1.3.0
13 | github.com/lib/pq v1.10.7
14 | github.com/metachris/flashbotsrpc v0.7.0
15 | github.com/pkg/errors v0.9.1
16 | github.com/stretchr/testify v1.9.0
17 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
18 | )
19 |
20 | require (
21 | github.com/Microsoft/go-winio v0.6.2 // indirect
22 | github.com/StackExchange/wmi v1.2.1 // indirect
23 | github.com/bits-and-blooms/bitset v1.17.0 // indirect
24 | github.com/consensys/bavard v0.1.22 // indirect
25 | github.com/consensys/gnark-crypto v0.14.0 // indirect
26 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
27 | github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
28 | github.com/davecgh/go-spew v1.1.1 // indirect
29 | github.com/deckarep/golang-set/v2 v2.6.0 // indirect
30 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
31 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
32 | github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
33 | github.com/ethereum/go-verkle v0.2.2 // indirect
34 | github.com/go-ole/go-ole v1.3.0 // indirect
35 | github.com/gorilla/websocket v1.4.2 // indirect
36 | github.com/holiman/uint256 v1.3.2 // indirect
37 | github.com/mmcloughlin/addchain v0.4.0 // indirect
38 | github.com/pmezard/go-difflib v1.0.0 // indirect
39 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
40 | github.com/supranational/blst v0.3.14 // indirect
41 | github.com/tklauser/go-sysconf v0.3.12 // indirect
42 | github.com/tklauser/numcpus v0.6.1 // indirect
43 | golang.org/x/sync v0.10.0 // indirect
44 | gopkg.in/yaml.v3 v3.0.1 // indirect
45 | rsc.io/tmplfunc v0.0.3 // indirect
46 | )
47 |
48 | require (
49 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
50 | github.com/gomodule/redigo v1.8.5 // indirect
51 | github.com/jmoiron/sqlx v1.3.4
52 | github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect
53 | golang.org/x/crypto v0.32.0 // indirect
54 | golang.org/x/sys v0.29.0 // indirect
55 | )
56 |
--------------------------------------------------------------------------------
/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/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
10 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
11 | github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
12 | github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
20 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
21 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
22 | github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
23 | github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
24 | github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
25 | github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
26 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
27 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
28 | github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
29 | github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
30 | github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
31 | github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
32 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
33 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
34 | github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A=
35 | github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
36 | github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E=
37 | github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0=
38 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
39 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
40 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
41 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
42 | github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4=
43 | github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
45 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
47 | github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
48 | github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
49 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
50 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
51 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
52 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
53 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
54 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
55 | github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
56 | github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
57 | github.com/ethereum/go-ethereum v1.15.2 h1:CcU13w1IXOo6FvS60JGCTVcAJ5Ik6RkWoVIvziiHdTU=
58 | github.com/ethereum/go-ethereum v1.15.2/go.mod h1:wGQINJKEVUunCeoaA9C9qKMQ9GEOsEIunzzqTUO2F6Y=
59 | github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
60 | github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
61 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
62 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
63 | github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
64 | github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
65 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
66 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
67 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
68 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
69 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
70 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
71 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
72 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
73 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
74 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
75 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
76 | github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
77 | github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
78 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
79 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
80 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
81 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
82 | github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc=
83 | github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
84 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
85 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
86 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
87 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
88 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
89 | github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
90 | github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
91 | github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
92 | github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
93 | github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
94 | github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
95 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
96 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
97 | github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
98 | github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
99 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
100 | github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
101 | github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
102 | github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
103 | github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
104 | github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
105 | github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
106 | github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
107 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
108 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
109 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
110 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
111 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
112 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
113 | github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
114 | github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
115 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
116 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
117 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
118 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
119 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
120 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
121 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
122 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
123 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
124 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
125 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
126 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
127 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
128 | github.com/metachris/flashbotsrpc v0.7.0 h1:DFpWWgaKE1hLARb5yNvKAiD5pb/VyGU4jrjhTH62ltQ=
129 | github.com/metachris/flashbotsrpc v0.7.0/go.mod h1:UrS249kKA1PK27sf12M6tUxo/M4ayfFrBk7IMFY1TNw=
130 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
131 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
132 | github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
133 | github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
134 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
135 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
136 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
137 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
138 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
139 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
140 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
141 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
142 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
143 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
144 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
145 | github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
146 | github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
147 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
148 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
149 | github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
150 | github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
151 | github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
152 | github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
153 | github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
154 | github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
155 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
156 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
157 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
158 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
159 | github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
160 | github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
161 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
162 | github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
163 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
164 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
165 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
166 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
167 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
168 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
169 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
170 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
171 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
172 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
173 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
174 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
175 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
176 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
177 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
178 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
179 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
180 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
181 | github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
182 | github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
183 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
184 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
185 | github.com/tidwall/gjson v1.8.1 h1:8j5EE9Hrh3l9Od1OIEDAb7IpezNA20UdRngNAj5N0WU=
186 | github.com/tidwall/gjson v1.8.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
187 | github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
188 | github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
189 | github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
190 | github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
191 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
192 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
193 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
194 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
195 | github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
196 | github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
197 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
198 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
199 | github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg=
200 | github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
201 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
202 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
203 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
204 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
205 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
206 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
207 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
208 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
209 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
210 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
211 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
212 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
213 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
214 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
215 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
216 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
217 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
218 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
219 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
220 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
221 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
222 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
223 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
224 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
225 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
226 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
227 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
228 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
229 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
230 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
231 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
232 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
233 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
234 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
235 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
236 |
--------------------------------------------------------------------------------
/server/configuration.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "crypto/ecdsa"
5 |
6 | "github.com/ethereum/go-ethereum/log"
7 | "github.com/flashbots/rpc-endpoint/database"
8 | )
9 |
10 | type Configuration struct {
11 | DB database.Store
12 | DrainAddress string
13 | DrainSeconds int
14 | ListenAddress string
15 | Logger log.Logger
16 | ProxyTimeoutSeconds int
17 | ProxyUrl string
18 | RedisUrl string
19 | RelaySigningKey *ecdsa.PrivateKey
20 | RelayUrl string
21 | Version string
22 | BuilderInfoSource string
23 | FetchInfoInterval int
24 | TTLCacheSeconds int64
25 | DefaultMempoolRPC string
26 | }
27 |
--------------------------------------------------------------------------------
/server/fingerprint.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/http"
7 | "strings"
8 | "time"
9 |
10 | "github.com/cespare/xxhash/v2"
11 | )
12 |
13 | type Fingerprint uint64
14 |
15 | // CIDR parsing code taken from https://github.com/tomasen/realip/blob/master/realip.go
16 | // MIT Licensed, Copyright (c) 2018 SHEN SHENG
17 | var (
18 | privateIpCidrBlocks []*net.IPNet
19 | rfc1918cidrBlockStrings = []string{
20 | "127.0.0.1/8", // localhost
21 | "10.0.0.0/8", // 24-bit block
22 | "172.16.0.0/12", // 20-bit block
23 | "192.168.0.0/16", // 16-bit block
24 | "169.254.0.0/16", // link local address
25 | "::1/128", // localhost IPv6
26 | "fc00::/7", // unique local address IPv6
27 | "fe80::/10", // link local address IPv6
28 | }
29 | )
30 |
31 | func init() {
32 | privateIpCidrBlocks = make([]*net.IPNet, len(rfc1918cidrBlockStrings))
33 | for i, maxCidrBlock := range rfc1918cidrBlockStrings {
34 | _, cidr, _ := net.ParseCIDR(maxCidrBlock)
35 | privateIpCidrBlocks[i] = cidr
36 | }
37 | }
38 |
39 | // FingerprintFromRequest returns a fingerprint for the request based on the X-Forwarded-For header
40 | // and a salted timestamp. The fingerprint is used to identify unique users sessions
41 | // over a short period of time, and thus can be used as a key for rate limiting.
42 | // The seed param is additional entropy to make the fingerprint resistant to rainbow table lookups.
43 | // Without the seed a malicious rpc operator could reverse a client IP address to a fingerprint
44 | // by exhausting all possible IP addresses and comparing the resulting fingerprints.
45 | //
46 | // We considered adding the User-Agent header to the fingerprint, but decided
47 | // against it because it would make the fingerprint gameable. Instead, we
48 | // will salt the fingerprint with the current timestamp rounded to the
49 | // latest hour. This will make sure fingerprints rotate every hour so we
50 | // cannot reasonably track user behavior over time.
51 | func FingerprintFromRequest(req *http.Request, at time.Time, seed uint64) (Fingerprint, error) {
52 | // X-Forwarded-For header contains a comma-separated list of IP addresses when
53 | // the request has been forwarded through multiple proxies. For example:
54 | //
55 | // X-Forwarded-For: 2600:8802:4700:bee:d13c:c7fb:8e0f:84ff, 172.70.210.100
56 | xff, err := getXForwardedForIP(req)
57 | if err != nil {
58 | return 0, err
59 | }
60 | if at.IsZero() {
61 | at = time.Now().UTC()
62 | }
63 | salt := uint64(at.Truncate(time.Hour).Unix()) ^ seed
64 | fingerprintPreimage := fmt.Sprintf("XFF:%s|SALT:%d", xff, salt)
65 | return Fingerprint(xxhash.Sum64String(fingerprintPreimage)), nil
66 | }
67 |
68 | func (f Fingerprint) ToIPv6() net.IP {
69 | // We'll generate a "fake" IPv6 address based on the fingerprint
70 | // We'll use the RFC 3849 documentation prefix (2001:DB8::/32) for this.
71 | // https://datatracker.ietf.org/doc/html/rfc3849
72 | addr := [16]byte{
73 | 0: 0x20,
74 | 1: 0x01,
75 | 2: 0x0d,
76 | 3: 0xb8,
77 | 8: byte(f >> 56),
78 | 9: byte(f >> 48),
79 | 10: byte(f >> 40),
80 | 11: byte(f >> 32),
81 | 12: byte(f >> 24),
82 | 13: byte(f >> 16),
83 | 14: byte(f >> 8),
84 | 15: byte(f),
85 | }
86 | return addr[:]
87 | }
88 |
89 | func getXForwardedForIP(r *http.Request) (string, error) {
90 | // gets the left-most non-private IP in the X-Forwarded-For header
91 | xff := r.Header.Get("X-Forwarded-For")
92 | if xff == "" {
93 | return "", fmt.Errorf("no X-Forwarded-For header")
94 | }
95 | ips := strings.Split(xff, ",")
96 | for _, ip := range ips {
97 | if !isPrivateIP(strings.TrimSpace(ip)) {
98 | return ip, nil
99 | }
100 | }
101 | return "", fmt.Errorf("no non-private IP in X-Forwarded-For header")
102 | }
103 |
104 | func isPrivateIP(ip string) bool {
105 | // compare ip to RFC-1918 known private IP ranges
106 | // https://en.wikipedia.org/wiki/Private_network
107 | ipAddr := net.ParseIP(ip)
108 | if ipAddr == nil {
109 | return false
110 | }
111 |
112 | for _, cidr := range privateIpCidrBlocks {
113 | if cidr.Contains(ipAddr) {
114 | return true
115 | }
116 | }
117 | return false
118 | }
119 |
--------------------------------------------------------------------------------
/server/fingerprint_test.go:
--------------------------------------------------------------------------------
1 | package server_test
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/require"
10 |
11 | "github.com/flashbots/rpc-endpoint/server"
12 | )
13 |
14 | func TestFingerprint_ToIPv6(t *testing.T) {
15 | seed := uint64(0x4242420000004242)
16 | req, err := http.NewRequest("GET", "http://example.com", nil)
17 | require.NoError(t, err)
18 |
19 | req.Header.Set("X-Forwarded-For", "2600:8802:4700:bee:d13c:c7fb:8e0f:84ff, 172.70.210.100")
20 | fingerprint1, err := server.FingerprintFromRequest(
21 | req,
22 | time.Date(2022, 1, 1, 1, 2, 3, 4, time.UTC),
23 | seed,
24 | )
25 | require.NoError(t, err)
26 | require.True(t, strings.HasPrefix(fingerprint1.ToIPv6().String(), "2001:db8::"))
27 |
28 | fingerprint2, err := server.FingerprintFromRequest(
29 | req,
30 | time.Date(2022, 1, 1, 2, 3, 4, 5, time.UTC),
31 | seed,
32 | )
33 | require.NoError(t, err)
34 | require.True(t, strings.HasPrefix(fingerprint2.ToIPv6().String(), "2001:db8::"))
35 |
36 | require.NotEqual(t, fingerprint1, fingerprint2)
37 | }
38 |
--------------------------------------------------------------------------------
/server/http_client.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/ethereum/go-ethereum/log"
10 | )
11 |
12 | type RPCProxyClient interface {
13 | ProxyRequest(body []byte) (*http.Response, error)
14 | }
15 |
16 | type rpcProxyClient struct {
17 | logger log.Logger
18 | httpClient http.Client
19 | proxyURL string
20 | fingerprint Fingerprint
21 | }
22 |
23 | func NewRPCProxyClient(logger log.Logger, proxyURL string, timeoutSeconds int, fingerprint Fingerprint) RPCProxyClient {
24 | return &rpcProxyClient{
25 | logger: logger,
26 | httpClient: http.Client{Timeout: time.Second * time.Duration(timeoutSeconds)},
27 | proxyURL: proxyURL,
28 | fingerprint: fingerprint,
29 | }
30 | }
31 |
32 | // ProxyRequest using http client to make http post request
33 | func (n *rpcProxyClient) ProxyRequest(body []byte) (*http.Response, error) {
34 | req, err := http.NewRequest(http.MethodPost, n.proxyURL, bytes.NewBuffer(body))
35 | if err != nil {
36 | return nil, err
37 | }
38 | if n.fingerprint != 0 {
39 | req.Header.Set(
40 | "X-Forwarded-For",
41 | n.fingerprint.ToIPv6().String(),
42 | )
43 | }
44 | req.Header.Set("Accept", "application/json")
45 | req.Header.Set("Content-Type", "application/json")
46 | req.Header.Set("Content-Length", strconv.Itoa(len(body)))
47 | start := time.Now()
48 | res, err := n.httpClient.Do(req)
49 | n.logger.Info("[ProxyRequest] completed", "timeNeeded", time.Since(start))
50 | return res, err
51 | }
52 |
--------------------------------------------------------------------------------
/server/ofacblacklist.go:
--------------------------------------------------------------------------------
1 | // OFAC banned addresses
2 | package server
3 |
4 | import "strings"
5 |
6 | var ofacBlacklist = map[string]bool{}
7 |
8 | func isOnOFACList(address string) bool {
9 | addrs := strings.ToLower(address)
10 | return ofacBlacklist[addrs]
11 | }
12 |
--------------------------------------------------------------------------------
/server/ofacblacklist_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import "testing"
4 |
5 | func Test_isOnOFACList(t *testing.T) {
6 | ofacBlacklist["0x53b6936513e738f44fb50d2b9476730c0ab3bfc1"] = true
7 |
8 | tests := map[string]struct {
9 | address string
10 | want bool
11 | }{
12 | "Check different cases": {
13 | address: "0x53b6936513e738f44FB50d2b9476730c0ab3bfc1",
14 | want: true,
15 | },
16 | "Check upper cases": {
17 | address: "0X53B6936513E738F44FB50D2B9476730C0AB3BFC1",
18 | want: true,
19 | },
20 | "Check unknow": {
21 | address: "0X5",
22 | want: false,
23 | },
24 | }
25 | for testName, testCase := range tests {
26 | t.Run(testName, func(t *testing.T) {
27 | if got := isOnOFACList(testCase.address); got != testCase.want {
28 | t.Errorf("isOnOFACList() = %v, want %v", got, testCase.want)
29 | }
30 | })
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/redisstate.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | "github.com/go-redis/redis/v8"
11 | "github.com/pkg/errors"
12 | )
13 |
14 | var RedisPrefix = "rpc-endpoint:"
15 |
16 | // Enable lookup of timeSentToRelay by txHash
17 | var RedisPrefixTxSentToRelay = RedisPrefix + "tx-sent-to-relay:"
18 | var RedisExpiryTxSentToRelay = 24 * time.Hour // 1 day
19 |
20 | // Enable lookup of txHash by txFrom+nonce (only if sent to relay)
21 | var RedisPrefixTxHashForSenderAndNonce = RedisPrefix + "txsender-and-nonce-to-txhash:"
22 | var RedisExpiryTxHashForSenderAndNonce = 24 * time.Hour // 1 day
23 |
24 | // nonce-fix of an account (with number of times sent)
25 | var RedisPrefixNonceFixForAccount = RedisPrefix + "txsender-with-nonce-fix:"
26 | var RedisExpiryNonceFixForAccount = 24 * time.Hour // 1 day
27 |
28 | // Enable lookup of txFrom by txHash
29 | var RedisPrefixSenderOfTxHash = RedisPrefix + "txsender-of-txhash:"
30 | var RedisExpirySenderOfTxHash = 24 * time.Hour // 1 day
31 |
32 | // Enable lookup of txNonce by txHash
33 | var RedisPrefixNonceOfTxHash = RedisPrefix + "txnonce-of-txhash:"
34 | var RedisExpiryNonceOfTxHash = 24 * time.Hour // 1 day
35 |
36 | // Remember nonce of pending user tx
37 | var RedisPrefixSenderMaxNonce = RedisPrefix + "txsender-pending-max-nonce:"
38 | var RedisExpirySenderMaxNonce = 280 * time.Second //weird time to be a little less than 5 minute default blockrange
39 |
40 | // Enable lookup of bundle txs by bundleId
41 | var RedisPrefixWhitehatBundleTransactions = RedisPrefix + "tx-for-whitehat-bundle:"
42 | var RedisExpiryWhitehatBundleTransactions = 24 * time.Hour // 1 day
43 |
44 | // Enable lookup of bundle txs by bundleId
45 | var RedisPrefixBlockedTxHash = RedisPrefix + "blocked-tx-hash:"
46 | var RedisExpiryBlockedTxHash = 24 * time.Hour // 1 day
47 |
48 | // // Enable lookup of last privateTransaction-txHash sent by txFrom
49 | // var RedisPrefixLastPrivTxHashOfAccount = RedisPrefix + "last-txhash-of-txsender:"
50 | // var RedisExpiryLastPrivTxHashOfAccount = time.Duration(24 * time.Hour) // 1 day
51 |
52 | func RedisKeyTxSentToRelay(txHash string) string {
53 | return RedisPrefixTxSentToRelay + strings.ToLower(txHash)
54 | }
55 |
56 | func RedisKeyTxHashForSenderAndNonce(txFrom string, nonce uint64) string {
57 | return fmt.Sprintf("%s%s_%d", RedisPrefixTxHashForSenderAndNonce, strings.ToLower(txFrom), nonce)
58 | }
59 |
60 | func RedisKeyNonceFixForAccount(txFrom string) string {
61 | return RedisPrefixNonceFixForAccount + strings.ToLower(txFrom)
62 | }
63 |
64 | func RedisKeySenderOfTxHash(txHash string) string {
65 | return RedisPrefixSenderOfTxHash + strings.ToLower(txHash)
66 | }
67 |
68 | func RedisKeyNonceOfTxHash(txHash string) string {
69 | return RedisPrefixNonceOfTxHash + strings.ToLower(txHash)
70 | }
71 |
72 | func RedisKeySenderMaxNonce(txFrom string) string {
73 | return RedisPrefixSenderMaxNonce + strings.ToLower(txFrom)
74 | }
75 |
76 | func RedisKeyWhitehatBundleTransactions(bundleId string) string {
77 | return RedisPrefixWhitehatBundleTransactions + strings.ToLower(bundleId)
78 | }
79 |
80 | func RedisKeyBlockedTxHash(txHash string) string {
81 | return RedisPrefixBlockedTxHash + strings.ToLower(txHash)
82 | }
83 |
84 | // func RedisKeyLastPrivTxHashOfAccount(txFrom string) string {
85 | // return RedisPrefixLastPrivTxHashOfAccount + strings.ToLower(txFrom)
86 | // }
87 |
88 | type RedisState struct {
89 | RedisClient *redis.Client
90 | }
91 |
92 | func NewRedisState(redisUrl string) (*RedisState, error) {
93 | // Setup redis client and check connection
94 | redisClient := redis.NewClient(&redis.Options{Addr: redisUrl})
95 |
96 | // Try to get a key to see if there's an error with the connection
97 | if err := redisClient.Get(context.Background(), "somekey").Err(); err != nil && err != redis.Nil {
98 | return nil, errors.Wrap(err, "redis init error")
99 | }
100 |
101 | // Create and return the RedisState
102 | return &RedisState{
103 | RedisClient: redisClient,
104 | }, nil
105 | }
106 |
107 | // Enable lookup of timeSentToRelay by txHash
108 | func (s *RedisState) SetTxSentToRelay(txHash string) error {
109 | key := RedisKeyTxSentToRelay(txHash)
110 | err := s.RedisClient.Set(context.Background(), key, Now().UTC().Unix(), RedisExpiryTxSentToRelay).Err()
111 | return err
112 | }
113 |
114 | func (s *RedisState) GetTxSentToRelay(txHash string) (timeSent time.Time, found bool, err error) {
115 | key := RedisKeyTxSentToRelay(txHash)
116 | val, err := s.RedisClient.Get(context.Background(), key).Result()
117 | if err == redis.Nil {
118 | return time.Time{}, false, nil // just not found
119 | } else if err != nil {
120 | return time.Time{}, false, err // found but error
121 | }
122 |
123 | timestampInt, err := strconv.Atoi(val)
124 | if err != nil {
125 | return time.Time{}, true, err // found but error
126 | }
127 |
128 | t := time.Unix(int64(timestampInt), 0)
129 | return t, true, nil
130 | }
131 |
132 | // Enable lookup of txHash by txFrom+nonce
133 | func (s *RedisState) SetTxHashForSenderAndNonce(txFrom string, nonce uint64, txHash string) error {
134 | key := RedisKeyTxHashForSenderAndNonce(txFrom, nonce)
135 | err := s.RedisClient.Set(context.Background(), key, strings.ToLower(txHash), RedisExpiryTxHashForSenderAndNonce).Err()
136 | return err
137 | }
138 |
139 | func (s *RedisState) GetTxHashForSenderAndNonce(txFrom string, nonce uint64) (txHash string, found bool, err error) {
140 | key := RedisKeyTxHashForSenderAndNonce(txFrom, nonce)
141 | txHash, err = s.RedisClient.Get(context.Background(), key).Result()
142 | if err == redis.Nil {
143 | return "", false, nil // not found
144 | } else if err != nil {
145 | return "", false, err
146 | }
147 |
148 | return txHash, true, nil
149 | }
150 |
151 | // nonce-fix per account
152 | func (s *RedisState) SetNonceFixForAccount(txFrom string, numTimesSent uint64) error {
153 | key := RedisKeyNonceFixForAccount(txFrom)
154 | err := s.RedisClient.Set(context.Background(), key, numTimesSent, RedisExpiryNonceFixForAccount).Err()
155 | return err
156 | }
157 |
158 | func (s *RedisState) DelNonceFixForAccount(txFrom string) error {
159 | key := RedisKeyNonceFixForAccount(txFrom)
160 | err := s.RedisClient.Del(context.Background(), key).Err()
161 | return err
162 | }
163 |
164 | func (s *RedisState) GetNonceFixForAccount(txFrom string) (numTimesSent uint64, found bool, err error) {
165 | key := RedisKeyNonceFixForAccount(txFrom)
166 | val, err := s.RedisClient.Get(context.Background(), key).Result()
167 | if err == redis.Nil {
168 | return 0, false, nil // not found
169 | } else if err != nil {
170 | return 0, false, err
171 | }
172 |
173 | numTimesSent, err = strconv.ParseUint(val, 10, 64)
174 | if err != nil {
175 | return 0, true, err
176 | }
177 | return numTimesSent, true, nil
178 | }
179 |
180 | // Enable lookup of txFrom and nonce by txHash
181 | func (s *RedisState) SetSenderAndNonceOfTxHash(txHash string, txFrom string, txNonce uint64) error {
182 | key := RedisKeySenderOfTxHash(txHash)
183 | err := s.RedisClient.Set(context.Background(), key, strings.ToLower(txFrom), RedisExpirySenderOfTxHash).Err()
184 | if err != nil {
185 | return err
186 | }
187 | key = RedisKeyNonceOfTxHash(txHash)
188 | err = s.RedisClient.Set(context.Background(), key, txNonce, RedisExpiryNonceOfTxHash).Err()
189 | return err
190 | }
191 |
192 | func (s *RedisState) GetSenderOfTxHash(txHash string) (txSender string, found bool, err error) {
193 | key := RedisKeySenderOfTxHash(txHash)
194 | txSender, err = s.RedisClient.Get(context.Background(), key).Result()
195 | if err == redis.Nil { // not found
196 | return "", false, nil
197 | } else if err != nil {
198 | return "", false, err
199 | }
200 |
201 | return strings.ToLower(txSender), true, nil
202 | }
203 |
204 | func (s *RedisState) GetNonceOfTxHash(txHash string) (txNonce uint64, found bool, err error) {
205 | key := RedisKeyNonceOfTxHash(txHash)
206 | val, err := s.RedisClient.Get(context.Background(), key).Result()
207 | if err == redis.Nil {
208 | return 0, false, nil
209 | }
210 |
211 | txNonce, err = strconv.ParseUint(val, 10, 64)
212 | if err != nil {
213 | return 0, true, err
214 | }
215 | return txNonce, true, nil
216 | }
217 |
218 | // Enable lookup of tx bundles by bundle ID
219 | func (s *RedisState) AddTxToWhitehatBundle(bundleId string, signedTx string) error {
220 | key := RedisKeyWhitehatBundleTransactions(bundleId)
221 |
222 | // Check if item already exists
223 | txs, err := s.GetWhitehatBundleTx(bundleId)
224 | if err == nil {
225 | for _, tx := range txs {
226 | if signedTx == tx {
227 | return nil
228 | }
229 | }
230 | }
231 |
232 | // Add item
233 | err = s.RedisClient.LPush(context.Background(), key, signedTx).Err()
234 | if err != nil {
235 | return err
236 | }
237 |
238 | // Set expiry
239 | err = s.RedisClient.Expire(context.Background(), key, RedisExpiryWhitehatBundleTransactions).Err()
240 | if err != nil {
241 | return err
242 | }
243 |
244 | // Limit to 15 entries
245 | err = s.RedisClient.LTrim(context.Background(), key, 0, 15).Err()
246 | return err
247 | }
248 |
249 | func (s *RedisState) GetWhitehatBundleTx(bundleId string) ([]string, error) {
250 | key := RedisKeyWhitehatBundleTransactions(bundleId)
251 | return s.RedisClient.LRange(context.Background(), key, 0, -1).Result()
252 | }
253 |
254 | func (s *RedisState) DelWhitehatBundleTx(bundleId string) error {
255 | key := RedisKeyWhitehatBundleTransactions(bundleId)
256 | return s.RedisClient.Del(context.Background(), key).Err()
257 | }
258 |
259 | //
260 | // Enable lookup of last txHash sent by txFrom
261 | //
262 | // func (s *RedisState) SetLastPrivTxHashOfAccount(txFrom string, txHash string) error {
263 | // key := RedisKeyLastPrivTxHashOfAccount(txFrom)
264 | // err := s.RedisClient.Set(context.Background(), key, strings.ToLower(txHash), RedisExpiryLastPrivTxHashOfAccount).Err()
265 | // return err
266 | // }
267 |
268 | // func (s *RedisState) GetLastPrivTxHashOfAccount(txFrom string) (txHash string, found bool, err error) {
269 | // key := RedisKeyLastPrivTxHashOfAccount(txFrom)
270 | // txHash, err = s.RedisClient.Get(context.Background(), key).Result()
271 | // if err == redis.Nil { // not found
272 | // return "", false, nil
273 | // } else if err != nil {
274 | // return "", false, err
275 | // }
276 |
277 | // return strings.ToLower(txHash), true, nil
278 | // }
279 |
280 | func (s *RedisState) SetSenderMaxNonce(txFrom string, nonce uint64, blockRange int) error {
281 | prevMaxNonce, found, err := s.GetSenderMaxNonce(txFrom)
282 | if err != nil {
283 | return err
284 | }
285 |
286 | // Do nothing if current nonce is not higher than already existing
287 | if found && prevMaxNonce >= nonce {
288 | return nil
289 | }
290 |
291 | expiry := RedisExpirySenderMaxNonce
292 | if blockRange > 0 {
293 | expiry = 12 * time.Duration(blockRange) * time.Second
294 | }
295 |
296 | key := RedisKeySenderMaxNonce(txFrom)
297 | err = s.RedisClient.Set(context.Background(), key, nonce, expiry).Err()
298 | return err
299 | }
300 |
301 | func (s *RedisState) GetSenderMaxNonce(txFrom string) (senderMaxNonce uint64, found bool, err error) {
302 | key := RedisKeySenderMaxNonce(txFrom)
303 | val, err := s.RedisClient.Get(context.Background(), key).Result()
304 | if err == redis.Nil {
305 | return 0, false, nil // not found
306 | } else if err != nil {
307 | return 0, false, err
308 | }
309 |
310 | senderMaxNonce, err = strconv.ParseUint(val, 10, 64)
311 | if err != nil {
312 | return 0, true, err
313 | }
314 | return senderMaxNonce, true, nil
315 | }
316 |
317 | func (s *RedisState) DelSenderMaxNonce(txFrom string) error {
318 | key := RedisKeySenderMaxNonce(txFrom)
319 | return s.RedisClient.Del(context.Background(), key).Err()
320 | }
321 |
322 | // Block transactions, with a specific return value (eg. "nonce too low")
323 | func (s *RedisState) SetBlockedTxHash(txHash string, returnValue string) error {
324 | key := RedisKeyBlockedTxHash(txHash)
325 | err := s.RedisClient.Set(context.Background(), key, returnValue, RedisExpiryBlockedTxHash).Err()
326 | return err
327 | }
328 |
329 | func (s *RedisState) GetBlockedTxHash(txHash string) (returnValue string, found bool, err error) {
330 | key := RedisKeyBlockedTxHash(txHash)
331 | returnValue, err = s.RedisClient.Get(context.Background(), key).Result()
332 | if err == redis.Nil { // not found
333 | return "", false, nil
334 | } else if err != nil {
335 | return "", false, err
336 | }
337 |
338 | return returnValue, true, nil
339 | }
340 |
--------------------------------------------------------------------------------
/server/redisstate_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "github.com/alicebob/miniredis"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | var redisServer *miniredis.Miniredis
14 | var redisState *RedisState
15 |
16 | func resetRedis() {
17 | var err error
18 | if redisServer != nil {
19 | redisServer.Close()
20 | }
21 |
22 | redisServer, err = miniredis.Run()
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | redisState, err = NewRedisState(redisServer.Addr())
28 | // redisState, err = server.NewRedisState("localhost:6379")
29 | if err != nil {
30 | panic(err)
31 | }
32 | }
33 |
34 | func TestRedisStateSetup(t *testing.T) {
35 | var err error
36 | redisState, err = NewRedisState("localhost:18279")
37 | require.NotNil(t, err, err)
38 | }
39 |
40 | func TestTxSentToRelay(t *testing.T) {
41 | var err error
42 | resetRedis()
43 |
44 | timeBeforeSet := time.Now()
45 | err = redisState.SetTxSentToRelay("foo")
46 | require.Nil(t, err, err)
47 |
48 | timeSent, found, err := redisState.GetTxSentToRelay("foo")
49 | require.Nil(t, err, err)
50 | require.True(t, found)
51 |
52 | // Returned time should be after time set and within 1 second of current time
53 | require.True(t, time.Since(timeSent) >= time.Since(timeBeforeSet))
54 | require.True(t, time.Since(timeSent) < time.Second)
55 |
56 | // Invalid key should return found: false but no error
57 | _, found, err = redisState.GetTxSentToRelay("XXX")
58 | require.Nil(t, err, err)
59 | require.False(t, found)
60 |
61 | // After resetting redis, we shouldn't be able to find the key
62 | resetRedis()
63 | _, found, err = redisState.GetTxSentToRelay("foo")
64 | require.Nil(t, err, err)
65 | require.False(t, found)
66 | }
67 |
68 | func TestTxHashForSenderAndNonce(t *testing.T) {
69 | var err error
70 | resetRedis()
71 |
72 | txFrom := "0x0Sender"
73 | nonce := uint64(1337)
74 | txHash := "0x0TxHash"
75 |
76 | // Ensure key is correct
77 | key := RedisKeyTxHashForSenderAndNonce(txFrom, nonce)
78 | expectedKey := fmt.Sprintf("%s%s_%d", RedisPrefixTxHashForSenderAndNonce, strings.ToLower(txFrom), nonce)
79 | require.Equal(t, expectedKey, key)
80 |
81 | // Get before set: should return not found
82 | txHashFromRedis, found, err := redisState.GetTxHashForSenderAndNonce(txFrom, nonce)
83 | require.Nil(t, err, err)
84 | require.False(t, found)
85 | require.Equal(t, "", txHashFromRedis)
86 |
87 | // Set
88 | err = redisState.SetTxHashForSenderAndNonce(txFrom, nonce, txHash)
89 | require.Nil(t, err, err)
90 |
91 | // Get
92 | txHashFromRedis, found, err = redisState.GetTxHashForSenderAndNonce(txFrom, nonce)
93 | require.Nil(t, err, err)
94 | require.True(t, found)
95 |
96 | // The txHash is stored lowercased, so it doesn't match directly
97 | require.NotEqual(t, txHash, txHashFromRedis)
98 | require.Equal(t, strings.ToLower(txHash), txHashFromRedis)
99 | }
100 |
101 | func TestNonceFixForAccount(t *testing.T) {
102 | var err error
103 | resetRedis()
104 |
105 | txFrom := "0x0Sender"
106 |
107 | numTimesSent, found, err := redisState.GetNonceFixForAccount(txFrom)
108 | require.Nil(t, err, err)
109 | require.False(t, found)
110 | require.Equal(t, uint64(0), numTimesSent)
111 |
112 | err = redisState.SetNonceFixForAccount(txFrom, 0)
113 | require.Nil(t, err, err)
114 |
115 | numTimesSent, found, err = redisState.GetNonceFixForAccount(txFrom)
116 | require.Nil(t, err, err)
117 | require.True(t, found)
118 | require.Equal(t, uint64(0), numTimesSent)
119 |
120 | err = redisState.DelNonceFixForAccount(txFrom)
121 | require.Nil(t, err, err)
122 |
123 | numTimesSent, found, err = redisState.GetNonceFixForAccount(txFrom)
124 | require.Nil(t, err, err)
125 | require.False(t, found)
126 | require.Equal(t, uint64(0), numTimesSent)
127 |
128 | err = redisState.SetNonceFixForAccount(txFrom, 17)
129 | require.Nil(t, err, err)
130 |
131 | numTimesSent, found, err = redisState.GetNonceFixForAccount(txFrom)
132 | require.Nil(t, err, err)
133 | require.True(t, found)
134 | require.Equal(t, uint64(17), numTimesSent)
135 |
136 | // Ensure it matches txFrom case-insensitive
137 | numTimesSent, found, err = redisState.GetNonceFixForAccount(strings.ToUpper(txFrom))
138 | require.Nil(t, err, err)
139 | require.True(t, found)
140 | require.Equal(t, uint64(17), numTimesSent)
141 | }
142 |
143 | func TestSenderOfTxHash(t *testing.T) {
144 | var err error
145 | resetRedis()
146 |
147 | txFrom := "0x0Sender"
148 | txHash := "0xDeadBeef"
149 | txNonce := uint64(1337)
150 |
151 | val, found, err := redisState.GetSenderOfTxHash(txHash)
152 | require.Nil(t, err, err)
153 | require.False(t, found)
154 | require.Equal(t, "", val)
155 |
156 | err = redisState.SetSenderAndNonceOfTxHash(txHash, txFrom, txNonce)
157 | require.Nil(t, err, err)
158 |
159 | val, found, err = redisState.GetSenderOfTxHash(txHash)
160 | require.Nil(t, err, err)
161 | require.True(t, found)
162 | require.Equal(t, strings.ToLower(txFrom), val)
163 | }
164 |
165 | func TestSenderMaxNonce(t *testing.T) {
166 | var err error
167 | resetRedis()
168 |
169 | txFrom := "0x0Sender"
170 |
171 | val, found, err := redisState.GetSenderMaxNonce(txFrom)
172 | require.Nil(t, err, err)
173 | require.False(t, found)
174 | require.Equal(t, uint64(0), val)
175 |
176 | err = redisState.SetSenderMaxNonce(txFrom, 17, 0)
177 | require.Nil(t, err, err)
178 |
179 | val, found, err = redisState.GetSenderMaxNonce(txFrom)
180 | require.Nil(t, err, err)
181 | require.True(t, found)
182 | require.Equal(t, uint64(17), val)
183 |
184 | err = redisState.SetSenderMaxNonce(txFrom, 16, 10)
185 | require.Nil(t, err, err)
186 |
187 | val, found, err = redisState.GetSenderMaxNonce(txFrom)
188 | require.Nil(t, err, err)
189 | require.True(t, found)
190 | require.Equal(t, uint64(17), val)
191 |
192 | err = redisState.SetSenderMaxNonce(txFrom, 18, 0)
193 | require.Nil(t, err, err)
194 |
195 | val, found, err = redisState.GetSenderMaxNonce(txFrom)
196 | require.Nil(t, err, err)
197 | require.True(t, found)
198 | require.Equal(t, uint64(18), val)
199 | }
200 |
201 | func TestWhitehatTx(t *testing.T) {
202 | resetRedis()
203 | bundleId := "123"
204 |
205 | // get (empty)
206 | txs, err := redisState.GetWhitehatBundleTx(bundleId)
207 | require.Nil(t, err, err)
208 | require.Equal(t, 0, len(txs))
209 |
210 | // add #1
211 | tx1 := "0xa12345"
212 | tx2 := "0xb123456"
213 | err = redisState.AddTxToWhitehatBundle(bundleId, tx1)
214 | require.Nil(t, err, err)
215 |
216 | txs, err = redisState.GetWhitehatBundleTx(bundleId)
217 | require.Nil(t, err, err)
218 | require.Equal(t, 1, len(txs))
219 |
220 | err = redisState.AddTxToWhitehatBundle(bundleId, tx1)
221 | require.Nil(t, err, err)
222 | err = redisState.AddTxToWhitehatBundle(bundleId, tx2)
223 | require.Nil(t, err, err)
224 |
225 | txs, err = redisState.GetWhitehatBundleTx(bundleId)
226 | require.Nil(t, err, err)
227 | require.Equal(t, 2, len(txs))
228 | require.Equal(t, tx2, txs[0])
229 | require.Equal(t, tx1, txs[1])
230 |
231 | err = redisState.DelWhitehatBundleTx(bundleId)
232 | require.Nil(t, err, err)
233 |
234 | txs, err = redisState.GetWhitehatBundleTx(bundleId)
235 | require.Nil(t, err, err)
236 | require.Equal(t, 0, len(txs))
237 | }
238 |
239 | func TestBlockedTxHash(t *testing.T) {
240 | resetRedis()
241 | txHash := "0x123"
242 | retVal := "foo"
243 |
244 | err := redisState.SetBlockedTxHash(txHash, retVal)
245 | require.Nil(t, err, err)
246 |
247 | val, found, err := redisState.GetBlockedTxHash(txHash)
248 | require.Nil(t, err, err)
249 | require.True(t, found)
250 | require.Equal(t, retVal, val)
251 | }
252 |
--------------------------------------------------------------------------------
/server/request_handler.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "encoding/json"
6 | "io"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/ethereum/go-ethereum/ethclient"
11 | "github.com/ethereum/go-ethereum/log"
12 | "github.com/google/uuid"
13 | "golang.org/x/exp/rand"
14 |
15 | "github.com/flashbots/rpc-endpoint/application"
16 | "github.com/flashbots/rpc-endpoint/database"
17 | "github.com/flashbots/rpc-endpoint/types"
18 | )
19 |
20 | var seed uint64 = uint64(rand.Int63())
21 |
22 | // RPC request handler for a single/ batch JSON-RPC request
23 | type RpcRequestHandler struct {
24 | respw *http.ResponseWriter
25 | req *http.Request
26 | logger log.Logger
27 | timeStarted time.Time
28 | defaultProxyUrl string
29 | proxyTimeoutSeconds int
30 | relaySigningKey *ecdsa.PrivateKey
31 | relayUrl string
32 | uid uuid.UUID
33 | requestRecord *requestRecord
34 | builderNames []string
35 | chainID []byte
36 | rpcCache *application.RpcCache
37 | defaultEthClient *ethclient.Client
38 | }
39 |
40 | func NewRpcRequestHandler(
41 | logger log.Logger,
42 | respw *http.ResponseWriter,
43 | req *http.Request,
44 | proxyUrl string,
45 | proxyTimeoutSeconds int,
46 | relaySigningKey *ecdsa.PrivateKey,
47 | relayUrl string,
48 | db database.Store,
49 | builderNames []string,
50 | chainID []byte,
51 | rpcCache *application.RpcCache,
52 | defaultEthClient *ethclient.Client,
53 | ) *RpcRequestHandler {
54 | return &RpcRequestHandler{
55 | logger: logger,
56 | respw: respw,
57 | req: req,
58 | timeStarted: Now(),
59 | defaultProxyUrl: proxyUrl,
60 | proxyTimeoutSeconds: proxyTimeoutSeconds,
61 | relaySigningKey: relaySigningKey,
62 | relayUrl: relayUrl,
63 | uid: uuid.New(),
64 | requestRecord: NewRequestRecord(db),
65 | builderNames: builderNames,
66 | chainID: chainID,
67 | rpcCache: rpcCache,
68 | defaultEthClient: defaultEthClient,
69 | }
70 | }
71 |
72 | // nolint
73 | func (r *RpcRequestHandler) process() {
74 | r.logger = r.logger.New("uid", r.uid)
75 | r.logger.Info("[process] POST request received")
76 |
77 | defer r.finishRequest()
78 | r.requestRecord.requestEntry.ReceivedAt = r.timeStarted
79 | r.requestRecord.requestEntry.Id = r.uid
80 | r.requestRecord.UpdateRequestEntry(r.req, http.StatusOK, "")
81 |
82 | whitehatBundleId := r.req.URL.Query().Get("bundle")
83 | isWhitehatBundleCollection := whitehatBundleId != ""
84 |
85 | origin := r.req.Header.Get("Origin")
86 | referer := r.req.Header.Get("Referer")
87 |
88 | // If users specify a proxy url in their rpc endpoint they can have their requests proxied to that endpoint instead of Infura
89 | // e.g. https://rpc.flashbots.net?url=http://RPC-ENDPOINT.COM
90 | customProxyUrl, ok := r.req.URL.Query()["url"]
91 | if ok && len(customProxyUrl[0]) > 1 {
92 | r.defaultProxyUrl = customProxyUrl[0]
93 | r.logger.Info("[process] Using custom url", "url", r.defaultProxyUrl)
94 | }
95 |
96 | // Decode request JSON RPC
97 | defer r.req.Body.Close()
98 | body, err := io.ReadAll(r.req.Body)
99 | if err != nil {
100 | r.requestRecord.UpdateRequestEntry(r.req, http.StatusBadRequest, err.Error())
101 | r.logger.Error("[process] Failed to read request body", "error", err)
102 | (*r.respw).WriteHeader(http.StatusBadRequest)
103 | return
104 | }
105 |
106 | if len(body) == 0 {
107 | r.requestRecord.UpdateRequestEntry(r.req, http.StatusBadRequest, "empty request body")
108 | (*r.respw).WriteHeader(http.StatusBadRequest)
109 | return
110 | }
111 |
112 | fingerprint, _ := FingerprintFromRequest(r.req, time.Now(), seed)
113 | if fingerprint != 0 {
114 | r.logger = r.logger.New("fingerprint", fingerprint.ToIPv6().String())
115 | }
116 |
117 | // create rpc proxy client for making proxy request
118 | client := NewRPCProxyClient(r.logger, r.defaultProxyUrl, r.proxyTimeoutSeconds, fingerprint)
119 |
120 | r.requestRecord.UpdateRequestEntry(r.req, http.StatusOK, "") // Data analytics
121 |
122 | // Parse JSON RPC payload
123 | var jsonReq *types.JsonRpcRequest
124 | if err = json.Unmarshal(body, &jsonReq); err != nil {
125 | r.logger.Warn("[process] Parse payload", "error", err)
126 | (*r.respw).WriteHeader(http.StatusBadRequest)
127 | return
128 | }
129 |
130 | // mev-share parameters
131 | urlParams, err := ExtractParametersFromUrl(r.req.URL, r.builderNames)
132 | if err != nil {
133 | r.logger.Warn("[process] Invalid auction preference", "error", err)
134 | res := AuctionPreferenceErrorToJSONRPCResponse(jsonReq, err)
135 | r._writeRpcResponse(res)
136 | return
137 | }
138 | r.logger = r.logger.New("rpc_method", jsonReq.Method)
139 |
140 | // Process single request
141 | r.processRequest(client, jsonReq, origin, referer, isWhitehatBundleCollection, whitehatBundleId, urlParams, r.req.URL.String(), body)
142 | }
143 |
144 | // processRequest handles single request
145 | func (r *RpcRequestHandler) processRequest(client RPCProxyClient, jsonReq *types.JsonRpcRequest, origin, referer string, isWhitehatBundleCollection bool, whitehatBundleId string, urlParams URLParameters, reqURL string, body []byte) {
146 | var entry *database.EthSendRawTxEntry
147 | if jsonReq.Method == "eth_sendRawTransaction" {
148 | entry = r.requestRecord.AddEthSendRawTxEntry(uuid.New())
149 | // log the full url for debugging
150 | r.logger.Info("[processRequest] eth_sendRawTransaction request URL", "url", reqURL)
151 | }
152 | // Handle single request
153 | rpcReq := NewRpcRequest(r.logger, client, jsonReq, r.relaySigningKey, r.relayUrl, origin, referer, isWhitehatBundleCollection, whitehatBundleId, entry, urlParams, r.chainID, r.rpcCache, r.defaultEthClient)
154 |
155 | if err := rpcReq.CheckFlashbotsSignature(r.req.Header.Get("X-Flashbots-Signature"), body); err != nil {
156 | r.logger.Warn("[processRequest] CheckFlashbotsSignature", "error", err)
157 | rpcReq.writeRpcError(err.Error(), types.JsonRpcInvalidRequest)
158 | r._writeRpcResponse(rpcReq.jsonRes)
159 | return
160 | }
161 | res := rpcReq.ProcessRequest()
162 | // Write response
163 | r._writeRpcResponse(res)
164 | }
165 |
166 | func (r *RpcRequestHandler) finishRequest() {
167 | reqDuration := time.Since(r.timeStarted) // At end of request, log the time it needed
168 | r.requestRecord.requestEntry.RequestDurationMs = reqDuration.Milliseconds()
169 | go func() {
170 | // Save both request entry and raw tx entries if present
171 | if err := r.requestRecord.SaveRecord(); err != nil {
172 | log.Error("saveRecord failed", "requestId", r.requestRecord.requestEntry.Id, "error", err, "requestId", r.uid)
173 | }
174 | }()
175 | r.logger.Info("Request finished", "duration", reqDuration.Seconds())
176 | }
177 |
--------------------------------------------------------------------------------
/server/request_intercepts.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/ethereum/go-ethereum/common/hexutil"
9 |
10 | "github.com/flashbots/rpc-endpoint/types"
11 | )
12 |
13 | var ProtectTxApiHost = GetEnv("TX_API_HOST", "https://protect.flashbots.net")
14 |
15 | // If public getTransactionReceipt of a submitted tx is null, then check internal API to see if tx has failed
16 | func (r *RpcRequest) check_post_getTransactionReceipt(jsonResp *types.JsonRpcResponse) (requestFinished bool) {
17 | if jsonResp == nil {
18 | return false
19 | }
20 |
21 | resultStr := string(jsonResp.Result)
22 | if resultStr != "null" {
23 | return false
24 | }
25 |
26 | if len(r.jsonReq.Params) < 1 {
27 | return false
28 | }
29 |
30 | txHashLower := strings.ToLower(r.jsonReq.Params[0].(string))
31 | r.logger.Info("[post_getTransactionReceipt] eth_getTransactionReceipt is null, check if it was a private tx", "txHash", txHashLower)
32 |
33 | // get tx status from private-tx-api
34 | statusApiResponse, err := GetTxStatus(txHashLower)
35 | if err != nil {
36 | r.logger.Error("[post_getTransactionReceipt] PrivateTxApi failed", "error", err)
37 | return false
38 | }
39 |
40 | resetMaxNonce := func(txFrom string, txHash string) {
41 | // if the tx failed then we want to reset the redis max nonce
42 | maxNonce, found, err := RState.GetSenderMaxNonce(txFrom)
43 | if err != nil {
44 | r.logger.Error("[post_getTransactionReceipt] GetSenderMaxNonce failed", "error", err)
45 | return
46 | }
47 |
48 | // if the user doesn't have a cached nonce then we can exit early
49 | if !found {
50 | return
51 | }
52 |
53 | // we can elide error checking here since a txNonce of 0 will never match
54 | txNonce, _, _ := RState.GetNonceOfTxHash(txHash)
55 | if maxNonce == txNonce {
56 | if err := RState.DelSenderMaxNonce(txFrom); err != nil {
57 | r.logger.Error("[post_getTransactionReceipt] DelSenderMaxNonce failed", "error", err)
58 | }
59 | }
60 | }
61 |
62 | ensureAccountFixIsInPlace := func() {
63 | // Get the sender of this transaction
64 | txFromLower, txFromFound, err := RState.GetSenderOfTxHash(txHashLower)
65 | if err != nil {
66 | r.logger.Error("[post_getTransactionReceipt] Redis:GetSenderOfTxHash failed", "error", err)
67 | return
68 | }
69 |
70 | if !txFromFound { // cannot sent nonce-fix if we don't have the sender
71 | return
72 | }
73 |
74 | // Check if nonceFix is already in place for this user
75 | _, nonceFixAlreadyExists, err := RState.GetNonceFixForAccount(txFromLower)
76 | if err != nil {
77 | r.logger.Error("[post_getTransactionReceipt] Redis:GetNonceFixForAccount failed", "error", err)
78 | return
79 | }
80 |
81 | if nonceFixAlreadyExists {
82 | return
83 | }
84 |
85 | // Setup a new nonce-fix for this user
86 | err = RState.SetNonceFixForAccount(txFromLower, 0)
87 | if err != nil {
88 | r.logger.Error("[post_getTransactionReceipt] Redis error", "error", err)
89 | return
90 | }
91 |
92 | r.logger.Info("[post_getTransactionReceipt] Nonce-fix set for tx", "tx", txFromLower)
93 |
94 | // Also reset the users max nonce if necessary
95 | resetMaxNonce(txFromLower, txHashLower)
96 | }
97 |
98 | r.logger.Info("[post_getTransactionReceipt] Priv-tx-api status", "status", statusApiResponse.Status)
99 | if statusApiResponse.Status == types.TxStatusFailed || (DebugDontSendTx && statusApiResponse.Status == types.TxStatusUnknown) {
100 | r.logger.Info("[post_getTransactionReceipt] Failed private tx, ensure account fix is in place")
101 | ensureAccountFixIsInPlace()
102 | // r.writeRpcError("Transaction failed") // If this is sent before metamask dropped the tx (received 4x invalid nonce), then it doesn't call getTransactionCount anymore
103 | // TODO: return standard failed tx payload?
104 | return false
105 |
106 | // } else if statusApiResponse.Status == types.TxStatusIncluded {
107 | // // NOTE: This branch can never happen, because if tx is included then Receipt will not return null
108 | // // TODO? If latest tx of this user was a successful, then we should remove the nonce fix
109 | // // This could lead to a ping-pong between checking 2 tx, with one check adding and another removing the nonce fix
110 | // // See also the branch tmp-check_post_getTransactionReceipt-removeNonceFix
111 | // _ = 1
112 | }
113 |
114 | return false
115 | }
116 |
117 | func (r *RpcRequest) intercept_mm_eth_getTransactionCount() (requestFinished bool) {
118 | if len(r.jsonReq.Params) < 1 {
119 | return false
120 | }
121 |
122 | addr := strings.ToLower(r.jsonReq.Params[0].(string))
123 |
124 | // Check if nonceFix is in place for this user
125 | numTimesSent, nonceFixInPlace, err := RState.GetNonceFixForAccount(addr)
126 | if err != nil {
127 | r.logger.Error("[eth_getTransactionCount] Redis:GetAccountWithNonceFix error:", "error", err)
128 | return false
129 | }
130 |
131 | if !nonceFixInPlace {
132 | return false
133 | }
134 |
135 | // Intercept max 4 times (after which Metamask marks it as dropped)
136 | numTimesSent += 1
137 | if numTimesSent > 4 {
138 | return false
139 | }
140 |
141 | err = RState.SetNonceFixForAccount(addr, numTimesSent)
142 | if err != nil {
143 | r.logger.Error("[eth_getTransactionCount] Redis:SetAccountWithNonceFix error", "error", err)
144 | return false
145 | }
146 |
147 | r.logger.Info("[eth_getTransactionCount] intercept", "numTimesSent", numTimesSent)
148 |
149 | // Return invalid nonce
150 | var wrongNonce uint64 = 1e9 + 1
151 | resp := fmt.Sprintf("0x%x", wrongNonce)
152 | r.writeRpcResult(resp)
153 | r.logger.Info("[eth_getTransactionCount] Intercepted eth_getTransactionCount for", "address", addr)
154 | return true
155 | }
156 |
157 | // Returns true if request has already received a response, false if req should contiue to normal proxy
158 | func (r *RpcRequest) intercept_eth_call_to_FlashRPC_Contract() (requestFinished bool) {
159 | if len(r.jsonReq.Params) < 1 {
160 | return false
161 | }
162 |
163 | ethCallReq := r.jsonReq.Params[0].(map[string]interface{})
164 | if ethCallReq["to"] == nil {
165 | return false
166 | }
167 |
168 | addressTo := strings.ToLower(ethCallReq["to"].(string))
169 |
170 | // Only handle calls to the Flashbots RPC check contract
171 | // 0xf1a54b075 --> 0xflashbots
172 | // https://etherscan.io/address/0xf1a54b0759b58661cea17cff19dd37940a9b5f1a#readContract
173 | if addressTo != "0xf1a54b0759b58661cea17cff19dd37940a9b5f1a" {
174 | return false
175 | }
176 |
177 | r.writeRpcResult("0x0000000000000000000000000000000000000000000000000000000000000001")
178 | r.logger.Info("Intercepted eth_call to FlashRPC contract")
179 | return true
180 | }
181 |
182 | func (r *RpcRequest) intercept_signed_eth_getTransactionCount() (requestFinished bool) {
183 | if r.flashbotsSigningAddress == "" {
184 | r.logger.Info("[eth_getTransactionCount] No signature found")
185 | return false
186 | }
187 |
188 | if len(r.jsonReq.Params) != 2 {
189 | r.logger.Info("[eth_getTransactionCount] Invalid params")
190 | return false
191 | }
192 |
193 | blockSpecifier, ok := r.jsonReq.Params[1].(string)
194 | if !ok || blockSpecifier != "pending" {
195 | r.logger.Info("[eth_getTransactionCount] non-pending blockSpecifier")
196 | return false
197 | }
198 |
199 | addr, ok := r.jsonReq.Params[0].(string)
200 | if !ok {
201 | r.logger.Info("[eth_getTransactionCount] non-string address")
202 | return false
203 | }
204 | addr = strings.ToLower(addr)
205 | if addr != strings.ToLower(r.flashbotsSigningAddress) {
206 | r.logger.Info("[eth_getTransactionCount] address mismatch", "addr", addr, "signingAddress", r.flashbotsSigningAddress)
207 | return false
208 | }
209 |
210 | // since it's possible that the user sent another tx via another provider, we need to check the nonce from
211 | // both the backend and our cache, and return the greater of the two
212 | cachedNonce, found, err := RState.GetSenderMaxNonce(addr)
213 | if err != nil {
214 | r.logger.Error("[eth_getTransactionCount] Redis:GetSenderMaxNonce error", "error", err)
215 | return false
216 | }
217 | if !found {
218 | r.logger.Info("[eth_getTransactionCount] No nonce found")
219 | return false
220 | }
221 | if !r.proxyRequestRead() {
222 | r.logger.Info("[ProcessRequest] Proxy to node failed", "method", r.jsonReq.Method)
223 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
224 | return true
225 | }
226 | r.logger.Info("[eth_getTransactionCount] intercept", "cachedNonce", cachedNonce, "addr", addr)
227 |
228 | backendTxCount := uint64(0)
229 | if r.jsonRes.Result != nil {
230 | count := hexutil.Uint64(0)
231 | if err := json.Unmarshal(r.jsonRes.Result, &count); err != nil {
232 | r.logger.Info("[ProcessRequest] Unmarshal backend response failed", "method", r.jsonReq.Method)
233 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
234 | return true
235 | }
236 | backendTxCount = uint64(count)
237 | r.logger.Info("[eth_getTransactionCount] intercept", "backendTxCount", backendTxCount, "addr", addr)
238 | }
239 |
240 | // return either the cached nonce plus one, or the backend tx count
241 | txCount := cachedNonce + 1
242 | if backendTxCount > txCount {
243 | txCount = backendTxCount
244 | // since the cached value is invalid lets remove it from redis
245 | r.logger.Info("[eth_getTransactionCount] intercept invalidated nonce", "addr", addr)
246 | if err := RState.DelSenderMaxNonce(addr); err != nil {
247 | // log the error but continue
248 | r.logger.Error("[eth_getTransactionCount] Redis:DelSenderMaxNonce error", "error", err, "addr", addr)
249 | }
250 | }
251 |
252 | resp := fmt.Sprintf("0x%x", txCount)
253 | r.writeRpcResult(resp)
254 | return true
255 | }
256 |
--------------------------------------------------------------------------------
/server/request_processor.go:
--------------------------------------------------------------------------------
1 | /*
2 | Request represents an incoming client request
3 | */
4 | package server
5 |
6 | import (
7 | "context"
8 | "crypto/ecdsa"
9 | "encoding/json"
10 | "errors"
11 | "io"
12 | "math/big"
13 | "reflect"
14 | "strings"
15 | "time"
16 |
17 | "github.com/ethereum/go-ethereum/ethclient"
18 | "github.com/flashbots/rpc-endpoint/adapters/flashbots"
19 | "github.com/flashbots/rpc-endpoint/application"
20 | "github.com/flashbots/rpc-endpoint/database"
21 |
22 | "github.com/ethereum/go-ethereum/log"
23 |
24 | ethtypes "github.com/ethereum/go-ethereum/core/types"
25 | "github.com/metachris/flashbotsrpc"
26 |
27 | "github.com/flashbots/rpc-endpoint/types"
28 | )
29 |
30 | type RpcRequest struct {
31 | logger log.Logger
32 | client RPCProxyClient
33 | defaultEthClient *ethclient.Client
34 | jsonReq *types.JsonRpcRequest
35 | jsonRes *types.JsonRpcResponse
36 | rawTxHex string
37 | tx *ethtypes.Transaction
38 | txFrom string
39 | relaySigningKey *ecdsa.PrivateKey
40 | relayUrl string
41 | origin string
42 | referer string
43 | isWhitehatBundleCollection bool
44 | whitehatBundleId string
45 | ethSendRawTxEntry *database.EthSendRawTxEntry
46 | urlParams URLParameters
47 | chainID []byte
48 | rpcCache *application.RpcCache
49 | flashbotsSigningAddress string
50 | }
51 |
52 | func NewRpcRequest(
53 | logger log.Logger,
54 | client RPCProxyClient,
55 | jsonReq *types.JsonRpcRequest,
56 | relaySigningKey *ecdsa.PrivateKey,
57 | relayUrl, origin, referer string,
58 | isWhitehatBundleCollection bool,
59 | whitehatBundleId string,
60 | ethSendRawTxEntry *database.EthSendRawTxEntry,
61 | urlParams URLParameters,
62 | chainID []byte,
63 | rpcCache *application.RpcCache,
64 | defaultEthClient *ethclient.Client,
65 | ) *RpcRequest {
66 | return &RpcRequest{
67 | logger: logger.With("method", jsonReq.Method),
68 | client: client,
69 | jsonReq: jsonReq,
70 | relaySigningKey: relaySigningKey,
71 | relayUrl: relayUrl,
72 | origin: origin,
73 | referer: referer,
74 | isWhitehatBundleCollection: isWhitehatBundleCollection,
75 | whitehatBundleId: whitehatBundleId,
76 | ethSendRawTxEntry: ethSendRawTxEntry,
77 | urlParams: urlParams,
78 | chainID: chainID,
79 | rpcCache: rpcCache,
80 | defaultEthClient: defaultEthClient,
81 | }
82 | }
83 |
84 | func (r *RpcRequest) logRequest() {
85 | if r.jsonReq.Method == "eth_call" && len(r.jsonReq.Params) > 0 {
86 | p := r.jsonReq.Params[0].(map[string]interface{})
87 | _to := ""
88 | _data := ""
89 | _method := _data
90 | if p["to"] != nil {
91 | _to = p["to"].(string)
92 | }
93 | if p["data"] != nil {
94 | _data = p["data"].(string)
95 | }
96 | if len(_data) >= 10 {
97 | _method = _data[:10]
98 | }
99 | r.logger.Info("JSON-RPC request", "method", r.jsonReq.Method, "paramsTo", _to, "paramsDataMethod", _method, "paramsDataLen", len(_data), "origin", r.origin, "referer", r.referer)
100 | } else {
101 | r.logger.Info("JSON-RPC request", "method", r.jsonReq.Method, "params", r.jsonReq.Params, "origin", r.origin, "referer", r.referer)
102 | }
103 | }
104 |
105 | func (r *RpcRequest) ProcessRequest() *types.JsonRpcResponse {
106 | r.logRequest()
107 |
108 | switch {
109 | case r.jsonReq.Method == "eth_sendRawTransaction":
110 | r.ethSendRawTxEntry.WhiteHatBundleId = r.whitehatBundleId
111 | r.handle_sendRawTransaction()
112 | case r.jsonReq.Method == "eth_getTransactionCount" && r.intercept_signed_eth_getTransactionCount():
113 | case r.jsonReq.Method == "eth_getTransactionCount" && r.intercept_mm_eth_getTransactionCount(): // intercept if MM needs to show an error to user
114 | case r.jsonReq.Method == "eth_call" && r.intercept_eth_call_to_FlashRPC_Contract(): // intercept if Flashbots isRPC contract
115 | case r.jsonReq.Method == "web3_clientVersion":
116 | res, ok := r.rpcCache.Get("web3_clientVersion")
117 | if ok {
118 | r.jsonRes = res
119 | return r.jsonRes
120 | }
121 |
122 | readJsonRpcSuccess := r.proxyRequestRead()
123 | if !readJsonRpcSuccess {
124 | r.logger.Info("[ProcessRequest] Proxy to node failed", "method", r.jsonReq.Method)
125 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
126 | return r.jsonRes
127 | }
128 | r.rpcCache.Set("web3_clientVersion", r.jsonRes)
129 | case r.jsonReq.Method == "net_version":
130 | r.writeRpcResult(json.RawMessage(r.chainID))
131 | case r.isWhitehatBundleCollection && r.jsonReq.Method == "eth_getBalance":
132 | r.writeRpcResult("0x56bc75e2d63100000") // 100 ETH, same as the eth_call SC call above returns
133 | default:
134 | if r.isWhitehatBundleCollection && r.jsonReq.Method == "eth_call" {
135 | r.WhitehatBalanceCheckerRewrite()
136 | }
137 | // Proxy the request to a node
138 | readJsonRpcSuccess := r.proxyRequestRead()
139 | if !readJsonRpcSuccess {
140 | r.logger.Info("[ProcessRequest] Proxy to node failed", "method", r.jsonReq.Method)
141 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
142 | return r.jsonRes
143 | }
144 |
145 | // After proxy, perhaps check backend [MM fix #3 step 2]
146 | if r.jsonReq.Method == "eth_getTransactionReceipt" {
147 | requestCompleted := r.check_post_getTransactionReceipt(r.jsonRes)
148 | if requestCompleted {
149 | return r.jsonRes
150 | }
151 | }
152 | }
153 | return r.jsonRes
154 | }
155 |
156 | // Proxies the incoming request to the target URL, and tries to parse JSON-RPC response (and check for specific)
157 | func (r *RpcRequest) proxyRequestRead() (readJsonRpsResponseSuccess bool) {
158 | timeProxyStart := Now() // for measuring execution time
159 | body, err := json.Marshal(r.jsonReq)
160 | if err != nil {
161 | r.logger.Error("[proxyRequestRead] Failed to marshal request before making proxy request", "error", err)
162 | return false
163 | }
164 |
165 | // Proxy request
166 | proxyResp, err := r.client.ProxyRequest(body)
167 | if err != nil {
168 | r.logger.Error("[proxyRequestRead] Failed to make proxy request", "error", err, "response", proxyResp)
169 | if proxyResp == nil {
170 | return false
171 | } else {
172 | return false
173 | }
174 | }
175 |
176 | // Afterwards, check time and result
177 | timeProxyNeeded := time.Since(timeProxyStart)
178 | r.logger.Info("[proxyRequestRead] proxied response", "statusCode", proxyResp.StatusCode, "secNeeded", timeProxyNeeded.Seconds())
179 |
180 | // Read body
181 | defer proxyResp.Body.Close()
182 | proxyRespBody, err := io.ReadAll(proxyResp.Body)
183 | if err != nil {
184 | r.logger.Error("[proxyRequestRead] Failed to read proxy request body", "error", err)
185 | return false
186 | }
187 |
188 | // Unmarshall JSON-RPC response and check for error inside
189 | jsonRpcResp := new(types.JsonRpcResponse)
190 | if err = json.Unmarshal(proxyRespBody, jsonRpcResp); err != nil {
191 | r.logger.Error("[proxyRequestRead] Failed decoding proxy json-rpc response", "error", err, "response", proxyRespBody)
192 | return false
193 | }
194 | r.jsonRes = jsonRpcResp
195 | return true
196 | }
197 |
198 | // Check whether to block resending this tx. Send only if (a) not sent before, (b) sent and status=failed, (c) sent, status=unknown and sent at least 5 min ago
199 | func (r *RpcRequest) blockResendingTxToRelay(txHash string) bool {
200 | timeSent, txWasSentToRelay, err := RState.GetTxSentToRelay(txHash)
201 | if err != nil {
202 | r.logger.Error("[blockResendingTxToRelay] Redis:GetTxSentToRelay error", "error", err)
203 | return false // don't block on redis error
204 | }
205 |
206 | if !txWasSentToRelay {
207 | return false // don't block if not sent before
208 | }
209 |
210 | // was sent before. check status and time
211 | txStatusApiResponse, err := GetTxStatus(txHash)
212 | if err != nil {
213 | r.logger.Error("[blockResendingTxToRelay] GetTxStatus error", "error", err)
214 | return false // don't block on redis error
215 | }
216 |
217 | // Allow sending to relay if tx has failed, or if it's still unknown after a while
218 | txStatus := txStatusApiResponse.Status
219 | if txStatus == types.TxStatusFailed {
220 | return false // don't block if tx failed
221 | } else if txStatus == types.TxStatusUnknown && time.Since(timeSent).Minutes() >= 5 {
222 | return false // don't block if unknown and sent at least 5 min ago
223 | } else {
224 | // block tx if pending or already included
225 | return true
226 | }
227 | }
228 |
229 | // Send tx to relay and finish request (write response)
230 | func (r *RpcRequest) sendTxToRelay() {
231 | txHash := strings.ToLower(r.tx.Hash().Hex())
232 | // Check if tx was already forwarded and should be blocked now
233 | IsBlocked := r.blockResendingTxToRelay(txHash)
234 | if IsBlocked {
235 | r.ethSendRawTxEntry.IsBlocked = IsBlocked
236 | r.logger.Info("[sendTxToRelay] Blocked", "tx", txHash)
237 | r.writeRpcResult(txHash)
238 | return
239 | }
240 |
241 | r.logger.Info("[sendTxToRelay] sending transaction to relay", "tx", txHash, "fromAddress", r.txFrom, "toAddress", r.tx.To())
242 | r.ethSendRawTxEntry.WasSentToRelay = true
243 |
244 | // mark tx as sent to relay
245 | err := RState.SetTxSentToRelay(txHash)
246 | if err != nil {
247 | r.logger.Error("[sendTxToRelay] Redis:SetTxSentToRelay failed", "error", err)
248 | }
249 |
250 | minNonce, maxNonce, err := r.GetAddressNonceRange(r.txFrom)
251 | if err != nil {
252 | r.logger.Error("[sendTxToRelay] GetAddressNonceRange error", "error", err)
253 | } else {
254 | if r.tx.Nonce() < minNonce || r.tx.Nonce() > maxNonce+1 {
255 | r.logger.Info("[sendTxToRelay] invalid nonce", "tx", txHash, "txFrom", r.txFrom, "minNonce", minNonce, "maxNonce", maxNonce+1, "txNonce", r.tx.Nonce())
256 | r.writeRpcError("invalid nonce", types.JsonRpcInternalError)
257 | return
258 | }
259 | }
260 |
261 | go RState.SetSenderMaxNonce(r.txFrom, r.tx.Nonce(), r.urlParams.blockRange)
262 |
263 | // only allow large non-blob transactions to certain addresses - default max tx size is 128KB
264 | // https://github.com/ethereum/go-ethereum/blob/master/core/tx_pool.go#L53
265 | if r.tx.Type() != ethtypes.BlobTxType && r.tx.Size() > 131072 {
266 | if r.tx.To() == nil {
267 | r.logger.Error("[sendTxToRelay] large tx not allowed to target null", "tx", txHash)
268 | r.writeRpcError("invalid target for large tx", types.JsonRpcInternalError)
269 | return
270 | } else if _, found := allowedLargeTxTargets[strings.ToLower(r.tx.To().Hex())]; !found {
271 | r.logger.Error("[sendTxToRelay] large tx not allowed to target", "tx", txHash, "target", r.tx.To())
272 | r.writeRpcError("invalid target for large tx", types.JsonRpcInternalError)
273 | return
274 | }
275 | r.logger.Info("sendTxToRelay] allowed large tx", "tx", txHash, "target", r.tx.To())
276 | }
277 |
278 | // remember this tx based on from+nonce (for cancel-tx)
279 | err = RState.SetTxHashForSenderAndNonce(r.txFrom, r.tx.Nonce(), txHash)
280 | if err != nil {
281 | r.logger.Error("[sendTxToRelay] Redis:SetTxHashForSenderAndNonce failed", "error", err)
282 | }
283 |
284 | // err = RState.SetLastPrivTxHashOfAccount(r.txFrom, txHash)
285 | // if err != nil {
286 | // r.Error("[sendTxToRelay] redis:SetLastTxHashOfAccount failed: %v", err)
287 | // }
288 |
289 | if DebugDontSendTx {
290 | r.logger.Info("[sendTxToRelay] Faked sending tx to relay, did nothing", "tx", txHash)
291 | r.writeRpcResult(txHash)
292 | return
293 | }
294 |
295 | sendPrivateTxArgs := types.SendPrivateTxRequestWithPreferences{}
296 | sendPrivateTxArgs.Tx = r.rawTxHex
297 | sendPrivateTxArgs.Preferences = &r.urlParams.pref
298 | if r.urlParams.fast {
299 | if len(sendPrivateTxArgs.Preferences.Validity.Refund) == 0 {
300 | addr, err := GetSenderAddressFromTx(r.tx)
301 | if err != nil {
302 | r.logger.Error("[sendTxToRelay] GetSenderAddressFromTx failed", "error", err)
303 | r.writeRpcError(err.Error(), types.JsonRpcInternalError)
304 | return
305 | }
306 | sendPrivateTxArgs.Preferences.Validity.Refund = []types.RefundConfig{
307 | {
308 | Address: addr,
309 | Percent: 50,
310 | },
311 | }
312 | }
313 | }
314 |
315 | if r.urlParams.auctionTimeout != 0 {
316 | sendPrivateTxArgs.Preferences.Privacy.AuctionTimeout = r.urlParams.auctionTimeout
317 | }
318 |
319 | if r.urlParams.blockRange > 0 {
320 | bn, err := r.defaultEthClient.BlockNumber(context.Background())
321 | if err != nil {
322 | r.logger.Error("[sendTxToRelay] BlockNumber failed", "error", err)
323 | r.writeRpcError(err.Error(), types.JsonRpcInternalError)
324 | return
325 | }
326 | // this actually means that we use blockRange+1, to avoid problems with lagging blocks etc.
327 | maxBlockNumber := bn + 1 + uint64(r.urlParams.blockRange)
328 | sendPrivateTxArgs.MaxBlockNumber = maxBlockNumber
329 | }
330 |
331 | fbRpc := flashbotsrpc.New(r.relayUrl, func(rpc *flashbotsrpc.FlashbotsRPC) {
332 | if r.urlParams.originId != "" {
333 | rpc.Headers["X-Flashbots-Origin"] = r.urlParams.originId
334 | }
335 | })
336 | r.logger.Info("[sendTxToRelay] sending transaction", "builders count", len(sendPrivateTxArgs.Preferences.Privacy.Builders), "is_fast", r.urlParams.fast)
337 | _, err = fbRpc.CallWithFlashbotsSignature("eth_sendPrivateTransaction", r.relaySigningKey, sendPrivateTxArgs)
338 | if err != nil {
339 | if errors.Is(err, flashbotsrpc.ErrRelayErrorResponse) {
340 | r.logger.Info("[sendTxToRelay] Relay error response", "error", err, "rawTx", r.rawTxHex)
341 | } else {
342 | r.logger.Error("[sendTxToRelay] Relay call failed", "error", err, "rawTx", r.rawTxHex)
343 | }
344 | // todo: we need to change the way we call bundle-relay-api as it's not json-rpc compatible so we don't get proper
345 | // error code/text
346 | r.writeRpcError("internal error", types.JsonRpcInternalError)
347 | return
348 | }
349 |
350 | r.writeRpcResult(txHash)
351 | r.logger.Info("[sendTxToRelay] Sent", "tx", txHash)
352 | }
353 |
354 | // Sends cancel-tx to relay as cancelPrivateTransaction, if initial tx was sent there too.
355 | func (r *RpcRequest) handleCancelTx() (requestCompleted bool) {
356 | cancelTxHash := strings.ToLower(r.tx.Hash().Hex())
357 | txFromLower := strings.ToLower(r.txFrom)
358 | r.logger.Info("[cancel-tx] cancelling transaction", "cancelTxHash", cancelTxHash, "txFromLower", txFromLower, "txNonce", r.tx.Nonce())
359 |
360 | // Get initial txHash by sender+nonce
361 | initialTxHash, txHashFound, err := RState.GetTxHashForSenderAndNonce(txFromLower, r.tx.Nonce())
362 | if err != nil {
363 | r.logger.Error("[cancelTx] Redis:GetTxHashForSenderAndNonce failed", "error", err)
364 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
365 | return true
366 | }
367 |
368 | if !txHashFound { // not found, send to mempool
369 | return false
370 | }
371 |
372 | // Check if initial tx was sent to relay
373 | _, txWasSentToRelay, err := RState.GetTxSentToRelay(initialTxHash)
374 | if err != nil {
375 | r.logger.Error("[cancelTx] Redis:GetTxSentToRelay failed", "error", err)
376 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
377 | return true
378 | }
379 |
380 | if !txWasSentToRelay { // was not sent to relay, send to mempool
381 | return false
382 | }
383 |
384 | // Should send cancel-tx to relay. Check if cancel-tx was already sent before
385 | _, cancelTxAlreadySentToRelay, err := RState.GetTxSentToRelay(cancelTxHash)
386 | if err != nil {
387 | r.logger.Error("[cancelTx] Redis:GetTxSentToRelay error", "error", err)
388 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
389 | return true
390 | }
391 |
392 | if cancelTxAlreadySentToRelay { // already sent
393 | r.writeRpcResult(cancelTxHash)
394 | return true
395 | }
396 |
397 | err = RState.SetTxSentToRelay(cancelTxHash)
398 | if err != nil {
399 | r.logger.Error("[cancelTx] Redis:SetTxSentToRelay failed", "error", err)
400 | }
401 |
402 | r.logger.Info("[cancel-tx] sending to relay", "initialTxHash", initialTxHash, "txFromLower", txFromLower, "txNonce", r.tx.Nonce())
403 |
404 | if DebugDontSendTx {
405 | r.logger.Info("[cancelTx] Faked sending cancel-tx to relay, did nothing", "tx", initialTxHash)
406 | r.writeRpcResult(initialTxHash)
407 | return true
408 | }
409 |
410 | if r.urlParams.pref.Privacy.UseMempool {
411 | r.logger.Info("[cancelTx] cancel-tx sending to mempool", "tx", initialTxHash)
412 | ethCl := r.defaultEthClient
413 | if r.urlParams.pref.Privacy.MempoolRPC != "" {
414 | ethCl, err = ethclient.Dial(r.urlParams.pref.Privacy.MempoolRPC)
415 | if err != nil {
416 | r.logger.Error("[cancelTx] Dial failed", "error", err, "rpc", r.urlParams.pref.Privacy.MempoolRPC)
417 | r.writeRpcError("invalid mempool rpc", types.JsonRpcInvalidParams)
418 | return true
419 | }
420 | }
421 |
422 | err = ethCl.SendTransaction(context.Background(), r.tx)
423 | if err != nil {
424 | r.logger.Error("[cancelTx] SendTransaction failed", "error", err)
425 | r.writeRpcError("proxying cancellation to mempool failed", types.JsonRpcInternalError)
426 | return true
427 | }
428 | }
429 |
430 | cancelPrivTxArgs := flashbotsrpc.FlashbotsCancelPrivateTransactionRequest{TxHash: initialTxHash}
431 |
432 | fbRpc := flashbotsrpc.New(r.relayUrl)
433 | _, err = fbRpc.FlashbotsCancelPrivateTransaction(r.relaySigningKey, cancelPrivTxArgs)
434 | if err != nil {
435 | if errors.Is(err, flashbotsrpc.ErrRelayErrorResponse) {
436 | // errors could be: 'tx not found', 'tx was already cancelled', 'tx has already expired'
437 | r.logger.Info("[cancelTx] Relay error response", "err", err, "rawTx", r.rawTxHex)
438 | r.writeRpcError(err.Error(), types.JsonRpcInternalError)
439 | } else {
440 | r.logger.Error("[cancelTx] Relay call failed", "error", err, "rawTx", r.rawTxHex)
441 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
442 | }
443 | return true
444 | }
445 |
446 | r.writeRpcResult(cancelTxHash)
447 | return true
448 | }
449 |
450 | func (r *RpcRequest) GetAddressNonceRange(address string) (minNonce, maxNonce uint64, err error) {
451 | // Get minimum nonce by asking the eth node for the current transaction count
452 | _req := types.NewJsonRpcRequest(1, "eth_getTransactionCount", []interface{}{r.txFrom, "latest"})
453 | jsonData, err := json.Marshal(_req)
454 | if err != nil {
455 | r.logger.Error("[GetAddressNonceRange] eth_getTransactionCount marshal failed", "error", err)
456 | return 0, 0, err
457 | }
458 | httpRes, err := r.client.ProxyRequest(jsonData)
459 | if err != nil {
460 | r.logger.Error("[GetAddressNonceRange] eth_getTransactionCount proxy request failed", "error", err)
461 | return 0, 0, err
462 | }
463 |
464 | resBytes, err := io.ReadAll(httpRes.Body)
465 | httpRes.Body.Close()
466 | if err != nil {
467 | r.logger.Error("[GetAddressNonceRange] eth_getTransactionCount read response failed", "error", err)
468 | return 0, 0, err
469 | }
470 | _res, err := respBytesToJsonRPCResponse(resBytes)
471 | if err != nil {
472 | r.logger.Error("[GetAddressNonceRange] eth_getTransactionCount parsing response failed", "error", err)
473 | return 0, 0, err
474 | }
475 | _userNonceStr := ""
476 | err = json.Unmarshal(_res.Result, &_userNonceStr)
477 | if err != nil {
478 | r.logger.Error("[GetAddressNonceRange] eth_getTransactionCount unmarshall failed", "error", err, "result", _res.Result)
479 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
480 | return
481 | }
482 | _userNonceStr = strings.Replace(_userNonceStr, "0x", "", 1)
483 | _userNonceBigInt := new(big.Int)
484 | _userNonceBigInt.SetString(_userNonceStr, 16)
485 | minNonce = _userNonceBigInt.Uint64()
486 |
487 | // Get maximum nonce by looking at redis, which has current pending transactions
488 | _redisMaxNonce, _, _ := RState.GetSenderMaxNonce(r.txFrom)
489 | maxNonce = Max(minNonce, _redisMaxNonce)
490 | return minNonce, maxNonce, nil
491 | }
492 |
493 | func (r *RpcRequest) WhitehatBalanceCheckerRewrite() {
494 | var err error
495 |
496 | if len(r.jsonReq.Params) == 0 {
497 | return
498 | }
499 |
500 | // Ensure param is of type map
501 | t := reflect.TypeOf(r.jsonReq.Params[0])
502 | if t.Kind() != reflect.Map {
503 | return
504 | }
505 |
506 | p := r.jsonReq.Params[0].(map[string]interface{})
507 | if to := p["to"]; to == "0xb1f8e55c7f64d203c1400b9d8555d050f94adf39" {
508 | r.jsonReq.Params[0].(map[string]interface{})["to"] = "0x268F7Cd7A396BCE178f0937095772C7fb83a9104"
509 | if err != nil {
510 | r.logger.Error("[WhitehatBalanceCheckerRewrite] isWhitehatBundleCollection json marshal failed:", "error", err)
511 | } else {
512 | r.logger.Info("[WhitehatBalanceCheckerRewrite] BalanceChecker contract was rewritten to new version")
513 | }
514 | }
515 | }
516 |
517 | func (r *RpcRequest) writeRpcError(msg string, errCode int) {
518 | if r.jsonReq.Method == "eth_sendRawTransaction" {
519 | r.ethSendRawTxEntry.Error = msg
520 | r.ethSendRawTxEntry.ErrorCode = errCode
521 | }
522 | r.jsonRes = &types.JsonRpcResponse{
523 | Id: r.jsonReq.Id,
524 | Version: "2.0",
525 | Error: &types.JsonRpcError{
526 | Code: errCode,
527 | Message: msg,
528 | },
529 | }
530 |
531 | }
532 |
533 | func (r *RpcRequest) writeRpcResult(result interface{}) {
534 | resBytes, err := json.Marshal(result)
535 | if err != nil {
536 | r.logger.Error("[writeRpcResult] writeRpcResult error marshalling", "error", err, "result", result)
537 | r.writeRpcError("internal server error", types.JsonRpcInternalError)
538 | return
539 | }
540 | r.jsonRes = &types.JsonRpcResponse{
541 | Id: r.jsonReq.Id,
542 | Version: "2.0",
543 | Result: resBytes,
544 | }
545 | }
546 |
547 | // CheckFlashbotsSignature parses and validates the Flashbots signature if present,
548 | // returning an error if the signature is invalid. If the signature is present and valid
549 | // the signing address is stored in the request.
550 | func (r *RpcRequest) CheckFlashbotsSignature(signature string, body []byte) error {
551 | // Most requests don't have a signature, so avoid parsing it if it's empty
552 | if signature == "" {
553 | return nil
554 | }
555 | signingAddress, err := flashbots.ParseSignature(signature, body)
556 | if err != nil {
557 | if errors.Is(err, flashbots.ErrNoSignature) {
558 | return nil
559 | } else {
560 | return err
561 | }
562 | }
563 | r.flashbotsSigningAddress = signingAddress
564 | return nil
565 | }
566 |
--------------------------------------------------------------------------------
/server/request_processor_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "net/http"
8 | "net/http/httptest"
9 | "testing"
10 | "time"
11 |
12 | "github.com/alicebob/miniredis"
13 | "github.com/ethereum/go-ethereum/crypto"
14 | "github.com/ethereum/go-ethereum/log"
15 | "github.com/flashbots/rpc-endpoint/database"
16 | "github.com/flashbots/rpc-endpoint/testutils"
17 | "github.com/flashbots/rpc-endpoint/types"
18 | "github.com/stretchr/testify/require"
19 | )
20 |
21 | func setupRedis() {
22 | redisServer, err := miniredis.Run()
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | RState, err = NewRedisState(redisServer.Addr())
28 | if err != nil {
29 | panic(err)
30 | }
31 | }
32 |
33 | func setupMockTxApi() {
34 | txApiServer := httptest.NewServer(http.HandlerFunc(testutils.MockTxApiHandler))
35 | ProtectTxApiHost = txApiServer.URL
36 | testutils.MockTxApiReset()
37 | }
38 |
39 | func setServerTimeNowOffset(td time.Duration) {
40 | Now = func() time.Time {
41 | return time.Now().Add(td)
42 | }
43 | }
44 |
45 | func TestRequestshouldSendTxToRelay(t *testing.T) {
46 | setupRedis()
47 | setupMockTxApi()
48 |
49 | request := RpcRequest{}
50 | txHash := "0x0Foo"
51 |
52 | // SEND when not seen before
53 | shouldSend := !request.blockResendingTxToRelay(txHash)
54 | require.True(t, shouldSend)
55 |
56 | // Fake a previous send
57 | err := RState.SetTxSentToRelay(txHash)
58 | require.Nil(t, err, err)
59 |
60 | // Ensure tx status is UNKNOWN
61 | txStatusApiResponse, err := GetTxStatus(txHash)
62 | require.Nil(t, err, err)
63 | require.Equal(t, types.TxStatusUnknown, txStatusApiResponse.Status)
64 |
65 | // NOT SEND when unknown and time since sent < 5 min
66 | shouldSend = !request.blockResendingTxToRelay(txHash)
67 | require.False(t, shouldSend)
68 |
69 | // Set tx status to Failed
70 | testutils.MockTxApiStatusForHash[txHash] = types.TxStatusFailed
71 | txStatusApiResponse, err = GetTxStatus(txHash)
72 | require.Nil(t, err, err)
73 | require.Equal(t, types.TxStatusFailed, txStatusApiResponse.Status)
74 |
75 | // SEND if failed
76 | shouldSend = !request.blockResendingTxToRelay(txHash)
77 | require.True(t, shouldSend)
78 |
79 | // Set tx status to pending
80 | testutils.MockTxApiStatusForHash[txHash] = types.TxStatusPending
81 | txStatusApiResponse, err = GetTxStatus(txHash)
82 | require.Nil(t, err, err)
83 | require.Equal(t, types.TxStatusPending, txStatusApiResponse.Status)
84 |
85 | // NOT SEND if pending
86 | shouldSend = !request.blockResendingTxToRelay(txHash)
87 | require.False(t, shouldSend)
88 |
89 | //
90 | // SEND if UNKNOWN and 5 minutes have passed
91 | //
92 | txHash = "0x0DeadBeef"
93 | setServerTimeNowOffset(time.Minute * -6)
94 | defer setServerTimeNowOffset(0)
95 |
96 | err = RState.SetTxSentToRelay(txHash)
97 | require.Nil(t, err, err)
98 |
99 | timeSent, found, err := RState.GetTxSentToRelay(txHash)
100 | require.Nil(t, err, err)
101 | require.True(t, found)
102 | require.True(t, time.Since(timeSent) > time.Minute*4)
103 |
104 | // Ensure tx status is UNKNOWN
105 | txStatusApiResponse, err = GetTxStatus(txHash)
106 | require.Nil(t, err, err)
107 | require.Equal(t, types.TxStatusUnknown, txStatusApiResponse.Status)
108 |
109 | shouldSend = !request.blockResendingTxToRelay(txHash)
110 | require.True(t, shouldSend)
111 | }
112 |
113 | type mockClient struct {
114 | err error
115 | nextResponse *http.Response
116 | }
117 |
118 | func (m mockClient) ProxyRequest(body []byte) (*http.Response, error) {
119 | return m.nextResponse, m.err
120 | }
121 |
122 | var _ RPCProxyClient = &mockClient{}
123 |
124 | func TestFailedTxShouldResetMaxNonce(t *testing.T) {
125 | setupRedis()
126 | setupMockTxApi()
127 |
128 | sender := "0x6bc84f6a0fabbd7102be338c048fe0ae54948c2e"
129 | txHash := "0x58e5a0fc7fbc849eddc100d44e86276168a8c7baaa5604e44ba6f5eb8ba1b7eb"
130 |
131 | mockClient := &mockClient{}
132 | privKey, _ := crypto.GenerateKey()
133 | require.NotNil(t, privKey)
134 |
135 | t.Run("setup", func(t *testing.T) {
136 | err := RState.SetSenderMaxNonce(sender, 4, 10)
137 | require.NoError(t, err)
138 |
139 | status, err := GetTxStatus(txHash)
140 | require.NoError(t, err)
141 | require.Equal(t, types.TxStatusUnknown, status.Status)
142 | })
143 |
144 | // Send a tx
145 | // NOTE: this portion is somewhat brittle and possibly prone to breakage
146 | // if we see this test failing then we should invest in proper mock tooling
147 | // around RpcRequest.
148 | t.Run("send tx", func(t *testing.T) {
149 | r := RpcRequest{}
150 | r.jsonReq = &types.JsonRpcRequest{
151 | Id: 1,
152 | Method: "eth_sendRawTransaction",
153 | Params: []any{
154 | "0xf86c258502540be40083035b609482e041e84074fc5f5947d4d27e3c44f824b7a1a187b1a2bc2ec500008078a04a7db627266fa9a4116e3f6b33f5d245db40983234eb356261f36808909d2848a0166fa098a2ce3bda87af6000ed0083e3bf7cc31c6686b670bd85cbc6da2d6e85",
155 | },
156 | Version: "2.0",
157 | }
158 | r.logger = log.New()
159 | r.client = mockClient
160 | r.ethSendRawTxEntry = &database.EthSendRawTxEntry{}
161 | r.relaySigningKey = privKey
162 | mockClient.nextResponse = &http.Response{
163 | StatusCode: http.StatusOK,
164 | Body: io.NopCloser(bytes.NewBufferString(`{"results":"0x58e5a0fc7fbc849eddc100d44e86276168a8c7baaa5604e44ba6f5eb8ba1b7eb"}`)),
165 | }
166 |
167 | // we expect handle_sendRawTransaction to set the
168 | // nonce in the RState cache to the txs nonce (0x25)
169 | r.handle_sendRawTransaction()
170 |
171 | // but actually the nonce is set asynchronously so we need to first
172 | // sleep until we see it in the cache.
173 | for i := 0; i < 5; i++ {
174 | _, found, _ := RState.GetSenderMaxNonce(sender)
175 | if !found {
176 | time.Sleep(time.Millisecond * time.Duration(i*10))
177 | }
178 | }
179 |
180 | // once found we can check the results
181 | require.Equal(t, r.tx.Nonce(), uint64(0x25))
182 | maxNonce, found, err := RState.GetSenderMaxNonce(sender)
183 | require.NoError(t, err)
184 | require.True(t, found)
185 | require.Equal(t, r.tx.Nonce(), maxNonce)
186 | })
187 |
188 | // mark tx failed in mock status API
189 | t.Run("fail tx", func(t *testing.T) {
190 | testutils.MockTxApiStatusForHash[txHash] = types.TxStatusFailed
191 | })
192 |
193 | // and now simulate the user sending a eth_getTransactionReceipt request
194 | t.Run("eth_getTransactionReceipt", func(t *testing.T) {
195 | r := RpcRequest{}
196 | r.logger = log.New()
197 | r.jsonReq = &types.JsonRpcRequest{
198 | Id: 1,
199 | Method: "eth_getTransactionReceipt",
200 | Params: []any{txHash},
201 | Version: "2.0",
202 | }
203 | response := &types.JsonRpcResponse{
204 | Id: 1,
205 | Result: json.RawMessage(`null`),
206 | Error: nil,
207 | Version: "2.0",
208 | }
209 | r.check_post_getTransactionReceipt(response)
210 | })
211 |
212 | // ensure that the max nonce is cleared
213 | t.Run("check nonce", func(t *testing.T) {
214 | maxNonce, found, err := RState.GetSenderMaxNonce(sender)
215 | require.NoError(t, err)
216 | require.Equal(t, uint64(0x0), maxNonce)
217 | require.False(t, found)
218 | })
219 | }
220 |
--------------------------------------------------------------------------------
/server/request_record.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "sync"
7 |
8 | "github.com/flashbots/rpc-endpoint/database"
9 | "github.com/google/uuid"
10 | )
11 |
12 | type requestRecord struct {
13 | requestEntry database.RequestEntry
14 | ethSendRawTxEntries []*database.EthSendRawTxEntry
15 | mutex sync.Mutex
16 | db database.Store
17 | }
18 |
19 | func NewRequestRecord(db database.Store) *requestRecord {
20 | return &requestRecord{
21 | db: db,
22 | }
23 | }
24 |
25 | func (r *requestRecord) AddEthSendRawTxEntry(id uuid.UUID) *database.EthSendRawTxEntry {
26 | entry := &database.EthSendRawTxEntry{
27 | Id: id,
28 | RequestId: r.requestEntry.Id,
29 | }
30 | r.mutex.Lock()
31 | defer r.mutex.Unlock()
32 | r.ethSendRawTxEntries = append(r.ethSendRawTxEntries, entry)
33 | return entry
34 | }
35 |
36 | func (r *requestRecord) UpdateRequestEntry(req *http.Request, reqStatus int, error string) {
37 | r.requestEntry.HttpMethod = req.Method
38 | r.requestEntry.Error = error
39 | r.requestEntry.HttpUrl = req.URL.Path
40 | r.requestEntry.HttpQueryParam = req.URL.RawQuery
41 | r.requestEntry.HttpResponseStatus = reqStatus
42 | r.requestEntry.Origin = req.Header.Get("Origin")
43 | r.requestEntry.Host = req.Header.Get("Host")
44 | }
45 |
46 | // SaveRecord will insert both requestRecord and rawTxEntries to db
47 | func (r *requestRecord) SaveRecord() error {
48 | entries := r.getValidRawTxEntriesToSave()
49 | if len(entries) > 0 { // Save entries if the request contains rawTxEntries
50 | if err := r.db.SaveRequestEntry(r.requestEntry); err != nil {
51 | return fmt.Errorf("SaveRequestEntry failed %v", err)
52 | }
53 | if err := r.db.SaveRawTxEntries(entries); err != nil {
54 | return fmt.Errorf("SaveRawTxEntries failed %v", err)
55 | }
56 | }
57 | return nil
58 | }
59 |
60 | // getValidRawTxEntriesToSave returns list of rawTxEntry which are either sent to relay or mempool or entry with error
61 | func (r *requestRecord) getValidRawTxEntriesToSave() []*database.EthSendRawTxEntry {
62 | entries := make([]*database.EthSendRawTxEntry, 0, len(r.ethSendRawTxEntries))
63 | r.mutex.Lock()
64 | defer r.mutex.Unlock()
65 | for _, entry := range r.ethSendRawTxEntries {
66 | if entry.ErrorCode != 0 || entry.WasSentToRelay || entry.WasSentToMempool {
67 | entries = append(entries, entry)
68 | }
69 | }
70 | return entries
71 | }
72 |
--------------------------------------------------------------------------------
/server/request_record_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "github.com/flashbots/rpc-endpoint/database"
5 | "github.com/google/uuid"
6 | "github.com/stretchr/testify/require"
7 | "sync"
8 | "testing"
9 | )
10 |
11 | func Test_requestRecord_getForwardedRawTxEntries(t *testing.T) {
12 | tests := map[string]struct {
13 | ethSendRawTxEntries []*database.EthSendRawTxEntry
14 | want []*database.EthSendRawTxEntry
15 | len int
16 | }{
17 | "Should return ethSendRawTxEntries when request sent to mempool": {
18 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{WasSentToMempool: true}, {WasSentToMempool: true}},
19 | want: []*database.EthSendRawTxEntry{{WasSentToMempool: true}, {WasSentToMempool: true}},
20 | len: 2,
21 | },
22 | "Should return ethSendRawTxEntries when request sent to relay": {
23 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{WasSentToRelay: true}, {WasSentToRelay: true}},
24 | want: []*database.EthSendRawTxEntry{{WasSentToRelay: true}, {WasSentToRelay: true}},
25 | len: 2,
26 | },
27 | "Should return ethSendRawTxEntries when request entry has error": {
28 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{ErrorCode: -32600}, {ErrorCode: -32601}},
29 | want: []*database.EthSendRawTxEntry{{ErrorCode: -32600}, {ErrorCode: -32601}},
30 | len: 2,
31 | },
32 | "Should return empty ethSendRawTxEntries when request entry has no error and not sent to mempool as well as to relay": {
33 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{IsOnOafcList: true}, {IsCancelTx: true}},
34 | want: []*database.EthSendRawTxEntry{},
35 | len: 0,
36 | },
37 | "Should return ethSendRawTxEntries when request entry met entry condition": {
38 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{IsOnOafcList: true},
39 | {IsCancelTx: true}, {WasSentToRelay: true, ErrorCode: -32600}, {WasSentToMempool: true}},
40 | want: []*database.EthSendRawTxEntry{{WasSentToRelay: true, ErrorCode: -32600}, {WasSentToMempool: true}},
41 | len: 2,
42 | },
43 | }
44 | for testName, testCase := range tests {
45 | t.Run(testName, func(t *testing.T) {
46 | r := &requestRecord{
47 | ethSendRawTxEntries: testCase.ethSendRawTxEntries,
48 | mutex: sync.Mutex{},
49 | }
50 | got := r.getValidRawTxEntriesToSave()
51 | require.Equal(t, testCase.want, got)
52 | require.Equal(t, testCase.len, len(got))
53 | })
54 | }
55 | }
56 |
57 | func Test_requestRecord_SaveRecord(t *testing.T) {
58 | id1 := uuid.New()
59 | id2 := uuid.New()
60 | id3 := uuid.New()
61 | id4 := uuid.New()
62 | tests := map[string]struct {
63 | id uuid.UUID
64 | requestEntry database.RequestEntry
65 | ethSendRawTxEntries []*database.EthSendRawTxEntry
66 | rawTxEntryLen int
67 | }{
68 | "Should successfully store batch request": {
69 | id: id1,
70 | requestEntry: database.RequestEntry{Id: id1},
71 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{
72 | {RequestId: id1, WasSentToMempool: true}, {RequestId: id1, WasSentToMempool: true},
73 | {RequestId: id1, IsCancelTx: true}, {RequestId: id1, WasSentToRelay: true, ErrorCode: -32600}, {RequestId: id1, WasSentToMempool: true},
74 | },
75 | rawTxEntryLen: 4,
76 | },
77 | "Should successfully store single request": {
78 | id: id2,
79 | requestEntry: database.RequestEntry{Id: id2},
80 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{RequestId: id2, WasSentToMempool: true}},
81 | rawTxEntryLen: 1,
82 | },
83 | "Should successfully store single request with rawTxEntry has error": {
84 | id: id3,
85 | requestEntry: database.RequestEntry{Id: id3},
86 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{RequestId: id3, ErrorCode: -32600}},
87 | rawTxEntryLen: 1,
88 | },
89 | "Should not store if the request doesnt meet entry condition": {
90 | id: id4,
91 | requestEntry: database.RequestEntry{Id: id4},
92 | ethSendRawTxEntries: []*database.EthSendRawTxEntry{{RequestId: id4, IsCancelTx: true}},
93 | rawTxEntryLen: 0,
94 | },
95 | }
96 | for testName, testCase := range tests {
97 | t.Run(testName, func(t *testing.T) {
98 | db := database.NewMemStore()
99 | r := &requestRecord{
100 | requestEntry: testCase.requestEntry,
101 | ethSendRawTxEntries: testCase.ethSendRawTxEntries,
102 | mutex: sync.Mutex{},
103 | db: db,
104 | }
105 | r.SaveRecord()
106 | require.Equal(t, testCase.rawTxEntryLen, len(db.EthSendRawTxs[testCase.id]))
107 | })
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/server/request_response.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/flashbots/rpc-endpoint/types"
6 | "net/http"
7 | )
8 |
9 | func (r *RpcRequestHandler) writeHeaderContentTypeJson() {
10 | (*r.respw).Header().Set("Content-Type", "application/json")
11 | }
12 |
13 | func (r *RpcRequestHandler) _writeRpcResponse(res *types.JsonRpcResponse) {
14 | // If the request is single and not batch
15 | // Write content type
16 | r.writeHeaderContentTypeJson() // Set content type to json
17 | (*r.respw).WriteHeader(http.StatusOK)
18 | // Write response
19 | if err := json.NewEncoder(*r.respw).Encode(res); err != nil {
20 | r.logger.Error("[_writeRpcResponse] Failed writing rpc response", "error", err)
21 | (*r.respw).WriteHeader(http.StatusInternalServerError)
22 | }
23 | }
24 |
25 | //lint:ignore U1000 Ignore all unused code, it's generated
26 | func (r *RpcRequestHandler) _writeRpcBatchResponse(res []*types.JsonRpcResponse) {
27 | r.writeHeaderContentTypeJson() // Set content type to json
28 | (*r.respw).WriteHeader(http.StatusOK)
29 | // Write response
30 | if err := json.NewEncoder(*r.respw).Encode(res); err != nil {
31 | r.logger.Error("[_writeRpcBatchResponse] Failed writing rpc response", "error", err)
32 | (*r.respw).WriteHeader(http.StatusInternalServerError)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/request_sendrawtx.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | "math/big"
6 | "strings"
7 | "time"
8 |
9 | "github.com/ethereum/go-ethereum/common/hexutil"
10 | "github.com/flashbots/rpc-endpoint/types"
11 | )
12 |
13 | const (
14 | scMethodBytes = 4 // first 4 byte of data field
15 | )
16 |
17 | func (r *RpcRequest) handle_sendRawTransaction() {
18 | var err error
19 |
20 | // JSON-RPC sanity checks
21 | if len(r.jsonReq.Params) < 1 {
22 | r.logger.Info("[sendRawTransaction] No params for eth_sendRawTransaction")
23 | r.writeRpcError("empty params for eth_sendRawTransaction", types.JsonRpcInvalidParams)
24 | return
25 | }
26 |
27 | if r.jsonReq.Params[0] == nil {
28 | r.logger.Info("[sendRawTransaction] Nil param for eth_sendRawTransaction")
29 | r.writeRpcError("nil params for eth_sendRawTransaction", types.JsonRpcInvalidParams)
30 | }
31 |
32 | r.rawTxHex = r.jsonReq.Params[0].(string)
33 | if len(r.rawTxHex) < 2 {
34 | r.logger.Error("[sendRawTransaction] Invalid raw transaction (wrong length)")
35 | r.writeRpcError("invalid raw transaction param (wrong length)", types.JsonRpcInvalidParams)
36 | return
37 | }
38 |
39 | // r.logger.Info("[sendRawTransaction] Raw tx value", "tx", r.rawTxHex, "txHash", r.tx.Hash())
40 | r.ethSendRawTxEntry.TxRaw = r.rawTxHex
41 | r.tx, err = GetTx(r.rawTxHex)
42 | if err != nil {
43 | r.logger.Info("[sendRawTransaction] Reading transaction object failed", "tx", r.rawTxHex)
44 | r.writeRpcError(fmt.Sprintf("reading transaction object failed - rawTx: %s", r.rawTxHex), types.JsonRpcInvalidRequest)
45 | return
46 | }
47 | r.ethSendRawTxEntry.TxHash = r.tx.Hash().String()
48 | r.logger = r.logger.New("txHash", r.tx.Hash().String())
49 | // Get address from tx
50 | r.logger.Info("[sendRawTransaction] start to process raw tx", "txHash", r.tx.Hash(), "timestamp", time.Now().Unix(), "time", time.Now().UTC())
51 | r.txFrom, err = GetSenderFromRawTx(r.tx)
52 | if err != nil {
53 |
54 | r.logger.Info("[sendRawTransaction] Couldn't get address from rawTx", "error", err)
55 | r.writeRpcError(fmt.Sprintf("couldn't get address from rawTx: %v", err), types.JsonRpcInvalidRequest)
56 | return
57 | }
58 |
59 | r.logger.Info("[sendRawTransaction] sending raw transaction", "tx", r.rawTxHex, "fromAddress", r.txFrom, "toAddress", AddressPtrToStr(r.tx.To()), "txNonce", r.tx.Nonce(), "txGasPrice", BigIntPtrToStr(r.tx.GasPrice()))
60 | txFromLower := strings.ToLower(r.txFrom)
61 |
62 | // store tx info to ethSendRawTxEntries which will be stored in db for data analytics reason
63 | r.ethSendRawTxEntry.TxFrom = r.txFrom
64 | r.ethSendRawTxEntry.TxTo = AddressPtrToStr(r.tx.To())
65 | r.ethSendRawTxEntry.TxNonce = int(r.tx.Nonce())
66 |
67 | if len(r.tx.Data()) > 0 {
68 | r.ethSendRawTxEntry.TxData = hexutil.Encode(r.tx.Data())
69 | }
70 |
71 | if len(r.tx.Data()) >= scMethodBytes {
72 | r.ethSendRawTxEntry.TxSmartContractMethod = hexutil.Encode(r.tx.Data()[:scMethodBytes])
73 | }
74 |
75 | if r.tx.Nonce() >= 1e9 {
76 | r.logger.Info("[sendRawTransaction] tx rejected - nonce too high", "txNonce", r.tx.Nonce(), "txFromLower", txFromLower, "origin", r.origin)
77 | r.writeRpcError("tx rejected - nonce too high", types.JsonRpcInvalidRequest)
78 | return
79 | }
80 |
81 | txHashLower := strings.ToLower(r.tx.Hash().Hex())
82 | // Check if tx was blocked (eg. "nonce too low")
83 | retVal, isBlocked, _ := RState.GetBlockedTxHash(txHashLower)
84 | if isBlocked {
85 | r.logger.Info("[sendRawTransaction] tx blocked", "retVal", retVal)
86 | r.writeRpcError(retVal, types.JsonRpcInternalError)
87 | return
88 | }
89 |
90 | // Remember sender and nonce of the tx, for lookup in getTransactionReceipt to possibly set nonce-fix
91 | err = RState.SetSenderAndNonceOfTxHash(txHashLower, txFromLower, r.tx.Nonce())
92 | if err != nil {
93 | r.logger.Error("[sendRawTransaction] Redis:SetSenderAndNonceOfTxHash failed: %v", err)
94 | }
95 | var txToAddr string
96 | if r.tx.To() != nil { // to address will be nil for contract creation tx
97 | txToAddr = r.tx.To().String()
98 | }
99 | isOnOfacList := isOnOFACList(r.txFrom) || isOnOFACList(txToAddr)
100 | r.ethSendRawTxEntry.IsOnOafcList = isOnOfacList
101 | if isOnOfacList {
102 | r.logger.Info("[sendRawTransaction] Blocked tx due to ofac sanctioned address", "txFrom", r.txFrom, "txTo", txToAddr)
103 | r.writeRpcError("blocked tx due to ofac sanctioned address", types.JsonRpcInvalidRequest)
104 | return
105 | }
106 |
107 | // Check if transaction needs protection
108 | r.ethSendRawTxEntry.NeedsFrontRunningProtection = true
109 | // If users specify a bundle ID, cache this transaction
110 | if r.isWhitehatBundleCollection {
111 | r.logger.Info("[WhitehatBundleCollection] Adding tx to bundle", "whiteHatBundleId", r.whitehatBundleId, "tx", r.rawTxHex)
112 | err = RState.AddTxToWhitehatBundle(r.whitehatBundleId, r.rawTxHex)
113 | if err != nil {
114 | r.logger.Error("[WhitehatBundleCollection] AddTxToWhitehatBundle failed", "error", err)
115 | r.writeRpcError("[WhitehatBundleCollection] AddTxToWhitehatBundle failed:", types.JsonRpcInternalError)
116 | return
117 | }
118 | r.writeRpcResult(r.tx.Hash().Hex())
119 | return
120 | }
121 |
122 | // Check for cancellation-tx
123 | if r.tx.To() != nil && len(r.tx.Data()) <= 2 && txFromLower == strings.ToLower(r.tx.To().Hex()) {
124 | r.ethSendRawTxEntry.IsCancelTx = true
125 | requestDone := r.handleCancelTx() // returns true if tx was cancelled at the relay and response has been sent to the user
126 | if !requestDone {
127 | r.ethSendRawTxEntry.IsCancelTx = false
128 | r.logger.Warn("[cancel-tx] This is not a cancellation tx, since we don't have original one. So we process it as usual tx", "txFromLower", txFromLower, "txNonce", r.tx.Nonce())
129 | r.sendTxToRelay()
130 | }
131 | return
132 | }
133 |
134 | // do it as the last step, in case it is used as cancellation
135 | if r.tx.GasTipCap().Cmp(big.NewInt(0)) == 0 {
136 | r.writeRpcError("transaction underpriced: gas tip cap 0, minimum needed 1", types.JsonRpcInvalidRequest)
137 | return
138 | }
139 | r.sendTxToRelay()
140 | }
141 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "crypto/ecdsa"
6 | "encoding/json"
7 | "io"
8 | "net/http"
9 | _ "net/http/pprof"
10 | "os"
11 | "os/signal"
12 | "runtime"
13 | "strings"
14 | "sync"
15 | "syscall"
16 | "time"
17 |
18 | "github.com/ethereum/go-ethereum/ethclient"
19 | "github.com/flashbots/rpc-endpoint/adapters/webfile"
20 | "github.com/flashbots/rpc-endpoint/application"
21 | "github.com/flashbots/rpc-endpoint/database"
22 |
23 | "github.com/ethereum/go-ethereum/log"
24 |
25 | "github.com/alicebob/miniredis"
26 | "github.com/pkg/errors"
27 |
28 | "github.com/flashbots/rpc-endpoint/types"
29 | )
30 |
31 | var Now = time.Now // used to mock time in tests
32 |
33 | var DebugDontSendTx = os.Getenv("DEBUG_DONT_SEND_RAWTX") != ""
34 |
35 | // Metamask fix helper
36 | var RState *RedisState
37 |
38 | type BuilderNameProvider interface {
39 | BuilderNames() []string
40 | }
41 | type RpcEndPointServer struct {
42 | server *http.Server
43 | drain *http.Server
44 |
45 | drainAddress string
46 | drainSeconds int
47 | db database.Store
48 | isHealthy bool
49 | isHealthyMx sync.RWMutex
50 | listenAddress string
51 | logger log.Logger
52 | proxyTimeoutSeconds int
53 | proxyUrl string
54 | relaySigningKey *ecdsa.PrivateKey
55 | relayUrl string
56 | startTime time.Time
57 | version string
58 | builderNameProvider BuilderNameProvider
59 | chainID []byte
60 | rpcCache *application.RpcCache
61 | defaultEthClient *ethclient.Client
62 | }
63 |
64 | func NewRpcEndPointServer(cfg Configuration) (*RpcEndPointServer, error) {
65 | var err error
66 | if DebugDontSendTx {
67 | cfg.Logger.Info("DEBUG MODE: raw transactions will not be sent out!", "redisUrl", cfg.RedisUrl)
68 | }
69 |
70 | if cfg.RedisUrl == "dev" {
71 | cfg.Logger.Info("Using integrated in-memory Redis instance", "redisUrl", cfg.RedisUrl)
72 | redisServer, err := miniredis.Run()
73 | if err != nil {
74 | return nil, err
75 | }
76 | cfg.RedisUrl = redisServer.Addr()
77 | }
78 | // Setup redis connection
79 | cfg.Logger.Info("Connecting to redis...", "redisUrl", cfg.RedisUrl)
80 | RState, err = NewRedisState(cfg.RedisUrl)
81 | if err != nil {
82 | return nil, errors.Wrap(err, "Redis init error")
83 | }
84 | var builderInfoFetcher application.Fetcher
85 | if cfg.BuilderInfoSource != "" {
86 | builderInfoFetcher = webfile.NewFetcher(cfg.BuilderInfoSource)
87 | }
88 | bis, err := application.StartBuilderInfoService(context.Background(), builderInfoFetcher, time.Second*time.Duration(cfg.FetchInfoInterval))
89 | if err != nil {
90 | return nil, errors.Wrap(err, "BuilderInfoService init error")
91 | }
92 |
93 | bts, err := fetchNetworkIDBytes(cfg)
94 | if err != nil {
95 | return nil, errors.Wrap(err, "fetchNetworkIDBytes error")
96 | }
97 |
98 | rpcCache := application.NewRpcCache(cfg.TTLCacheSeconds)
99 | ethCl, err := ethclient.Dial(cfg.DefaultMempoolRPC)
100 | if err != nil {
101 | return nil, errors.Wrap(err, "ethclient.Dial error")
102 | }
103 | return &RpcEndPointServer{
104 | db: cfg.DB,
105 | drainAddress: cfg.DrainAddress,
106 | drainSeconds: cfg.DrainSeconds,
107 | isHealthy: true,
108 | listenAddress: cfg.ListenAddress,
109 | logger: cfg.Logger,
110 | proxyTimeoutSeconds: cfg.ProxyTimeoutSeconds,
111 | proxyUrl: cfg.ProxyUrl,
112 | relaySigningKey: cfg.RelaySigningKey,
113 | relayUrl: cfg.RelayUrl,
114 | startTime: Now(),
115 | version: cfg.Version,
116 | chainID: bts,
117 | builderNameProvider: bis,
118 | rpcCache: rpcCache,
119 | defaultEthClient: ethCl,
120 | }, nil
121 | }
122 |
123 | func fetchNetworkIDBytes(cfg Configuration) ([]byte, error) {
124 |
125 | cl := NewRPCProxyClient(cfg.Logger, cfg.ProxyUrl, cfg.ProxyTimeoutSeconds, 0)
126 |
127 | _req := types.NewJsonRpcRequest(1, "net_version", []interface{}{})
128 | jsonData, err := json.Marshal(_req)
129 | if err != nil {
130 | return nil, errors.Wrap(err, "network version request failed")
131 | }
132 | httpRes, err := cl.ProxyRequest(jsonData)
133 | if err != nil {
134 | return nil, errors.Wrap(err, "cl.NetworkID error")
135 | }
136 |
137 | resBytes, err := io.ReadAll(httpRes.Body)
138 | httpRes.Body.Close()
139 | if err != nil {
140 | return nil, err
141 | }
142 | _res, err := respBytesToJsonRPCResponse(resBytes)
143 | if err != nil {
144 | return nil, err
145 | }
146 |
147 | return _res.Result, nil
148 | }
149 |
150 | func (s *RpcEndPointServer) Start() {
151 | s.logger.Info("Starting rpc endpoint...", "version", s.version, "listenAddress", s.listenAddress)
152 |
153 | // Regularly log debug info
154 | go func() {
155 | for {
156 | s.logger.Info("[stats] num-goroutines", "count", runtime.NumGoroutine())
157 | time.Sleep(10 * time.Second)
158 | }
159 | }()
160 |
161 | s.startMainServer()
162 | s.startDrainServer()
163 |
164 | notifier := make(chan os.Signal, 1)
165 | signal.Notify(notifier, os.Interrupt, syscall.SIGTERM)
166 |
167 | <-notifier
168 |
169 | s.stopDrainServer()
170 | s.stopMainServer()
171 | }
172 |
173 | func (s *RpcEndPointServer) startMainServer() {
174 | if s.server != nil {
175 | panic("http server is already running")
176 | }
177 | // Handler for root URL (JSON-RPC on POST, public/index.html on GET)
178 | mux := http.NewServeMux()
179 | mux.HandleFunc("/", s.HandleHttpRequest)
180 | mux.HandleFunc("/health", s.handleHealthRequest)
181 | mux.HandleFunc("/bundle", s.HandleBundleRequest)
182 | s.server = &http.Server{
183 | Addr: s.listenAddress,
184 | Handler: mux,
185 | WriteTimeout: 30 * time.Second,
186 | ReadTimeout: 30 * time.Second,
187 | }
188 | go func() {
189 | if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
190 | s.logger.Error("http server failed", "error", err)
191 | }
192 | }()
193 | }
194 |
195 | func (s *RpcEndPointServer) stopMainServer() {
196 | if s.server != nil {
197 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
198 | defer cancel()
199 | if err := s.server.Shutdown(ctx); err != nil {
200 | s.logger.Error("http server shutdown failed", "error", err)
201 | }
202 | s.logger.Info("http server stopped")
203 | s.server = nil
204 | }
205 | }
206 |
207 | func (s *RpcEndPointServer) startDrainServer() {
208 | if s.drain != nil {
209 | panic("drain http server is already running")
210 | }
211 | mux := http.NewServeMux()
212 | mux.HandleFunc("/", s.handleDrain)
213 | s.drain = &http.Server{
214 | Addr: s.drainAddress,
215 | Handler: mux,
216 | }
217 | go func() {
218 | if err := s.drain.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
219 | s.logger.Error("drain http server failed", "error", err)
220 | }
221 | }()
222 | }
223 |
224 | func (s *RpcEndPointServer) stopDrainServer() {
225 | if s.drain != nil {
226 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
227 | defer cancel()
228 | if err := s.drain.Shutdown(ctx); err != nil {
229 | s.logger.Error("drain http server shutdown failed", "error", err)
230 | }
231 | s.logger.Info("drain http server stopped")
232 | s.drain = nil
233 | }
234 | }
235 |
236 | func (s *RpcEndPointServer) HandleHttpRequest(respw http.ResponseWriter, req *http.Request) {
237 | setCorsHeaders(respw)
238 |
239 | if req.Method == http.MethodGet {
240 | if strings.Trim(req.URL.Path, "/") == "fast" {
241 | http.Redirect(respw, req, "https://docs.flashbots.net/flashbots-protect/quick-start#faster-transactions", http.StatusFound)
242 | } else {
243 | http.Redirect(respw, req, "https://docs.flashbots.net/flashbots-protect/rpc/quick-start/", http.StatusFound)
244 | }
245 | return
246 | }
247 |
248 | if req.Method == http.MethodOptions {
249 | respw.WriteHeader(http.StatusOK)
250 | return
251 | }
252 |
253 | request := NewRpcRequestHandler(s.logger, &respw, req, s.proxyUrl, s.proxyTimeoutSeconds, s.relaySigningKey, s.relayUrl, s.db, s.builderNameProvider.BuilderNames(), s.chainID, s.rpcCache, s.defaultEthClient)
254 | request.process()
255 | }
256 |
257 | func (s *RpcEndPointServer) handleDrain(respw http.ResponseWriter, req *http.Request) {
258 | s.isHealthyMx.Lock()
259 | if !s.isHealthy {
260 | s.isHealthyMx.Unlock()
261 | return
262 | }
263 |
264 | s.isHealthy = false
265 | s.logger.Info("Server marked as unhealthy")
266 |
267 | // Let's not hold onto the lock in our sleep
268 | s.isHealthyMx.Unlock()
269 |
270 | // Give LB enough time to detect us unhealthy
271 | time.Sleep(
272 | time.Duration(s.drainSeconds) * time.Second,
273 | )
274 | }
275 |
276 | func (s *RpcEndPointServer) handleHealthRequest(respw http.ResponseWriter, req *http.Request) {
277 | s.isHealthyMx.RLock()
278 | defer s.isHealthyMx.RUnlock()
279 | res := types.HealthResponse{
280 | Now: Now(),
281 | StartTime: s.startTime,
282 | Version: s.version,
283 | }
284 |
285 | jsonResp, err := json.Marshal(res)
286 | if err != nil {
287 | s.logger.Info("[healthCheck] Json error", "error", err)
288 | respw.WriteHeader(http.StatusInternalServerError)
289 | return
290 | }
291 |
292 | respw.Header().Set("Content-Type", "application/json")
293 | if s.isHealthy {
294 | respw.WriteHeader(http.StatusOK)
295 | } else {
296 | respw.WriteHeader(http.StatusInternalServerError)
297 | }
298 | respw.Write(jsonResp)
299 | }
300 |
301 | func (s *RpcEndPointServer) HandleBundleRequest(respw http.ResponseWriter, req *http.Request) {
302 | setCorsHeaders(respw)
303 | bundleId := req.URL.Query().Get("id")
304 | if bundleId == "" {
305 | http.Error(respw, "no bundle id", http.StatusBadRequest)
306 | return
307 | }
308 |
309 | if req.Method == http.MethodGet {
310 | txs, err := RState.GetWhitehatBundleTx(bundleId)
311 | if err != nil {
312 | s.logger.Info("[handleBundleRequest] GetWhitehatBundleTx failed", "bundleId", bundleId, "error", err)
313 | respw.WriteHeader(http.StatusInternalServerError)
314 | return
315 | }
316 |
317 | res := types.BundleResponse{
318 | BundleId: bundleId,
319 | RawTxs: txs,
320 | }
321 |
322 | jsonResp, err := json.Marshal(res)
323 | if err != nil {
324 | s.logger.Info("[handleBundleRequest] Json marshal failed", "error", err)
325 | respw.WriteHeader(http.StatusInternalServerError)
326 | return
327 | }
328 | respw.Header().Set("Content-Type", "application/json")
329 | respw.WriteHeader(http.StatusOK)
330 | respw.Write(jsonResp)
331 |
332 | } else if req.Method == http.MethodDelete {
333 | RState.DelWhitehatBundleTx(bundleId)
334 | respw.WriteHeader(http.StatusOK)
335 |
336 | } else {
337 | respw.WriteHeader(http.StatusMethodNotAllowed)
338 | }
339 | }
340 |
341 | func setCorsHeaders(respw http.ResponseWriter) {
342 | respw.Header().Set("Access-Control-Allow-Origin", "*")
343 | respw.Header().Set("Access-Control-Allow-Headers", "Accept,Content-Type")
344 | }
345 |
--------------------------------------------------------------------------------
/server/url_params.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/ethereum/go-ethereum/common"
10 | "github.com/flashbots/rpc-endpoint/types"
11 | "github.com/pkg/errors"
12 | )
13 |
14 | var (
15 | DefaultAuctionHint = []string{"hash", "special_logs"}
16 |
17 | ErrIncorrectMempoolURL = errors.New("Incorrect mempool URL.")
18 | ErrIncorrectURLParam = errors.New("Incorrect URL parameter")
19 | ErrEmptyHintQuery = errors.New("Hint query must be non-empty if set.")
20 | ErrEmptyTargetBuilderQuery = errors.New("Target builder query must be non-empty if set.")
21 | ErrIncorrectAuctionHints = errors.New("Incorrect auction hint, must be one of: contract_address, function_selector, logs, calldata, default_logs.")
22 | ErrIncorrectOriginId = errors.New("Incorrect origin id, must be less then 255 char.")
23 | ErrIncorrectRefundQuery = errors.New("Incorrect refund query, must be 0xaddress:percentage.")
24 | ErrIncorrectRefundAddressQuery = errors.New("Incorrect refund address.")
25 | ErrIncorrectRefundPercentageQuery = errors.New("Incorrect refund percentage.")
26 | ErrIncorrectRefundTotalPercentageQuery = errors.New("Incorrect refund total percentage, must be bellow 100%.")
27 | )
28 |
29 | type URLParameters struct {
30 | pref types.PrivateTxPreferences
31 | prefWasSet bool
32 | originId string
33 | fast bool
34 | blockRange int
35 | auctionTimeout uint64
36 | }
37 |
38 | // normalizeQueryParams takes a URL and returns a map of query parameters with all keys normalized to lowercase.
39 | // This helps in making the parameter extraction case-insensitive.
40 | func normalizeQueryParams(url *url.URL) map[string][]string {
41 | normalizedQuery := make(map[string][]string)
42 | for key, values := range url.Query() {
43 | normalizedKey := strings.ToLower(key)
44 | normalizedQuery[normalizedKey] = values
45 | }
46 | return normalizedQuery
47 | }
48 |
49 | var allowedHints = map[string]struct{}{
50 | "hash": struct{}{},
51 | "contract_address": struct{}{},
52 | "function_selector": struct{}{},
53 | "logs": struct{}{},
54 | "calldata": struct{}{},
55 | "default_logs": struct{}{},
56 | "tx_hash": struct{}{},
57 | "full": struct{}{},
58 | }
59 |
60 | // ExtractParametersFromUrl extracts the auction preference from the url query
61 | // Allowed query params:
62 | // - hint: mev share hints, can be set multiple times, default: hash, special_logs
63 | // - originId: origin id, default: ""
64 | // - builder: target builder, can be set multiple times, default: empty (only send to flashbots builders)
65 | // - refund: refund in the form of 0xaddress:percentage, default: empty (will be set by default when backrun is produced)
66 | // - auctionTimeout: auction timeout in milliseconds
67 | // example: 0x123:80 - will refund 80% of the backrun profit to 0x123
68 | func ExtractParametersFromUrl(reqUrl *url.URL, allBuilders []string) (params URLParameters, err error) {
69 | if strings.HasPrefix(reqUrl.Path, "/fast") {
70 | params.fast = true
71 | }
72 | // Normalize all query parameters to lowercase keys
73 | normalizedQuery := normalizeQueryParams(reqUrl)
74 |
75 | var hint []string
76 | hintQuery, ok := normalizedQuery["hint"]
77 | if ok {
78 | if len(hintQuery) == 0 {
79 | return params, ErrEmptyHintQuery
80 | }
81 | for _, hint := range hintQuery {
82 | // valid hints are: "hash", "contract_address", "function_selector", "logs", "calldata"
83 | if _, ok := allowedHints[hint]; !ok {
84 | return params, ErrIncorrectAuctionHints
85 |
86 | }
87 | }
88 | hint = hintQuery
89 | params.prefWasSet = true
90 | } else {
91 | hint = DefaultAuctionHint
92 | }
93 | params.pref.Privacy.Hints = hint
94 |
95 | originIdQuery, ok := normalizedQuery["originid"]
96 | if ok {
97 | if len(originIdQuery) == 0 {
98 | return params, ErrIncorrectOriginId
99 | }
100 | params.originId = originIdQuery[0]
101 | }
102 |
103 | targetBuildersQuery, ok := normalizedQuery["builder"]
104 | if ok {
105 | if len(targetBuildersQuery) == 0 {
106 | return params, ErrEmptyTargetBuilderQuery
107 | }
108 | params.pref.Privacy.Builders = targetBuildersQuery
109 | }
110 | if params.fast {
111 | params.pref.Fast = true
112 | // set all builders no matter what's in the reqUrl
113 | params.pref.Privacy.Builders = allBuilders
114 | }
115 |
116 | refundAddressQuery, ok := normalizedQuery["refund"]
117 | if ok {
118 | if len(refundAddressQuery) == 0 {
119 | return params, ErrIncorrectRefundQuery
120 | }
121 |
122 | var (
123 | addresses = make([]common.Address, len(refundAddressQuery))
124 | percents = make([]int, len(refundAddressQuery))
125 | )
126 |
127 | for i, refundAddress := range refundAddressQuery {
128 | split := strings.Split(refundAddress, ":")
129 | if len(split) != 2 {
130 | return params, ErrIncorrectRefundQuery
131 | }
132 | if !common.IsHexAddress(split[0]) {
133 | return params, ErrIncorrectRefundAddressQuery
134 | }
135 | address := common.HexToAddress(split[0])
136 | percent, err := strconv.Atoi(split[1])
137 | if err != nil {
138 | return params, ErrIncorrectRefundPercentageQuery
139 | }
140 | if percent <= 0 || percent >= 100 {
141 | return params, ErrIncorrectRefundPercentageQuery
142 | }
143 | addresses[i] = address
144 | percents[i] = percent
145 | }
146 |
147 | // should not exceed 100%
148 | var totalRefund int
149 | for _, percent := range percents {
150 | totalRefund += percent
151 | }
152 | if totalRefund > 100 {
153 | return params, ErrIncorrectRefundTotalPercentageQuery
154 | }
155 |
156 | refundConfig := make([]types.RefundConfig, len(percents))
157 | for i, percent := range percents {
158 | refundConfig[i] = types.RefundConfig{
159 | Address: addresses[i],
160 | Percent: percent,
161 | }
162 | }
163 |
164 | params.pref.Validity.Refund = refundConfig
165 | }
166 |
167 | useMempoolQuery := normalizedQuery["usemempool"]
168 | if len(useMempoolQuery) != 0 && useMempoolQuery[0] == "true" {
169 | params.pref.Privacy.UseMempool = true
170 | }
171 | canRevertQuery := normalizedQuery["canrevert"]
172 | if len(canRevertQuery) != 0 && canRevertQuery[0] == "true" {
173 | params.pref.CanRevert = true
174 | }
175 | mempoolRPC := normalizedQuery["mempoolrpc"]
176 | if len(mempoolRPC) != 0 {
177 | cm, err := url.QueryUnescape(mempoolRPC[0])
178 | if err != nil {
179 | return params, ErrIncorrectMempoolURL
180 | }
181 | parsedUrl, err := url.Parse(cm)
182 | if err != nil {
183 | return params, ErrIncorrectMempoolURL
184 | }
185 | parsedUrl.Scheme = "https"
186 | params.pref.Privacy.MempoolRPC = parsedUrl.String()
187 | }
188 | blockRange := normalizedQuery["blockrange"]
189 | if len(blockRange) != 0 {
190 | brange, err := strconv.Atoi(blockRange[0])
191 | if err != nil || brange < 0 {
192 | return params, ErrIncorrectURLParam
193 | }
194 | params.blockRange = brange
195 | }
196 | auctionTimeout := normalizedQuery["auctiontimeout"]
197 | if len(auctionTimeout) != 0 {
198 | timeout, err := strconv.Atoi(auctionTimeout[0])
199 | if err != nil || timeout < 0 {
200 | return params, ErrIncorrectURLParam
201 | }
202 | params.auctionTimeout = uint64(timeout)
203 | }
204 |
205 | return params, nil
206 | }
207 |
208 | func AuctionPreferenceErrorToJSONRPCResponse(jsonReq *types.JsonRpcRequest, err error) *types.JsonRpcResponse {
209 | message := fmt.Sprintf("Invalid auction preference in the rpc endpoint url. %s", err.Error())
210 | return &types.JsonRpcResponse{
211 | Id: jsonReq.Id,
212 | Error: &types.JsonRpcError{Code: types.JsonRpcInvalidRequest, Message: message},
213 | Version: "2.0",
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/server/url_params_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "net/url"
5 | "testing"
6 |
7 | "github.com/ethereum/go-ethereum/common"
8 | "github.com/flashbots/rpc-endpoint/types"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestExtractAuctionPreferenceFromUrl(t *testing.T) {
13 | tests := map[string]struct {
14 | url string
15 | want URLParameters
16 | err error
17 | }{
18 | "no auction preference": {
19 | url: "https://rpc.flashbots.net",
20 | want: URLParameters{
21 | pref: types.PrivateTxPreferences{
22 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}},
23 | Validity: types.TxValidityPreferences{},
24 | },
25 | prefWasSet: false,
26 | originId: "",
27 | },
28 | err: nil,
29 | },
30 | "only hash hint": {
31 | url: "https://rpc.flashbots.net?hint=hash",
32 | want: URLParameters{
33 | pref: types.PrivateTxPreferences{
34 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash"}},
35 | Validity: types.TxValidityPreferences{},
36 | },
37 | prefWasSet: true,
38 | originId: "",
39 | },
40 | err: nil,
41 | },
42 | "correct hint preference": {
43 | url: "https://rpc.flashbots.net?hint=contract_address&hint=function_selector&hint=logs&hint=calldata&hint=hash",
44 | want: URLParameters{
45 | pref: types.PrivateTxPreferences{
46 | Privacy: types.TxPrivacyPreferences{Hints: []string{"contract_address", "function_selector", "logs", "calldata", "hash"}},
47 | Validity: types.TxValidityPreferences{},
48 | },
49 | prefWasSet: true,
50 | originId: "",
51 | },
52 | err: nil,
53 | },
54 | "incorrect hint preference": {
55 | url: "https://rpc.flashbots.net?hint=contract_address&hint=function_selector&hint=logs&hint=incorrect",
56 | want: URLParameters{},
57 | err: ErrIncorrectAuctionHints,
58 | },
59 | "rpc endpoint set": {
60 | url: "https://rpc.flashbots.net?rpc=https://mainnet.infura.io/v3/123",
61 | want: URLParameters{
62 | pref: types.PrivateTxPreferences{
63 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}},
64 | Validity: types.TxValidityPreferences{},
65 | },
66 | prefWasSet: false,
67 | originId: "",
68 | },
69 | err: nil,
70 | },
71 | "origin id": {
72 | url: "https://rpc.flashbots.net?originId=123",
73 | want: URLParameters{
74 | pref: types.PrivateTxPreferences{
75 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}},
76 | Validity: types.TxValidityPreferences{},
77 | },
78 | prefWasSet: false,
79 | originId: "123",
80 | },
81 | err: nil,
82 | },
83 | "origin id common spelling 1": {
84 | url: "https://rpc.flashbots.net?originid=123",
85 | want: URLParameters{
86 | pref: types.PrivateTxPreferences{
87 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}},
88 | Validity: types.TxValidityPreferences{},
89 | },
90 | prefWasSet: false,
91 | originId: "123",
92 | },
93 | err: nil,
94 | },
95 | "origin id common spelling 2": {
96 | url: "https://rpc.flashbots.net?originID=123",
97 | want: URLParameters{
98 | pref: types.PrivateTxPreferences{
99 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}},
100 | Validity: types.TxValidityPreferences{},
101 | },
102 | prefWasSet: false,
103 | originId: "123",
104 | },
105 | err: nil,
106 | },
107 | "target builder": {
108 | url: "https://rpc.flashbots.net?builder=builder1&builder=builder2",
109 | want: URLParameters{
110 | pref: types.PrivateTxPreferences{
111 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}, Builders: []string{"builder1", "builder2"}},
112 | Validity: types.TxValidityPreferences{},
113 | },
114 | prefWasSet: false,
115 | originId: "",
116 | },
117 | err: nil,
118 | },
119 | "set refund": {
120 | url: "https://rpc.flashbots.net?refund=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:17",
121 | want: URLParameters{
122 | pref: types.PrivateTxPreferences{
123 | Privacy: types.TxPrivacyPreferences{
124 | Hints: []string{"hash", "special_logs"},
125 | },
126 | Validity: types.TxValidityPreferences{
127 | Refund: []types.RefundConfig{{Address: common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), Percent: 17}},
128 | },
129 | },
130 | prefWasSet: false,
131 | originId: "",
132 | },
133 | err: nil,
134 | },
135 | "set refund, two addresses": {
136 | url: "https://rpc.flashbots.net?&refund=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:70&refund=0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:10",
137 | want: URLParameters{
138 | pref: types.PrivateTxPreferences{
139 | Privacy: types.TxPrivacyPreferences{
140 | Hints: []string{"hash", "special_logs"},
141 | },
142 | Validity: types.TxValidityPreferences{
143 | Refund: []types.RefundConfig{
144 | {Address: common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), Percent: 70},
145 | {Address: common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), Percent: 10},
146 | },
147 | },
148 | },
149 | prefWasSet: false,
150 | originId: "",
151 | },
152 | err: nil,
153 | },
154 | "set refund, incorrect query": {
155 | url: "https://rpc.flashbots.net?refund",
156 | want: URLParameters{
157 | pref: types.PrivateTxPreferences{},
158 | prefWasSet: false,
159 | originId: "",
160 | },
161 | err: ErrIncorrectRefundQuery,
162 | },
163 | "set refund, incorrect 110": {
164 | url: "https://rpc.flashbots.net?refund=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:110",
165 | want: URLParameters{
166 | pref: types.PrivateTxPreferences{},
167 | prefWasSet: false,
168 | originId: "",
169 | },
170 | err: ErrIncorrectRefundPercentageQuery,
171 | },
172 | "set refund, incorrect address": {
173 | url: "https://rpc.flashbots.net?refund=0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:80",
174 | want: URLParameters{
175 | pref: types.PrivateTxPreferences{},
176 | prefWasSet: false,
177 | originId: "",
178 | },
179 | err: ErrIncorrectRefundAddressQuery,
180 | },
181 | "set refund, incorrect 50 + 60": {
182 | url: "https://rpc.flashbots.net?refund=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:50&refund=0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:60",
183 | want: URLParameters{
184 | pref: types.PrivateTxPreferences{},
185 | prefWasSet: false,
186 | originId: "",
187 | },
188 | err: ErrIncorrectRefundTotalPercentageQuery,
189 | },
190 | "fast": {
191 | url: "https://rpc.flashbots.net/fast",
192 | want: URLParameters{
193 | pref: types.PrivateTxPreferences{
194 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}, Builders: []string{"builder1", "builder2"}},
195 | Fast: true,
196 | },
197 | prefWasSet: false,
198 | fast: true,
199 | originId: "",
200 | },
201 | err: nil,
202 | },
203 | "fast, ignore builders": {
204 | url: "https://rpc.flashbots.net/fast?builder=builder3&builder=builder4",
205 | want: URLParameters{
206 | pref: types.PrivateTxPreferences{
207 | Privacy: types.TxPrivacyPreferences{Hints: []string{"hash", "special_logs"}, Builders: []string{"builder1", "builder2"}},
208 | Fast: true,
209 | },
210 | prefWasSet: false,
211 | fast: true,
212 | originId: "",
213 | },
214 | err: nil,
215 | },
216 | "fast, keep hints": {
217 | url: "https://rpc.flashbots.net/fast?hint=contract_address&hint=function_selector&hint=logs&hint=calldata&hint=hash",
218 | want: URLParameters{
219 | pref: types.PrivateTxPreferences{
220 | Privacy: types.TxPrivacyPreferences{Hints: []string{"contract_address", "function_selector", "logs", "calldata", "hash"}, Builders: []string{"builder1", "builder2"}},
221 | Fast: true,
222 | },
223 | prefWasSet: true,
224 | fast: true,
225 | originId: "",
226 | },
227 | err: nil,
228 | },
229 | "fast, keep hints, auctionTimeout": {
230 | url: "https://rpc.flashbots.net/fast?hint=contract_address&hint=function_selector&hint=logs&hint=calldata&hint=hash&auctionTimeout=1000",
231 | want: URLParameters{
232 | pref: types.PrivateTxPreferences{
233 | Privacy: types.TxPrivacyPreferences{Hints: []string{"contract_address", "function_selector", "logs", "calldata", "hash"}, Builders: []string{"builder1", "builder2"}},
234 | Fast: true,
235 | },
236 | prefWasSet: true,
237 | fast: true,
238 | originId: "",
239 | auctionTimeout: 1000,
240 | },
241 | err: nil,
242 | },
243 | }
244 |
245 | for name, tt := range tests {
246 | t.Run(name, func(t *testing.T) {
247 | url, err := url.Parse(tt.url)
248 | if err != nil {
249 | t.Fatal("failed to parse url: ", err)
250 | }
251 |
252 | got, err := ExtractParametersFromUrl(url, []string{"builder1", "builder2"})
253 | if tt.err != nil {
254 | require.ErrorIs(t, err, tt.err)
255 | } else {
256 | require.NoError(t, err)
257 | }
258 |
259 | if tt.err == nil {
260 | require.Equal(t, tt.want, got)
261 | }
262 | })
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/server/util.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "math/big"
10 | "net/http"
11 | "os"
12 | "strings"
13 |
14 | "github.com/ethereum/go-ethereum/common"
15 | ethtypes "github.com/ethereum/go-ethereum/core/types"
16 | "github.com/flashbots/rpc-endpoint/types"
17 | "github.com/pkg/errors"
18 | )
19 |
20 | func Min(a uint64, b uint64) uint64 {
21 | if a < b {
22 | return a
23 | }
24 | return b
25 | }
26 |
27 | func Max(a uint64, b uint64) uint64 {
28 | if a > b {
29 | return a
30 | }
31 | return b
32 | }
33 |
34 | func GetTx(rawTxHex string) (*ethtypes.Transaction, error) {
35 | if len(rawTxHex) < 2 {
36 | return nil, errors.New("invalid raw transaction")
37 | }
38 |
39 | rawTxBytes, err := hex.DecodeString(rawTxHex[2:])
40 | if err != nil {
41 | return nil, errors.New("invalid raw transaction")
42 | }
43 |
44 | tx := new(ethtypes.Transaction)
45 | if err = tx.UnmarshalBinary(rawTxBytes); err != nil {
46 | return nil, errors.New("error unmarshalling")
47 | }
48 |
49 | return tx, nil
50 | }
51 |
52 | func GetSenderAddressFromTx(tx *ethtypes.Transaction) (common.Address, error) {
53 | signer := ethtypes.LatestSignerForChainID(tx.ChainId())
54 | sender, err := ethtypes.Sender(signer, tx)
55 | if err != nil {
56 | return common.Address{}, err
57 | }
58 | return sender, nil
59 | }
60 | func GetSenderFromTx(tx *ethtypes.Transaction) (string, error) {
61 | signer := ethtypes.LatestSignerForChainID(tx.ChainId())
62 | sender, err := ethtypes.Sender(signer, tx)
63 | if err != nil {
64 | return "", err
65 | }
66 | return sender.Hex(), nil
67 | }
68 |
69 | func GetSenderFromRawTx(tx *ethtypes.Transaction) (string, error) {
70 | from, err := GetSenderFromTx(tx)
71 | if err != nil {
72 | return "", errors.New("error getting from")
73 | }
74 |
75 | return from, nil
76 | }
77 |
78 | func GetTxStatus(txHash string) (*types.PrivateTxApiResponse, error) {
79 | privTxApiUrl := fmt.Sprintf("%s/tx/%s", ProtectTxApiHost, txHash)
80 | resp, err := http.Get(privTxApiUrl)
81 | if err != nil {
82 | return nil, errors.Wrap(err, "privTxApi call failed for "+txHash)
83 | }
84 | defer resp.Body.Close()
85 |
86 | bodyBytes, err := io.ReadAll(resp.Body)
87 | if err != nil {
88 | return nil, errors.Wrap(err, "privTxApi body-read failed for "+txHash)
89 | }
90 |
91 | respObj := new(types.PrivateTxApiResponse)
92 | err = json.Unmarshal(bodyBytes, respObj)
93 | if err != nil {
94 | msg := fmt.Sprintf("privTxApi jsonUnmarshal failed for %s - status: %d / body: %s", txHash, resp.StatusCode, string(bodyBytes))
95 | return nil, errors.Wrap(err, msg)
96 | }
97 |
98 | return respObj, nil
99 | }
100 |
101 | func GetIP(r *http.Request) string {
102 | forwarded := r.Header.Get("X-Forwarded-For")
103 | if forwarded != "" {
104 | if strings.Contains(forwarded, ",") { // return first entry of list of IPs
105 | return strings.Split(forwarded, ",")[0]
106 | }
107 | return forwarded
108 | }
109 | return r.RemoteAddr
110 | }
111 |
112 | func GetIPHash(r *http.Request) string {
113 | hash := md5.Sum([]byte(GetIP(r)))
114 | return hex.EncodeToString(hash[:])
115 | }
116 |
117 | // CHROME_ID: nkbihfbeogaeaoehlefnkodbefgpgknn
118 | func IsMetamask(r *http.Request) bool {
119 | return r.Header.Get("Origin") == "chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn"
120 | }
121 |
122 | // FIREFOX_ID: webextension@metamask.io
123 | func IsMetamaskMoz(r *http.Request) bool {
124 | return r.Header.Get("Origin") == "moz-extension://57f9aaf6-270a-154f-9a8a-632d0db4128c"
125 | }
126 |
127 | func respBytesToJsonRPCResponse(respBytes []byte) (*types.JsonRpcResponse, error) {
128 |
129 | jsonRpcResp := new(types.JsonRpcResponse)
130 |
131 | // Check if returned an error, if so then convert to standard JSON-RPC error
132 | errorResp := new(types.RelayErrorResponse)
133 | if err := json.Unmarshal(respBytes, errorResp); err == nil && errorResp.Error != "" {
134 | // relay returned an error, convert to standard JSON-RPC error now
135 | jsonRpcResp.Error = &types.JsonRpcError{Message: errorResp.Error}
136 | return jsonRpcResp, nil
137 | }
138 |
139 | // Unmarshall JSON-RPC response and check for error inside
140 | if err := json.Unmarshal(respBytes, jsonRpcResp); err != nil {
141 | return nil, errors.Wrap(err, "unmarshal")
142 | }
143 |
144 | return jsonRpcResp, nil
145 | }
146 |
147 | func BigIntPtrToStr(i *big.Int) string {
148 | if i == nil {
149 | return ""
150 | }
151 | return i.String()
152 | }
153 |
154 | func AddressPtrToStr(a *common.Address) string {
155 | if a == nil {
156 | return ""
157 | }
158 | return a.Hex()
159 | }
160 |
161 | // GetEnv returns the value of the environment variable named by key, or defaultValue if the environment variable doesn't exist
162 | func GetEnv(key string, defaultValue string) string {
163 | if value, ok := os.LookupEnv(key); ok {
164 | return value
165 | }
166 | return defaultValue
167 | }
168 |
--------------------------------------------------------------------------------
/server/whitelist.go:
--------------------------------------------------------------------------------
1 | // Whitelist for smart contract functions that never need protection.
2 | package server
3 |
4 | import "strings"
5 |
6 | var allowedLargeTxTargets = map[string]bool{
7 | "0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455": true, // Aztec rollup contract
8 | "0x47312450B3Ac8b5b8e247a6bB6d523e7605bDb60": true, // StarkWare SHARP Verifier (Mainnet)
9 | "0x8f97970aC5a9aa8D130d35146F5b59c4aef57963": true, // StarkWare SHARP Verifier (Goerli)
10 | "0x07ec0D28e50322Eb0C159B9090ecF3aeA8346DFe": true, // StarkWare SHARP Verifier (Sepolia)
11 | }
12 |
13 | func init() {
14 | // Ensure that addresses are also indexed lowercase
15 | for target, val := range allowedLargeTxTargets {
16 | allowedLargeTxTargets[strings.ToLower(target)] = val
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/sql/psql/001_initial.schema.down.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 | DROP TABLE rpc_endpoint_requests;
3 | DROP TABLE rpc_endpoint_eth_send_raw_txs;
4 | COMMIT;
--------------------------------------------------------------------------------
/sql/psql/001_initial.schema.up.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 |
3 | CREATE TABLE rpc_endpoint_requests(
4 | id uuid not null unique primary key default gen_random_uuid(),
5 | received_at timestamp with time zone not null,
6 | inserted_at timestamp with time zone not null default now(),
7 | request_duration_ms bigint not null,
8 | is_batch_request boolean,
9 | num_request_in_batch integer,
10 | http_method varchar(10) not null,
11 | http_url varchar(100) not null,
12 | http_query_param text,
13 | http_response_status integer,
14 | origin text,
15 | host text,
16 | error text
17 | );
18 |
19 | CREATE TABLE rpc_endpoint_eth_send_raw_txs(
20 | id uuid not null unique primary key,
21 | request_id uuid not null,
22 | inserted_at timestamp with time zone not null default now(),
23 | is_on_oafc_list boolean,
24 | is_white_hat_bundle_collection boolean,
25 | white_hat_bundle_id varchar,
26 | is_cancel_tx boolean,
27 | needs_front_running_protection boolean,
28 | was_sent_to_relay boolean,
29 | was_sent_to_mempool boolean,
30 | is_blocked_bcz_already_sent boolean,
31 | error text,
32 | error_code integer,
33 | tx_raw text,
34 | tx_hash varchar(66),
35 | tx_from varchar(42),
36 | tx_to varchar(42),
37 | tx_nonce integer,
38 | tx_data text,
39 | tx_smart_contract_method varchar(10)
40 | );
41 |
42 | COMMIT;
43 |
--------------------------------------------------------------------------------
/sql/psql/002_add_fast.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE
2 | rpc_endpoint_eth_send_raw_txs
3 | DROP COLUMN
4 | fast;
5 |
--------------------------------------------------------------------------------
/sql/psql/002_add_fast.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE
2 | rpc_endpoint_eth_send_raw_txs
3 | ADD COLUMN
4 | fast boolean
5 | DEFAULT FALSE;
6 |
--------------------------------------------------------------------------------
/sql/psql/003_alter_is_blocked.down.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/rpc-endpoint/1af08813cf5603ea5e4fa1faa6349942458b6a8b/sql/psql/003_alter_is_blocked.down.sql
--------------------------------------------------------------------------------
/sql/psql/003_alter_is_blocked.up.sql:
--------------------------------------------------------------------------------
1 | alter table rpc_endpoint_eth_send_raw_txs rename column is_blocked_bcz_already_sent to is_blocked;
2 |
--------------------------------------------------------------------------------
/sql/redshift/001_initial.schema.down.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 | DROP TABLE rpc_endpoint_requests;
3 | DROP TABLE rpc_endpoint_eth_send_raw_txs;
4 | COMMIT;
--------------------------------------------------------------------------------
/sql/redshift/001_initial.schema.up.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 |
3 | CREATE TABLE rpc_endpoint_requests(
4 | id varchar(128) not null distkey,
5 | received_at timestamptz not null,
6 | inserted_at timestamptz not null default sysdate sortkey,
7 | request_duration_ms bigint not null,
8 | is_batch_request boolean,
9 | num_request_in_batch integer,
10 | http_method varchar(10) not null,
11 | http_url varchar(100) not null,
12 | http_query_param varchar(1024),
13 | http_response_status integer,
14 | origin varchar(100),
15 | host varchar(100),
16 | error varchar(1000)
17 | );
18 |
19 | CREATE TABLE rpc_endpoint_eth_send_raw_txs(
20 | id varchar(128) not null distkey,
21 | request_id varchar(128) not null,
22 | inserted_at timestamptz default sysdate sortkey,
23 | is_on_oafc_list boolean,
24 | is_white_hat_bundle_collection boolean,
25 | white_hat_bundle_id varchar(1024),
26 | is_cancel_tx boolean,
27 | needs_front_running_protection boolean,
28 | was_sent_to_relay boolean,
29 | was_sent_to_mempool boolean,
30 | is_blocked_bcz_already_sent boolean,
31 | error varchar(max),
32 | error_code integer,
33 | tx_raw varchar(max),
34 | tx_hash varchar(66),
35 | tx_from varchar(42),
36 | tx_to varchar(42),
37 | tx_nonce integer,
38 | tx_data varchar(max),
39 | tx_smart_contract_method varchar(10)
40 | );
41 |
42 | COMMIT;
43 |
--------------------------------------------------------------------------------
/sql/redshift/002_add_fast.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE
2 | rpc_endpoint_eth_send_raw_txs
3 | DROP COLUMN
4 | fast;
5 |
--------------------------------------------------------------------------------
/sql/redshift/002_add_fast.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE
2 | rpc_endpoint_eth_send_raw_txs
3 | ADD COLUMN
4 | fast boolean
5 | DEFAULT FALSE;
6 |
--------------------------------------------------------------------------------
/sql/redshift/003_alter_is_blocked.down.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flashbots/rpc-endpoint/1af08813cf5603ea5e4fa1faa6349942458b6a8b/sql/redshift/003_alter_is_blocked.down.sql
--------------------------------------------------------------------------------
/sql/redshift/003_alter_is_blocked.up.sql:
--------------------------------------------------------------------------------
1 | alter table rpc_endpoint_eth_send_raw_txs rename column is_blocked_bcz_already_sent to is_blocked;
2 |
--------------------------------------------------------------------------------
/staticcheck.conf:
--------------------------------------------------------------------------------
1 | checks = ["all", "-ST1000", "-ST1003", "-ST1020", "-ST1021", "-ST1022", "-ST1023"]
2 | 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"]
3 | dot_import_whitelist = ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"]
4 | http_status_code_whitelist = ["200", "400", "404", "500"]
5 |
--------------------------------------------------------------------------------
/testutils/mock_rpcbackend.go:
--------------------------------------------------------------------------------
1 | /*
2 | * Dummy RPC backend for both Ethereum node and Flashbots Relay.
3 | * Implements JSON-RPC calls that the tests need.
4 | */
5 | package testutils
6 |
7 | import (
8 | "encoding/json"
9 | "fmt"
10 | "io"
11 | "log"
12 | "net/http"
13 | "time"
14 |
15 | "github.com/flashbots/rpc-endpoint/types"
16 | )
17 |
18 | var MockBackendLastRawRequest *http.Request
19 | var MockBackendLastJsonRpcRequest *types.JsonRpcRequest
20 | var MockBackendLastJsonRpcRequestTimestamp time.Time
21 |
22 | func MockRpcBackendReset() {
23 | MockBackendLastRawRequest = nil
24 | MockBackendLastJsonRpcRequest = nil
25 | MockBackendLastJsonRpcRequestTimestamp = time.Time{}
26 | }
27 |
28 | func handleRpcRequest(req *types.JsonRpcRequest) (result interface{}, err error) {
29 | MockBackendLastJsonRpcRequest = req
30 |
31 | switch req.Method {
32 | case "eth_getTransactionCount":
33 | if req.Params[0] == TestTx_BundleFailedTooManyTimes_From {
34 | return TestTx_BundleFailedTooManyTimes_Nonce, nil
35 | } else if req.Params[0] == TestTx_CancelAtRelay_Cancel_From {
36 | return TestTx_CancelAtRelay_Cancel_Nonce, nil
37 | }
38 | return "0x22", nil
39 |
40 | case "eth_call":
41 | return "0x12345", nil
42 |
43 | case "eth_getTransactionReceipt":
44 | if req.Params[0] == TestTx_BundleFailedTooManyTimes_Hash {
45 | return nil, nil
46 | } else if req.Params[0] == TestTx_MM2_Hash {
47 | return nil, nil
48 | }
49 |
50 | case "eth_sendRawTransaction":
51 | txHash := req.Params[0].(string)
52 | if txHash == TestTx_CancelAtRelay_Cancel_RawTx {
53 | return TestTx_CancelAtRelay_Cancel_Hash, nil
54 | }
55 | return "tx-hash1", nil
56 |
57 | case "net_version":
58 | return "3", nil
59 |
60 | case "null":
61 | return nil, nil
62 |
63 | // Relay calls
64 | case "eth_sendPrivateTransaction":
65 | param := req.Params[0].(map[string]interface{})
66 | if param["tx"] == TestTx_BundleFailedTooManyTimes_RawTx {
67 | return TestTx_BundleFailedTooManyTimes_Hash, nil
68 | } else {
69 | return "tx-hash2", nil
70 | }
71 |
72 | case "eth_cancelPrivateTransaction":
73 | param := req.Params[0].(map[string]interface{})
74 | if param["txHash"] == TestTx_CancelAtRelay_Cancel_Hash {
75 | return true, nil
76 | } else {
77 | return false, nil
78 | }
79 | }
80 |
81 | return "", fmt.Errorf("no RPC method handler implemented for %s", req.Method)
82 | }
83 |
84 | func RpcBackendHandler(w http.ResponseWriter, req *http.Request) {
85 | defer req.Body.Close()
86 | MockBackendLastRawRequest = req
87 | MockBackendLastJsonRpcRequestTimestamp = time.Now()
88 |
89 | log.Printf("%s %s %s\n", req.RemoteAddr, req.Method, req.URL)
90 |
91 | w.Header().Set("Content-Type", "application/json")
92 | testHeader := req.Header.Get("Test")
93 | w.Header().Set("Test", testHeader)
94 |
95 | returnError := func(id interface{}, msg string) {
96 | log.Println("returnError:", msg)
97 | res := types.JsonRpcResponse{
98 | Id: id,
99 | Error: &types.JsonRpcError{
100 | Code: -32603,
101 | Message: msg,
102 | },
103 | }
104 |
105 | if err := json.NewEncoder(w).Encode(res); err != nil {
106 | log.Printf("error writing response 1: %v - data: %s", err, res)
107 | }
108 | }
109 |
110 | body, err := io.ReadAll(req.Body)
111 | if err != nil {
112 | returnError(-1, fmt.Sprintf("failed to read request body: %v", err))
113 | return
114 | }
115 |
116 | // Parse JSON RPC
117 | jsonReq := new(types.JsonRpcRequest)
118 | if err = json.Unmarshal(body, &jsonReq); err != nil {
119 | returnError(-1, fmt.Sprintf("failed to parse JSON RPC request: %v", err))
120 | return
121 | }
122 |
123 | rawRes, err := handleRpcRequest(jsonReq)
124 | if err != nil {
125 | returnError(jsonReq.Id, err.Error())
126 | return
127 | }
128 |
129 | w.WriteHeader(http.StatusOK)
130 | resBytes, err := json.Marshal(rawRes)
131 | if err != nil {
132 | fmt.Println("error mashalling rawRes:", rawRes, err)
133 | }
134 |
135 | res := types.NewJsonRpcResponse(jsonReq.Id, resBytes)
136 |
137 | // Write to client request
138 | if err := json.NewEncoder(w).Encode(res); err != nil {
139 | log.Printf("error writing response 2: %v - data: %s", err, rawRes)
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/testutils/mock_txapibackend.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "strings"
9 |
10 | "github.com/flashbots/rpc-endpoint/types"
11 | )
12 |
13 | var MockTxApiStatusForHash map[string]types.PrivateTxStatus = make(map[string]types.PrivateTxStatus)
14 |
15 | func MockTxApiReset() {
16 | MockTxApiStatusForHash = make(map[string]types.PrivateTxStatus)
17 | }
18 |
19 | func MockTxApiHandler(w http.ResponseWriter, req *http.Request) {
20 | defer req.Body.Close()
21 |
22 | fmt.Println("TX API", req.URL)
23 |
24 | if !strings.HasPrefix(req.URL.Path, "/tx/") {
25 | w.WriteHeader(http.StatusBadRequest)
26 | return
27 | }
28 |
29 | txHash := req.URL.Path[4:] // by default, the first 4 characters are "/tx/"
30 | resp := types.PrivateTxApiResponse{Status: types.TxStatusUnknown}
31 |
32 | if status, found := MockTxApiStatusForHash[txHash]; found {
33 | resp.Status = status
34 | }
35 |
36 | if err := json.NewEncoder(w).Encode(resp); err != nil {
37 | log.Printf("error writing response 2: %v - data: %v", err, resp)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/testutils/rpctesthelpers.go:
--------------------------------------------------------------------------------
1 | /*
2 | * Test helpers.
3 | */
4 | package testutils
5 |
6 | import (
7 | "bytes"
8 | "encoding/json"
9 | "io"
10 | "net/http"
11 | "testing"
12 |
13 | "github.com/pkg/errors"
14 |
15 | "github.com/flashbots/rpc-endpoint/types"
16 | )
17 |
18 | var RpcEndpointUrl string // set by tests
19 |
20 | func SendRpcAndParseResponse(req *types.JsonRpcRequest) (*types.JsonRpcResponse, error) {
21 | return SendRpcAndParseResponseTo(RpcEndpointUrl, req)
22 | }
23 |
24 | func SendBatchRpcAndParseResponse(req []*types.JsonRpcRequest) ([]*types.JsonRpcResponse, error) {
25 | return SendBatchRpcAndParseResponseTo(RpcEndpointUrl, req)
26 | }
27 | func SendRpcWithFastPreferenceAndParseResponse(t *testing.T, req *types.JsonRpcRequest) *types.JsonRpcResponse {
28 | url := RpcEndpointUrl + "/fast/"
29 | res, err := SendRpcAndParseResponseTo(url, req)
30 | if err != nil {
31 | t.Fatal("sendRpcAndParseResponse error:", err)
32 | }
33 | return res
34 | }
35 | func SendRpcWithAuctionPreferenceAndParseResponse(t *testing.T, req *types.JsonRpcRequest, urlSuffix string) *types.JsonRpcResponse {
36 | url := RpcEndpointUrl + urlSuffix
37 | res, err := SendRpcAndParseResponseTo(url, req)
38 | if err != nil {
39 | t.Fatal("sendRpcAndParseResponse error:", err)
40 | }
41 | return res
42 | }
43 |
44 | func SendRpcAndParseResponseOrFailNow(t *testing.T, req *types.JsonRpcRequest) *types.JsonRpcResponse {
45 | res, err := SendRpcAndParseResponse(req)
46 | if err != nil {
47 | t.Fatal("sendRpcAndParseResponse error:", err)
48 | }
49 | return res
50 | }
51 |
52 | func SendRpcAndParseResponseOrFailNowString(t *testing.T, req *types.JsonRpcRequest) string {
53 | var rpcResult string
54 | resp := SendRpcAndParseResponseOrFailNow(t, req)
55 | json.Unmarshal(resp.Result, &rpcResult)
56 | return rpcResult
57 | }
58 |
59 | func SendRpcAndParseResponseOrFailNowAllowRpcError(t *testing.T, req *types.JsonRpcRequest) *types.JsonRpcResponse {
60 | res, err := SendRpcAndParseResponse(req)
61 | if err != nil {
62 | t.Fatal(err)
63 | }
64 | return res
65 | }
66 |
67 | func SendRpcAndParseResponseTo(url string, req *types.JsonRpcRequest) (*types.JsonRpcResponse, error) {
68 | jsonData, err := json.Marshal(req)
69 | if err != nil {
70 | return nil, errors.Wrap(err, "marshal")
71 | }
72 |
73 | // fmt.Printf("%s\n", jsonData)
74 | resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
75 | if err != nil {
76 | return nil, errors.Wrap(err, "post")
77 | }
78 |
79 | respData, err := io.ReadAll(resp.Body)
80 | if err != nil {
81 | return nil, errors.Wrap(err, "read")
82 | }
83 |
84 | jsonRpcResp := new(types.JsonRpcResponse)
85 |
86 | // Check if returned an error, if so then convert to standard JSON-RPC error
87 | errorResp := new(types.RelayErrorResponse)
88 | if err := json.Unmarshal(respData, errorResp); err == nil && errorResp.Error != "" {
89 | // relay returned an error, convert to standard JSON-RPC error now
90 | jsonRpcResp.Error = &types.JsonRpcError{Message: errorResp.Error}
91 | return jsonRpcResp, nil
92 | }
93 |
94 | // Unmarshall JSON-RPC response and check for error inside
95 | if err := json.Unmarshal(respData, jsonRpcResp); err != nil {
96 | return nil, errors.Wrap(err, "unmarshal")
97 | }
98 |
99 | return jsonRpcResp, nil
100 | }
101 |
102 | func SendBatchRpcAndParseResponseTo(url string, req []*types.JsonRpcRequest) ([]*types.JsonRpcResponse, error) {
103 | jsonData, err := json.Marshal(req)
104 | if err != nil {
105 | return nil, errors.Wrap(err, "marshal")
106 | }
107 |
108 | // fmt.Printf("%s\n", jsonData)
109 | resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
110 | if err != nil {
111 | return nil, errors.Wrap(err, "post")
112 | }
113 |
114 | respData, err := io.ReadAll(resp.Body)
115 | if err != nil {
116 | return nil, errors.Wrap(err, "read")
117 | }
118 |
119 | var jsonRpcResp []*types.JsonRpcResponse
120 |
121 | // Unmarshall JSON-RPC response and check for error inside
122 | if err := json.Unmarshal(respData, &jsonRpcResp); err != nil {
123 | return nil, errors.Wrap(err, "unmarshal")
124 | }
125 |
126 | return jsonRpcResp, nil
127 | }
128 |
--------------------------------------------------------------------------------
/testutils/transactions.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | // Test tx for bundle-failed-too-many-times MM1 fix
4 | var TestTx_BundleFailedTooManyTimes_RawTx = "0x02f9019d011e843b9aca008477359400830247fa94def1c0ded9bec7f1a1670819833240f027b25eff88016345785d8a0000b90128d9627aa40000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000001394b63b2cbaea253a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f869584cd00000000000000000000000086003b044f70dac0abc80ac8957305b6370893ed0000000000000000000000000000000000000000000000d3443651ba615f6cd6c001a011a9f58ebe30aa679783b31793f897fdb603dd2ea086845723a22dae85ab2864a0090cf1fcce0f6e85da54f4eccf32a485d71a7d39bc0b43a53a9e64901c656230"
5 | var TestTx_BundleFailedTooManyTimes_From = "0xc84edF69E78C0E9dE5ccFE4fB9017F6F7566787f"
6 | var TestTx_BundleFailedTooManyTimes_Hash = "0xfb34b88cd77215867aa8e8ff0abc7060178b8fed6519a85d0b22853dfd5e9fec"
7 | var TestTx_BundleFailedTooManyTimes_Nonce = "0x1e"
8 |
9 | // Test tx for MM2 fix
10 | var TestTx_MM2_RawTx = "0xf86980850e5b35485d8252089409f427f1bd2d7537a02812275d03be7747dbd68c859d64bd68008025a0a94ba415e4d7c517548551442828aa192f149a8a04554e452084ee0ea55ea013a015966c84d9d38779ce63454a3f38038b269544d70433bfa7db0f6a37034b8e93"
11 | var TestTx_MM2_From = "0x7AaBc7915DF92a85E199DbB4B1D21E637e1a90A2"
12 | var TestTx_MM2_Hash = "0xc543e2ad05cffdee95b984df20edd2e38e124c54461faa1276adc36e826588c9"
13 |
14 | // Test tx for MM2 fix
15 | var TestTx_CancelAtRelay_Initial_RawTx = "0x02f90197010c851a13b86000851a13b860008302134094def1c0ded9bec7f1a1670819833240f027b25eff80b90128d9627aa400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000008b6f8cc1baffdb0400000000000000000000000000000000000000000000000000084b1e89ebc7c5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee869584cd00000000000000000000000086003b044f70dac0abc80ac8957305b6370893ed0000000000000000000000000000000000000000000000c2b17f6d6d619e0daac080a032854f4cd2ef72624bd4b24ac933677fa82d68915fb40cbc5ded938e55e0d60ba03d668997706bf52d9d6fd06de1e0b3ecb5de60c78faba54ee6f61c017236b530"
16 | var TestTx_CancelAtRelay_Cancel_RawTx = "0x02f86d010c851caf4ad000851caf4ad0008302134094c0e1142e97a9679fa0f9c13067af656cc24753738080c080a04db949f68b275646517c8225f2b783476579bfc58b6e6af9c0ead0b41f3f5fb1a0540bbff674288973d5c82c25bc7b8fe42343c4d64d85f79a9e21ab6698738103"
17 | var TestTx_CancelAtRelay_Cancel_Hash = "0x0a5237dafa5e65d7e5030b08012fd8399206a9130fbc9f2d3d5cf3865fa972ef"
18 | var TestTx_CancelAtRelay_Cancel_From = "0xc0E1142E97A9679FA0f9C13067Af656Cc2475373"
19 | var TestTx_CancelAtRelay_Cancel_Nonce = "0xc"
20 |
21 | // Test sendRawTx invalid nonce
22 | var TestTx_Invalid_Nonce_1 = "0xf9016d8226068514c5fa06a88307a12394d9e1ce17f2641f24ae83637ab66a2cca9c378b9f80b9010418cbafe5000000000000000000000000000000000000000000000131e2aaad46e36000000000000000000000000000000000000000000000000000004e002aee3c56380000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000bfd6fbc015907976a48eb83fd1d972b2bfc4ce4600000000000000000000000000000000000000000000000000000000621731cf000000000000000000000000000000000000000000000000000000000000000200000000000000000000000025f8087ead173b73d6e8b84329989a8eea16cf73000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc225a081ddc6c2ee6d93d5eba52e12c9b44524c28d6d724efc903110e4cab13ba98d6ba03e7c22541060787da52f36d52b826f7a53ee892d75a0a626a9f9d51a4b99f2ba"
23 | var TestTx_Invalid_Nonce_2 = "0x02f90c530142850826299e00850826299e0083048f6f947f268357a8c2552623316e2562d90e642bb538e580b90be4ab834bab0000000000000000000000007f268357a8c2552623316e2562d90e642bb538e5000000000000000000000000604a35a2a4df447cecbebab73158cd516de7fcbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000005b3256965e7c3cf26e11fcaf296dfc8807c01073000000000000000000000000baf2127b49fc93cbca6269fade0f7f31df4c88a70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000007f268357a8c2552623316e2562d90e642bb538e50000000000000000000000001eac87b51afabba0d40e7f207e96f1943d149ed4000000000000000000000000604a35a2a4df447cecbebab73158cd516de7fcbb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000baf2127b49fc93cbca6269fade0f7f31df4c88a70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009b6e64a8ec600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006217417000000000000000000000000000000000000000000000000000000000621b364b8f6ddfb08291623280c02ebc90b83bfc7f15a571590c0b8b339662b5197fbc17000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009b6e64a8ec600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006217dc7d00000000000000000000000000000000000000000000000000000000000000007195b39cdac6182e6bdd34771bd43d7d457db6590ae2fd7901b5ff65de26b1d90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000006a000000000000000000000000000000000000000000000000000000000000007e000000000000000000000000000000000000000000000000000000000000009200000000000000000000000000000000000000000000000000000000000000a600000000000000000000000000000000000000000000000000000000000000ba00000000000000000000000000000000000000000000000000000000000000bc0000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000001c5f0c6b3f7dcc767b2be33609e16c39a739aa1e7be3a33174914f531e510cf8cf0a0268c5192a8a78808bcee4c73f00f83beebf5cc4713e0d305facc196e227485f0c6b3f7dcc767b2be33609e16c39a739aa1e7be3a33174914f531e510cf8cf0a0268c5192a8a78808bcee4c73f00f83beebf5cc4713e0d305facc196e227480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010496809f900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000604a35a2a4df447cecbebab73158cd516de7fcbb000000000000000000000000495f947276749ce646f68ac8c248420045cb7b5eda9c56071673633dd0582b2741b77b51b92c3fb60000000000003a00000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010496809f900000000000000000000000001eac87b51afabba0d40e7f207e96f1943d149ed40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000495f947276749ce646f68ac8c248420045cb7b5eda9c56071673633dd0582b2741b77b51b92c3fb60000000000003a00000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010400000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c001a02ad50113dbf4a09106aa555adf652b9cc0a1eaf0a7cc9dbd242730f768f4f501a05a9eb25e3b358065d7191b91919d2a5dcdae4c516ba1a6c40247ab802f140fd1"
24 |
--------------------------------------------------------------------------------
/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/ethereum/go-ethereum/common"
9 | )
10 |
11 | // As per JSON-RPC 2.0 Specification
12 | // https://www.jsonrpc.org/specification#error_object
13 | const (
14 | JsonRpcParseError = -32700
15 | JsonRpcInvalidRequest = -32600
16 | JsonRpcMethodNotFound = -32601
17 | JsonRpcInvalidParams = -32602
18 | JsonRpcInternalError = -32603
19 | )
20 |
21 | type JsonRpcRequest struct {
22 | Id interface{} `json:"id"`
23 | Method string `json:"method"`
24 | Params []interface{} `json:"params"`
25 | Version string `json:"jsonrpc,omitempty"`
26 | }
27 |
28 | func NewJsonRpcRequest(id interface{}, method string, params []interface{}) *JsonRpcRequest {
29 | return &JsonRpcRequest{
30 | Id: id,
31 | Method: method,
32 | Params: params,
33 | Version: "2.0",
34 | }
35 | }
36 |
37 | func NewJsonRpcRequest1(id interface{}, method string, param interface{}) *JsonRpcRequest {
38 | return NewJsonRpcRequest(id, method, []interface{}{param})
39 | }
40 |
41 | type JsonRpcResponse struct {
42 | Id interface{} `json:"id"`
43 | Result json.RawMessage `json:"result,omitempty"`
44 | Error *JsonRpcError `json:"error,omitempty"`
45 | Version string `json:"jsonrpc"`
46 | }
47 |
48 | // RpcError: https://www.jsonrpc.org/specification#error_object
49 | type JsonRpcError struct {
50 | Code int `json:"code"`
51 | Message string `json:"message"`
52 | }
53 |
54 | func (err JsonRpcError) Error() string {
55 | return fmt.Sprintf("Error %d (%s)", err.Code, err.Message)
56 | }
57 |
58 | func NewJsonRpcResponse(id interface{}, result json.RawMessage) *JsonRpcResponse {
59 | return &JsonRpcResponse{
60 | Id: id,
61 | Result: result,
62 | Version: "2.0",
63 | }
64 | }
65 |
66 | type GetBundleStatusByTransactionHashResponse struct {
67 | TxHash string `json:"txHash"` // "0x0aeb9c61b342f7fc94a10d41c5d30a049a9cfa9ab764c6dd02204a19960ee567"
68 | Status string `json:"status"` // "FAILED_BUNDLE"
69 | Message string `json:"message"` // "Expired - The base fee was to low to execute this transaction, please try again"
70 | Error string `json:"error"` // "max fee per gas less than block base fee"
71 | BlocksCount int `json:"blocksCount"` // 2
72 | ReceivedTimestamp int `json:"receivedTimestamp"` // 1634568851003
73 | StatusTimestamp int `json:"statusTimestamp"` // 1634568873862
74 | }
75 |
76 | type HealthResponse struct {
77 | Now time.Time `json:"time"`
78 | StartTime time.Time `json:"startTime"`
79 | Version string `json:"version"`
80 | }
81 |
82 | type TransactionReceipt struct {
83 | TransactionHash string
84 | Status string
85 | }
86 |
87 | type PrivateTxStatus string
88 |
89 | var TxStatusUnknown PrivateTxStatus = "UNKNOWN"
90 | var TxStatusPending PrivateTxStatus = "PENDING"
91 | var TxStatusIncluded PrivateTxStatus = "INCLUDED"
92 | var TxStatusFailed PrivateTxStatus = "FAILED"
93 |
94 | type PrivateTxApiResponse struct {
95 | Status PrivateTxStatus `json:"status"`
96 | Hash string `json:"hash"`
97 | MaxBlockNumber int `json:"maxBlockNumber"`
98 | }
99 |
100 | type RelayErrorResponse struct {
101 | Error string `json:"error"`
102 | }
103 |
104 | type BundleResponse struct {
105 | BundleId string `json:"bundleId"`
106 | RawTxs []string `json:"rawTxs"`
107 | }
108 |
109 | type SendPrivateTxRequestWithPreferences struct {
110 | Tx string `json:"tx"`
111 | Preferences *PrivateTxPreferences `json:"preferences,omitempty"`
112 | MaxBlockNumber uint64 `json:"maxBlockNumber"`
113 | }
114 |
115 | type TxPrivacyPreferences struct {
116 | Hints []string `json:"hints"`
117 | Builders []string `json:"builders"`
118 | UseMempool bool `json:"useMempool"`
119 | MempoolRPC string `json:"mempoolRpc"`
120 | AuctionTimeout uint64 `json:"auctionTimeout,omitempty"`
121 | }
122 |
123 | type TxValidityPreferences struct {
124 | Refund []RefundConfig `json:"refund,omitempty"`
125 | }
126 |
127 | type RefundConfig struct {
128 | Address common.Address `json:"address"`
129 | Percent int `json:"percent"`
130 | }
131 |
132 | type PrivateTxPreferences struct {
133 | Privacy TxPrivacyPreferences `json:"privacy"`
134 | Validity TxValidityPreferences `json:"validity"`
135 | Fast bool `json:"fast"`
136 | CanRevert bool `json:"canRevert"`
137 | }
138 |
--------------------------------------------------------------------------------