├── .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 | [![Test status](https://github.com/flashbots/rpc-endpoint/workflows/Test/badge.svg)](https://github.com/flashbots/rpc-endpoint/actions?query=workflow%3A%22Test%22) 4 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 5 | [![Discord](https://img.shields.io/discord/755466764501909692)](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 | --------------------------------------------------------------------------------