Compare commits

..

16 Commits

Author SHA1 Message Date
ca4e4b24e0
Settings 2024-10-20 06:41:36 +00:00
1ad517098a
Listen websocket on same port, allow direct connections and more helpful logging 2024-10-20 06:41:35 +00:00
smartcontracts
d8cec08bd9
Update tag-service.yml (#62) 2024-10-17 09:34:35 -04:00
Hamdi Allam
ac9ae52d52
op-txproxy: capitalize the env var prefix (#58)
* env var prefix

* tag-service workflow
2024-10-07 14:19:14 -04:00
Hamdi Allam
dff24e9fca
op-txproxy: external validating proxy for conditional transactions (#42)
* txpool svc

* change mod github path

* tag-tool

* codeowners
2024-09-30 13:43:34 -07:00
Sam Stokes
88521bef29
circleci: fix release job deps (#55) 2024-09-10 16:12:10 -04:00
Sam Stokes
75b02dff3d
op-signer: add to this repo (#51)
* op-signer: add to this repo

* circleci: add op-signer jobs/workflows

* ops: update tag service to include op-signer

* readme: add op-signer one sentence description

* ci: add op-signer option to github action

* ops: add op-signer min version
2024-09-10 15:01:09 -04:00
Zach Howard
7f8095c9ad
op-conductor-ops: adds force-active-sequencer command (#50) 2024-09-04 17:34:03 -04:00
dependabot[bot]
bfdc66fa28
chore(deps): bump certifi from 2024.6.2 to 2024.7.4 in /op-conductor-ops (#47)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.6.2 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2024.06.02...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 11:55:05 -05:00
Zach Howard
3b420a5c41
Couple of usability improvements and cert config path fix (#49) 2024-08-22 17:08:06 -04:00
Jacob Elias
c03568e408
Multicall testcase improvements (#39)
* feat: improve the multicall test suite to use channels to explictly control the response flow, rather than sleep functions
2024-08-22 12:46:43 -05:00
Jacob Elias
88c767e316
feat: update proxyd references to infra repo (#48) 2024-08-22 12:19:26 -05:00
Jacob Elias
ad47122943
feat: add op-conductor-ops script with updated readme and example config file (#44) 2024-08-21 18:47:16 -05:00
dependabot[bot]
9a36d674b5
chore(deps): bump github.com/rs/cors from 1.10.1 to 1.11.0 in /proxyd (#25)
Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.10.1 to 1.11.0.
- [Commits](https://github.com/rs/cors/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/rs/cors
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-21 10:15:16 -05:00
cody-wang-cb
1cacaab6b5
Use not healthy as error msg (#41)
* use not healthy as error msg

* update wording only
2024-08-20 08:20:15 -07:00
Tei Im
98e261e7f9
proxyd: Support Redis read endpoint for caching (#45) 2024-08-19 14:23:09 -06:00
84 changed files with 4977 additions and 109 deletions

View File

@ -16,6 +16,8 @@ workflows:
# <regex path-to-test> <parameter-to-set> <value-of-pipeline-parameter> # <regex path-to-test> <parameter-to-set> <value-of-pipeline-parameter>
mapping: | mapping: |
op-conductor-mon/.* run-build-op-conductor-mon true op-conductor-mon/.* run-build-op-conductor-mon true
op-signer/.* run-build-op-signer true
op-txproxy/.* run-build-op-txproxy true
op-ufm/.* run-build-op-ufm true op-ufm/.* run-build-op-ufm true
proxyd/.* run-build-proxyd true proxyd/.* run-build-proxyd true
.circleci/.* run-all true .circleci/.* run-all true

View File

@ -10,6 +10,12 @@ parameters:
run-build-op-conductor-mon: run-build-op-conductor-mon:
type: boolean type: boolean
default: false default: false
run-build-op-signer:
type: boolean
default: false
run-build-op-txproxy:
type: boolean
default: false
run-build-op-ufm: run-build-op-ufm:
type: boolean type: boolean
default: false default: false
@ -76,6 +82,7 @@ jobs:
command: | command: |
echo "Configuration Results:" echo "Configuration Results:"
echo "run-build-op-conductor-mon: << pipeline.parameters.run-build-op-conductor-mon >>" echo "run-build-op-conductor-mon: << pipeline.parameters.run-build-op-conductor-mon >>"
echo "run-build-op-signer: << pipeline.parameters.run-build-op-signer >>"
echo "run-build-op-ufm: << pipeline.parameters.run-build-op-ufm >>" echo "run-build-op-ufm: << pipeline.parameters.run-build-op-ufm >>"
echo "run-build-proxyd: << pipeline.parameters.run-build-proxyd >>" echo "run-build-proxyd: << pipeline.parameters.run-build-proxyd >>"
echo "run-all: << pipeline.parameters.run-all >>" echo "run-all: << pipeline.parameters.run-all >>"
@ -100,6 +107,12 @@ jobs:
echo "op-conductor-mon tag regex match: false" echo "op-conductor-mon tag regex match: false"
fi fi
if [[ $CURRENT_TAG =~ ^op-signer/v.* ]]; then
echo "op-signer tag regex match: true"
else
echo "op-signer tag regex match: false"
fi
if [[ $CURRENT_TAG =~ ^op-ufm/v.* ]]; then if [[ $CURRENT_TAG =~ ^op-ufm/v.* ]]; then
echo "op-ufm tag regex match: true" echo "op-ufm tag regex match: true"
else else
@ -408,6 +421,38 @@ workflows:
docker_name: op-conductor-mon docker_name: op-conductor-mon
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context: . docker_context: .
op-signer:
when:
or: [<< pipeline.parameters.run-build-op-signer >>, << pipeline.parameters.run-all >>]
jobs:
- go-lint:
name: op-signer-lint
module: op-signer
- go-test:
name: op-signer-tests
module: op-signer
- docker-build:
name: op-signer-docker-build
docker_file: op-signer/Dockerfile
docker_name: op-signer
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context: .
op-txproxy:
when:
or: [<< pipeline.parameters.run-build-op-txproxy >>, << pipeline.parameters.run-all >>]
jobs:
- go-lint:
name: op-txproxy-lint
module: op-txproxy
- go-test:
name: op-txproxy-tests
module: op-txproxy
- docker-build:
name: op-txproxy-docker-build
docker_file: op-txproxy/Dockerfile
docker_name: op-txproxy
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context: .
op-ufm: op-ufm:
when: when:
or: [<< pipeline.parameters.run-build-op-ufm >>, << pipeline.parameters.run-all >>] or: [<< pipeline.parameters.run-build-op-ufm >>, << pipeline.parameters.run-all >>]
@ -455,6 +500,41 @@ workflows:
only: /^(proxyd|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)\/v.*/ only: /^(proxyd|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)\/v.*/
branches: branches:
ignore: /.*/ ignore: /.*/
- docker-build:
name: op-signer-docker-build
filters:
tags:
only: /^op-signer\/v.*/
docker_name: op-signer
docker_tags: <<pipeline.git.revision>>
docker_context: .
docker_file: op-signer/Dockerfile
context:
- oplabs-gcr-release
requires:
- hold
- docker-publish:
name: op-signer-docker-publish
filters:
tags:
only: /^op-signer\/v.*/
docker_name: op-signer
docker_tags: <<pipeline.git.revision>>
context:
- oplabs-gcr-release
requires:
- op-signer-docker-build
- docker-tag-op-stack-release:
name: op-signer-docker-tag
filters:
tags:
only: /^op-signer\/v.*/
branches:
ignore: /.*/
context:
- oplabs-gcr-release
requires:
- op-signer-docker-publish
- docker-build: - docker-build:
name: op-ufm-docker-build name: op-ufm-docker-build
filters: filters:
@ -470,6 +550,9 @@ workflows:
- hold - hold
- docker-publish: - docker-publish:
name: op-ufm-docker-publish name: op-ufm-docker-publish
filters:
tags:
only: /^op-ufm\/v.*/
docker_name: op-ufm docker_name: op-ufm
docker_tags: <<pipeline.git.revision>> docker_tags: <<pipeline.git.revision>>
context: context:
@ -477,7 +560,7 @@ workflows:
requires: requires:
- op-ufm-docker-build - op-ufm-docker-build
- docker-tag-op-stack-release: - docker-tag-op-stack-release:
name: docker-tag-op-ufm-release name: op-ufm-docker-tag
filters: filters:
tags: tags:
only: /^op-ufm\/v.*/ only: /^op-ufm\/v.*/
@ -501,7 +584,7 @@ workflows:
requires: requires:
- hold - hold
- docker-publish: - docker-publish:
name: proxyd-docker-release name: proxyd-docker-publish
filters: filters:
tags: tags:
only: /^proxyd\/v.*/ only: /^proxyd\/v.*/
@ -512,7 +595,7 @@ workflows:
requires: requires:
- proxyd-docker-build - proxyd-docker-build
- docker-tag-op-stack-release: - docker-tag-op-stack-release:
name: docker-tag-op-stack-release name: proxyd-docker-tag
filters: filters:
tags: tags:
only: /^proxyd\/v.*/ only: /^proxyd\/v.*/
@ -521,7 +604,7 @@ workflows:
context: context:
- oplabs-gcr-release - oplabs-gcr-release
requires: requires:
- proxyd-docker-release - proxyd-docker-publish
- docker-build: - docker-build:
name: op-conductor-mon-docker-build name: op-conductor-mon-docker-build
filters: filters:
@ -537,6 +620,9 @@ workflows:
- hold - hold
- docker-publish: - docker-publish:
name: op-conductor-mon-docker-publish name: op-conductor-mon-docker-publish
filters:
tags:
only: /^op-conductor-mon\/v.*/
docker_name: op-conductor-mon docker_name: op-conductor-mon
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
context: context:
@ -544,7 +630,7 @@ workflows:
requires: requires:
- op-conductor-mon-docker-build - op-conductor-mon-docker-build
- docker-tag-op-stack-release: - docker-tag-op-stack-release:
name: docker-tag-op-stack-release name: op-conductor-mon-docker-tag
filters: filters:
tags: tags:
only: /^op-conductor-mon\/v.*/ only: /^op-conductor-mon\/v.*/

2
.github/CODEOWNERS vendored
View File

@ -1 +1,3 @@
* @ethereum-optimism/infra-reviewers * @ethereum-optimism/infra-reviewers
/op-txproxy @ethereum-optimism/devxpod

View File

@ -1,5 +1,8 @@
name: Tag Service name: Tag Service
permissions:
contents: write
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@ -18,7 +21,9 @@ on:
required: true required: true
type: choice type: choice
options: options:
- op-signer
- op-ufm - op-ufm
- op-txproxy
- proxyd - proxyd
prerelease: prerelease:
description: Increment major/minor/patch as prerelease? description: Increment major/minor/patch as prerelease?

View File

@ -4,4 +4,5 @@ This repository is an extension of the [Optimism monorepo](https://github.com/et
## Components ## Components
- op-conductor-mon: Monitors multiple op-conductor instances and provides a unified interface for reporting metrics. - op-conductor-mon: Monitors multiple op-conductor instances and provides a unified interface for reporting metrics.
- op-signer: Thin gateway that supports `eth_signTransaction` RPC endpoint to sign ethereum tx payloads using private key stored in KMS
- op-ufm: User facing monitoring creates transactions at regular intervals and observe transaction propagation across different RPC providers. - op-ufm: User facing monitoring creates transactions at regular intervals and observe transaction propagation across different RPC providers.

View File

@ -0,0 +1,51 @@
# op-conductor-ops
op-conductor-ops is a CLI tool for managing op-conductor sequencer clusters.
**WARNING!!! This tool can cause a network outage if used improperly. Please consult #pod-devinfra before using.**
## Setup
Requires [poetry](https://github.com/python-poetry/poetry).
Install python dependencies with `poetry install`.
Recommended updates to your .bashrc/zshrc:
1. `export PATH="export PATH="<path-to-infra-repo>/op-conductor-ops:$PATH""`
2. `export CONDUCTOR_CONFIG="<path-to-op-conductor-ops-config.toml>"`
## Usage
After installing dependencies with `poetry`, the tool can be invoked with `./op-conductor-ops`,
which just calls `poetry run python main.py` and passes on any arguments.
### Example Usage
* Example usage with implicit config file with lookup at ./config.toml
```./op-conductor-ops status <network-name>```
* Usage with explicit path to config and certificate
```./op-conductor-ops -c ./<path>/config.toml --cert ./<path>/cacert.pem <command> <network-name>```
## Example Configuration File: example.config.toml
This configuration file is used to set up the networks and sequencers for your application.
### Structure
The configuration file is divided into two main sections:
1. **Networks**: This section defines the networks that your application will use. There is an example network configuration (`op-network-1`) and a blank network configuration (`op-network-N`) for you to fill out.
2. **Sequencers**: This section defines the sequencers for each network. Again, there is an example sequencer configuration for `op-network-1` and a blank sequencer configuration for `op-network-N`.
Is is recommended to update the network name and sequencer names for your specifc configuration in the toml object declaration
### Config Usage
1. Copy this file to `config.toml` in your application's root directory.
2. Modify the example configurations or fill out the blank configurations as needed for your application.
3. Save the `config.toml` file and use it to configure your application's networks and sequencers.
Remember, the example configurations are provided for your convenience, but you should review and update them to match your specific requirements.

View File

@ -0,0 +1,32 @@
from network import Network
from sequencer import Sequencer
import toml
def read_config(config_path: str) -> tuple[dict[str, Sequencer], str]:
config = toml.load(config_path)
cert_path = config.get('cert_path', "")
# if cert path is relative, pre-pend the config path
if not cert_path.startswith('/'):
cert_path = f"{config_path.rsplit('/', 1)[0]}/{cert_path}"
# load sequencers into a map
sequencers = {}
for name, seq_config in config['sequencers'].items():
sequencers[name] = Sequencer(
sequencer_id=name,
raft_addr=seq_config['raft_addr'],
conductor_rpc_url=seq_config['conductor_rpc_url'],
node_rpc_url=seq_config['node_rpc_url'],
voting=seq_config['voting']
)
# Initialize network, with list of sequencers
networks = {}
for network_name, network_config in config['networks'].items():
network_sequencers = [sequencers[seq_name]
for seq_name in network_config['sequencers']]
networks[network_name] = Network(network_name, network_sequencers)
return networks, cert_path

View File

@ -0,0 +1,62 @@
# Path to the SSL/TLS certificate file
cert_path = "./cacert.pem"
# Network configurations
[networks]
# Example network configuration
[networks.op-network-1]
sequencers = [
"op-network-1-sequencer-0",
"op-network-1-sequencer-1",
"op-network-1-sequencer-2",
]
# Blank network configuration
[networks.op-network-N]
sequencers = [
"op-network-N-sequencer-0",
"op-network-N-sequencer-1",
"op-network-N-sequencer-2",
]
# Sequencer configurations
[sequencers]
# Example sequencer configuration for op-network-1 with three sequencers
[sequencers.op-network-1-sequencer-0]
raft_addr = "op-network-1-sequencer-0-op-conductor-raft:50050"
conductor_rpc_url = "https://op-network-1-sequencer-0-op-conductor"
node_rpc_url = "https://op-network-1-sequencer-0-op-node"
voting = true
[sequencers.op-network-1-sequencer-1]
raft_addr = "op-network-1-sequencer-1-op-conductor-raft.50050"
conductor_rpc_url = "https://op-network-1-sequencer-1-op-conductor"
node_rpc_url = "https://op-network-1-sequencer-1-op-node"
voting = false
[sequencers.op-network-1-sequencer-2]
raft_addr = "op-network-1-sequencer-2-op-conductor-raft:50050"
conductor_rpc_url = "https://op-network-1-sequencer-2-op-conductor"
node_rpc_url = "https://op-network-1-sequencer-2-op-node"
voting = true
# Blank sequencer configuration for op-network-N with three blank sequencers
[sequencers.op-network-N-sequencer-0]
raft_addr = ""
conductor_rpc_url = ""
node_rpc_url = ""
voting = true
[sequencers.op-network-N-sequencer-1]
raft_addr = ""
conductor_rpc_url = ""
node_rpc_url = ""
voting = true
[sequencers.op-network-N-sequencer-2]
raft_addr = ""
conductor_rpc_url = ""
node_rpc_url = ""
voting = true

View File

@ -0,0 +1,35 @@
import concurrent.futures
class Network:
def __init__(self, name, sequencers):
self.name = name
self.sequencers = sequencers
def update(self):
def _update(sequencer):
sequencer.update()
with concurrent.futures.ThreadPoolExecutor() as executor:
list(executor.map(_update, self.sequencers))
def get_sequencer_by_id(self, sequencer_id: str):
return next(
(
sequencer
for sequencer in self.sequencers
if sequencer.sequencer_id == sequencer_id
),
None,
)
def find_conductor_leader(self):
return next(
(sequencer for sequencer in self.sequencers if sequencer.conductor_leader),
None,
)
def find_active_sequencer(self):
return next(
(sequencer for sequencer in self.sequencers if sequencer.sequencer_active),
None,
)

View File

@ -0,0 +1,7 @@
#!/bin/sh
set -e
# cd to the directory of the script
cd "$(dirname "$0")"
poetry install --no-interaction --quiet
poetry run python op-conductor-ops.py "${@}"

View File

@ -0,0 +1,372 @@
#!/usr/bin/env python
import os
import time
import requests
from rich.console import Console
from rich.table import Table
import typer
from typing_extensions import Annotated
from config import read_config
from utils import make_rpc_payload, print_boolean, print_warn, print_error
app = typer.Typer(
help="CLI for managing OP Conductor sequencers. WARNING: This tool can cause a network outage if used improperly. Please consult #pod-devinfra before using."
)
console = Console()
@app.callback()
def load_config(
cert: Annotated[str, typer.Option(
"--cert",
help="[Optional] Certificate file path for https. Takes precedece over cert_path config",
envvar="CONDUCTOR_CERT",
)] = "",
config_path: Annotated[str, typer.Option(
"--config", "-c",
help="Path to config file.",
envvar="CONDUCTOR_CONFIG",
)] = "./config.toml",
):
networks, config_cert_path = read_config(config_path)
global NETWORKS
NETWORKS = networks
# Use the cert path from the command line if provided,
# otherwise use the one from the config
# Export the certificate for https connections
cert_path = cert or config_cert_path
if cert_path:
os.environ["REQUESTS_CA_BUNDLE"] = cert_path
os.environ["SSL_CERT_FILE"] = cert_path
def get_network(network: str):
if network not in NETWORKS:
typer.echo(f"Network must be one of {', '.join(NETWORKS.keys())}")
raise typer.Exit(code=1)
network_obj = NETWORKS[network]
network_obj.update()
return network_obj
@app.command()
def status(network: str):
"""Print the status of all sequencers in a network."""
network_obj = get_network(network)
sequencers = network_obj.sequencers
table = Table(
"Sequencer ID",
"Conductor Active",
"Sequencer Healthy",
"Conductor Leader",
"Active Sequencer",
"Unsafe Number",
"Unsafe Hash",
)
for sequencer in sequencers:
table.add_row(
sequencer.sequencer_id,
print_boolean(sequencer.conductor_active),
print_boolean(sequencer.sequencer_healthy),
print_boolean(sequencer.conductor_leader),
print_boolean(sequencer.sequencer_active),
str(sequencer.unsafe_l2_number),
str(sequencer.unsafe_l2_hash),
)
console.print(table)
leader = network_obj.find_conductor_leader()
if leader is None:
print_warn(f"Could not find current leader in network {network}")
else:
display_correction = False
membership = {x["id"]: x for x in leader.cluster_membership()}
for sequencer in sequencers:
if sequencer.sequencer_id in membership:
if (
int(not sequencer.voting)
!= membership[sequencer.sequencer_id]["suffrage"]
):
print_error(
f": {sequencer.sequencer_id} does not have the correct voting status.")
display_correction = True
else:
print_warn(
f": {sequencer.sequencer_id} is not in the cluster")
display_correction = True
if display_correction:
print_warn(
"Run 'update-cluster-membership' to correct membership issues")
@app.command()
def transfer_leader(network: str, sequencer_id: str):
"""Transfer leadership to a specific sequencer."""
network_obj = get_network(network)
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"Sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
if sequencer.voting is False:
print_error(f"Sequencer {sequencer_id} is not a voter")
raise typer.Exit(code=1)
healthy = sequencer.sequencer_healthy
if not healthy:
print_error(f"Target sequencer {sequencer_id} is not healthy")
raise typer.Exit(code=1)
leader = network_obj.find_conductor_leader()
if leader is None:
print_error(f"Could not find current leader in network {network}")
raise typer.Exit(code=1)
resp = requests.post(
leader.conductor_rpc_url,
json=make_rpc_payload(
"conductor_transferLeaderToServer",
params=[sequencer.sequencer_id, sequencer.raft_addr],
),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(
f"Failed to transfer leader to {sequencer_id}: {resp.json()['error']}"
)
raise typer.Exit(code=1)
typer.echo(f"Successfully transferred leader to {sequencer_id}")
@app.command()
def pause(network: str, sequencer_id: str = None):
"""Pause all conductors.
If --sequencer-id is provided, only pause conductor for that sequencer.
"""
network_obj = get_network(network)
sequencers = network_obj.sequencers
if sequencer_id is not None:
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"Sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
sequencers = [sequencer]
error = False
for sequencer in sequencers:
resp = requests.post(
sequencer.conductor_rpc_url,
json=make_rpc_payload("conductor_pause"),
)
try:
resp.raise_for_status()
if "error" in resp.json():
raise Exception(resp.json()["error"])
typer.echo(f"Successfully paused {sequencer.sequencer_id}")
except Exception as e:
typer.echo(f"Failed to pause {sequencer.sequencer_id}: {e}")
if error:
raise typer.Exit(code=1)
@app.command()
def resume(network: str, sequencer_id: str = None):
"""Resume all conductors.
If --sequencer-id is provided, only resume conductor for that sequencer.
"""
network_obj = get_network(network)
sequencers = network_obj.sequencers
if sequencer_id is not None:
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
sequencers = [sequencer]
error = False
for sequencer in sequencers:
resp = requests.post(
sequencer.conductor_rpc_url,
json=make_rpc_payload("conductor_resume"),
)
try:
resp.raise_for_status()
if "error" in resp.json():
raise Exception(resp.json()["error"])
typer.echo(f"Successfully resumed {sequencer.sequencer_id}")
except Exception as e:
print_error(f"Failed to resume {sequencer.sequencer_id}: {e}")
if error:
raise typer.Exit(code=1)
@app.command()
def override_leader(network: str, sequencer_id: str):
"""Override the conductor_leader response for a sequencer to True.
Note that this does not affect consensus and it should only be used for disaster recovery purposes.
"""
network_obj = get_network(network)
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
resp = requests.post(
sequencer.conductor_rpc_url,
json=make_rpc_payload("conductor_overrideLeader"),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(
f"Failed to override conductor leader status for {sequencer_id}: {resp.json()['error']}"
)
raise typer.Exit(code=1)
resp = requests.post(
sequencer.node_rpc_url,
json=make_rpc_payload("admin_overrideLeader"),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(
f"Failed to override sequencer leader status for {sequencer_id}: {resp.json()['error']}"
)
raise typer.Exit(code=1)
typer.echo(f"Successfully overrode leader for {sequencer_id}")
@app.command()
def remove_server(network: str, sequencer_id: str):
"""Remove a sequencer from the cluster."""
network_obj = get_network(network)
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
print_error(
f"sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
leader = network_obj.find_conductor_leader()
resp = requests.post(
leader.conductor_rpc_url,
json=make_rpc_payload("conductor_removeServer",
params=[sequencer_id, 0]),
)
resp.raise_for_status()
if "error" in resp.json():
print_error(f"Failed to remove {sequencer_id}: {resp.json()['error']}")
raise typer.Exit(code=1)
typer.echo(f"Successfully removed {sequencer_id}")
@app.command()
def update_cluster_membership(network: str):
"""Update the cluster membership to match the sequencer configuration."""
network_obj = get_network(network)
sequencers = network_obj.sequencers
leader = network_obj.find_conductor_leader()
if leader is None:
print_error(f"Could not find current leader in network {network}")
raise typer.Exit(code=1)
membership = {x["id"]: x for x in leader.cluster_membership()}
error = False
for sequencer in sequencers:
if sequencer.sequencer_id in membership:
if (
int(not sequencer.voting)
!= membership[sequencer.sequencer_id]["suffrage"]
):
typer.echo(
f"Removing {sequencer.sequencer_id} from cluster to update voting status"
)
remove_server(network, sequencer.sequencer_id)
method = (
"conductor_addServerAsVoter"
if sequencer.voting
else "conductor_addServerAsNonvoter"
)
resp = requests.post(
leader.conductor_rpc_url,
json=make_rpc_payload(
method,
params=[sequencer.sequencer_id, sequencer.raft_addr, 0],
),
)
try:
resp.raise_for_status()
if "error" in resp.json():
raise Exception(resp.json()["error"])
typer.echo(
f"Successfully added {sequencer.sequencer_id} as {'voter' if sequencer.voting else 'non-voter'}"
)
except Exception as e:
print_warn(f"Failed to add {sequencer.sequencer_id} as voter: {e}")
if error:
raise typer.Exit(code=1)
@app.command()
def force_active_sequencer(network: str, sequencer_id: str):
"""Forces a sequencer to become active using stop/start."""
network_obj = get_network(network)
sequencer = network_obj.get_sequencer_by_id(sequencer_id)
if sequencer is None:
typer.echo(f"sequencer ID {sequencer_id} not found in network {network}")
raise typer.Exit(code=1)
hash = sequencer.unsafe_l2_hash
active_sequencer = network_obj.find_active_sequencer()
if active_sequencer:
typer.echo(f"Stopping {active_sequencer.sequencer_id}")
resp = requests.post(
active_sequencer.node_rpc_url,
json=make_rpc_payload("admin_stopSequencer", params=[]),
)
resp.raise_for_status()
if "error" in resp.json():
typer.echo(f"Failed to stop {active_sequencer.sequencer_id}: {resp.json()['error']}")
raise typer.Exit(code=1)
hash = resp.json()["result"]
if not hash:
typer.echo(f"Failed to get a hash to start sequencer")
raise typer.Exit(code=1)
# sleep for a bit to allow sequencer to catch up
time.sleep(1)
# start sequencer
typer.echo(f"Starting {sequencer_id} with hash {hash}")
resp = requests.post(
sequencer.node_rpc_url,
json=make_rpc_payload("admin_startSequencer", params=[hash]),
)
resp.raise_for_status()
if "error" in resp.json():
typer.echo(f"Failed to start {sequencer_id}: {resp.json()['error']}")
raise typer.Exit(code=1)
typer.echo(f"Successfully forced {sequencer_id} to become active")
if __name__ == "__main__":
app()

307
op-conductor-ops/poetry.lock generated Normal file
View File

@ -0,0 +1,307 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "certifi"
version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "idna"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "pygments"
version = "2.18.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "requests"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rich"
version = "13.7.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "shellingham"
version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
files = [
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
[[package]]
name = "typer"
version = "0.12.3"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.7"
files = [
{file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"},
{file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"},
]
[package.dependencies]
click = ">=8.0.0"
rich = ">=10.11.0"
shellingham = ">=1.3.0"
typing-extensions = ">=3.7.4.3"
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "urllib3"
version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "b56021bf3dca52508e895f8746030fedc8d5d4a7b5e91cd9e25874ed1970f580"

View File

@ -0,0 +1,17 @@
[tool.poetry]
name = "op-conductor-ops"
description = "CLI tool for managing op-conductor sequencer clusters"
authors = ["Zach Howard <zach@oplabs.co>"]
license = "MIT"
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
typer = "^0.12.3"
requests = "^2.31.0"
toml = "^0.10.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -0,0 +1,105 @@
import concurrent.futures
import requests
import typer
from utils import make_rpc_payload
class Sequencer:
def __init__(
self, sequencer_id, raft_addr, conductor_rpc_url, node_rpc_url, voting
):
self.sequencer_id = sequencer_id
self.raft_addr = raft_addr
self.conductor_rpc_url = conductor_rpc_url
self.node_rpc_url = node_rpc_url
self.voting = voting
self.conductor_active = None
self.conductor_leader = None
self.sequencer_healthy = None
self.sequencer_active = None
self.unsafe_l2_hash = None
self.unsafe_l2_number = None
def _get_sequencer_active(self):
resp = requests.post(
self.node_rpc_url,
json=make_rpc_payload("admin_sequencerActive"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.sequencer_active = resp.json()["result"]
def _get_sequencer_healthy(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_sequencerHealthy"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.sequencer_healthy = resp.json()["result"]
def _get_conductor_active(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_active"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.conductor_active = resp.json()["result"]
def _get_conductor_leader(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_leader"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
self.conductor_leader = resp.json()["result"]
def _get_unsafe_l2(self):
resp = requests.post(
self.node_rpc_url,
json=make_rpc_payload("optimism_syncStatus"),
)
try:
resp.raise_for_status()
except Exception as e:
return None
result = resp.json()["result"]
self.unsafe_l2_number = result["unsafe_l2"]["number"]
self.unsafe_l2_hash = result["unsafe_l2"]["hash"]
def cluster_membership(self):
resp = requests.post(
self.conductor_rpc_url,
json=make_rpc_payload("conductor_clusterMembership"),
)
resp.raise_for_status()
return resp.json()["result"]["servers"]
def update(self):
functions = [
self._get_conductor_active,
self._get_conductor_leader,
self._get_sequencer_healthy,
self._get_sequencer_active,
self._get_unsafe_l2,
]
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {executor.submit(func): func for func in functions}
for future in concurrent.futures.as_completed(futures):
func = futures[future]
try:
result = future.result()
except Exception as e:
typer.echo(f"{func.__name__} raised an exception: {e}")

26
op-conductor-ops/utils.py Normal file
View File

@ -0,0 +1,26 @@
from rich import print
def print_boolean(value):
if value is None:
return ""
return "" if value else ""
def make_rpc_payload(method: str, params: list = None):
if params is None:
params = []
return {
"id": 1,
"jsonrpc": "2.0",
"method": method,
"params": params,
}
def print_error(msg: str):
print(f"[bold red]ERROR![/bold red] {msg}")
def print_warn(msg: str):
print(f"[bold yellow]WARNING![/bold yellow] {msg}")

37
op-signer/.air.toml Normal file
View File

@ -0,0 +1,37 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "bin"
[build]
args_bin = []
bin = "./bin/op-signer"
cmd = "make build"
delay = 1000
exclude_dir = ["bin"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false

4
op-signer/.envrc Normal file
View File

@ -0,0 +1,4 @@
export OP_SIGNER_LOG_LEVEL=debug
export OP_SIGNER_LOG_COLOR=true
export OP_SIGNER_RPC_PORT=8080
export OP_SIGNER_METRICS_ENABLED=true

2
op-signer/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin/
tls/

1
op-signer/CHANGELOG.md Normal file
View File

@ -0,0 +1 @@
# @eth-optimism/signer

18
op-signer/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM golang:1.21.3-alpine3.18 as builder
COPY ./op-signer /app
WORKDIR /app
RUN apk --no-cache add make jq bash git alpine-sdk
RUN make build
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
COPY --from=builder /app/bin/op-signer /app
ENTRYPOINT ["/app/op-signer"]

37
op-signer/Makefile Normal file
View File

@ -0,0 +1,37 @@
GITCOMMIT := $(shell git rev-parse HEAD)
GITDATE := $(shell git show -s --format='%ct')
VERSION := v0.0.0
LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT)
LDFLAGSSTRING +=-X main.GitDate=$(GITDATE)
LDFLAGSSTRING +=-X main.Version=$(VERSION)
LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
all: build
docker:
docker build ../ -f Dockerfile -t op-signer:latest
build:
env GO111MODULE=on go build -v $(LDFLAGS) -o ./bin/op-signer ./cmd
clean:
rm ./bin/op-signer
generate:
[ '$(shell mockgen --version)' = 'v1.6.0' ] || go install github.com/golang/mock/mockgen@v1.6.0
go generate ./...
test: generate
go test -v ./...
lint:
golangci-lint run ./...
.PHONY: \
build \
clean \
test \
generate \
lint \
docker

14
op-signer/README.md Normal file
View File

@ -0,0 +1,14 @@
# @eth-optimism/signer
Signer service and client library
## Setup
Install go1.18
```bash
make build
source .env.example # (or copy to .envrc if using direnv)
./bin/signer
```

239
op-signer/app.go Normal file
View File

@ -0,0 +1,239 @@
package app
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"sync/atomic"
"github.com/prometheus/client_golang/prometheus"
"github.com/urfave/cli/v2"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/ethereum-optimism/optimism/op-service/httputil"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/tls/certman"
"github.com/ethereum-optimism/infra/op-signer/client"
"github.com/ethereum-optimism/infra/op-signer/service"
)
type SignerApp struct {
log log.Logger
version string
pprofServer *oppprof.Service
metricsServer *httputil.HTTPServer
registry *prometheus.Registry
signer *service.SignerService
rpc *oprpc.Server
stopped atomic.Bool
}
func InitFromConfig(ctx context.Context, log log.Logger, cfg *Config, version string) (*SignerApp, error) {
if err := cfg.Check(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
app := &SignerApp{log: log, version: version}
if err := app.init(cfg); err != nil {
return nil, errors.Join(err, app.Stop(ctx)) // clean up the failed init attempt
}
return app, nil
}
func (s *SignerApp) init(cfg *Config) error {
if err := s.initPprof(cfg); err != nil {
return fmt.Errorf("pprof error: %w", err)
}
if err := s.initMetrics(cfg); err != nil {
return fmt.Errorf("metrics error: %w", err)
}
if err := s.initRPC(cfg); err != nil {
return fmt.Errorf("metrics error: %w", err)
}
return nil
}
func (s *SignerApp) initPprof(cfg *Config) error {
if !cfg.PprofConfig.ListenEnabled {
return nil
}
s.pprofServer = oppprof.New(
cfg.PprofConfig.ListenEnabled,
cfg.PprofConfig.ListenAddr,
cfg.PprofConfig.ListenPort,
cfg.PprofConfig.ProfileType,
cfg.PprofConfig.ProfileDir,
cfg.PprofConfig.ProfileFilename,
)
s.log.Info("Starting pprof server", "addr", cfg.PprofConfig.ListenAddr, "port", cfg.PprofConfig.ListenPort)
if err := s.pprofServer.Start(); err != nil {
return fmt.Errorf("failed to start pprof server: %w", err)
}
return nil
}
func (s *SignerApp) initMetrics(cfg *Config) error {
registry := opmetrics.NewRegistry()
registry.MustRegister(service.MetricSignTransactionTotal)
s.registry = registry // some things require metrics registry
if !cfg.MetricsConfig.Enabled {
return nil
}
metricsCfg := cfg.MetricsConfig
s.log.Info("Starting metrics server", "addr", metricsCfg.ListenAddr, "port", metricsCfg.ListenPort)
metricsServer, err := opmetrics.StartServer(registry, metricsCfg.ListenAddr, metricsCfg.ListenPort)
if err != nil {
return fmt.Errorf("failed to start metrics server: %w", err)
}
s.log.Info("Started metrics server", "endpoint", metricsServer.Addr())
s.metricsServer = metricsServer
return nil
}
func (s *SignerApp) initRPC(cfg *Config) error {
caCert, err := os.ReadFile(cfg.TLSConfig.TLSCaCert)
if err != nil {
return fmt.Errorf("failed to read tls ca cert: %s", string(caCert))
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
cm, err := certman.New(s.log, cfg.TLSConfig.TLSCert, cfg.TLSConfig.TLSKey)
if err != nil {
return fmt.Errorf("failed to read tls cert or key: %w", err)
}
if err := cm.Watch(); err != nil {
return fmt.Errorf("failed to start certman watcher: %w", err)
}
tlsConfig := &tls.Config{
GetCertificate: cm.GetCertificate,
ClientCAs: caCertPool,
ClientAuth: tls.VerifyClientCertIfGiven, // necessary for k8s healthz probes, but we check the cert in service/auth.go
}
serverTlsConfig := &oprpc.ServerTLSConfig{
Config: tlsConfig,
CLIConfig: &cfg.TLSConfig,
}
rpcCfg := cfg.RPCConfig
s.rpc = oprpc.NewServer(
rpcCfg.ListenAddr,
rpcCfg.ListenPort,
s.version,
oprpc.WithLogger(s.log),
oprpc.WithTLSConfig(serverTlsConfig),
oprpc.WithMiddleware(service.NewAuthMiddleware()),
oprpc.WithHTTPRecorder(opmetrics.NewPromHTTPRecorder(s.registry, "signer")),
)
serviceCfg, err := service.ReadConfig(cfg.ServiceConfigPath)
if err != nil {
return fmt.Errorf("failed to read service config: %w", err)
}
s.signer = service.NewSignerService(s.log, serviceCfg)
s.signer.RegisterAPIs(s.rpc)
if err := s.rpc.Start(); err != nil {
return fmt.Errorf("error starting RPC server: %w", err)
}
s.log.Info("Started op-signer RPC server", "addr", s.rpc.Endpoint())
return nil
}
func (s *SignerApp) Start(ctx context.Context) error {
return nil
}
func (s *SignerApp) Stop(ctx context.Context) error {
var result error
if s.rpc != nil {
if err := s.rpc.Stop(); err != nil {
result = errors.Join(result, fmt.Errorf("failed to stop RPC server: %w", err))
}
}
if s.pprofServer != nil {
if err := s.pprofServer.Stop(ctx); err != nil {
result = errors.Join(result, fmt.Errorf("failed to stop pprof server: %w", err))
}
}
if s.metricsServer != nil {
if err := s.metricsServer.Stop(ctx); err != nil {
result = errors.Join(result, fmt.Errorf("failed to stop metrics server: %w", err))
}
}
return result
}
func (s *SignerApp) Stopped() bool {
return s.stopped.Load()
}
var _ cliapp.Lifecycle = (*SignerApp)(nil)
func MainAppAction(version string) cliapp.LifecycleAction {
return func(cliCtx *cli.Context, _ context.CancelCauseFunc) (cliapp.Lifecycle, error) {
cfg := NewConfig(cliCtx)
logger := oplog.NewLogger(cliCtx.App.Writer, cfg.LogConfig)
return InitFromConfig(cliCtx.Context, logger, cfg, version)
}
}
func ClientSign(version string) func(cliCtx *cli.Context) error {
return func(cliCtx *cli.Context) error {
cfg := NewConfig(cliCtx)
if err := cfg.Check(); err != nil {
return fmt.Errorf("invalid CLI flags: %w", err)
}
l := oplog.NewLogger(os.Stdout, cfg.LogConfig)
log.Root().SetHandler(l.GetHandler())
txarg := cliCtx.Args().First()
if txarg == "" {
return errors.New("no transaction argument was provided")
}
txraw, err := hexutil.Decode(txarg)
if err != nil {
return errors.New("failed to decode transaction argument")
}
client, err := client.NewSignerClient(l, cfg.ClientEndpoint, cfg.TLSConfig)
if err != nil {
return err
}
tx := &types.Transaction{}
if err := tx.UnmarshalBinary(txraw); err != nil {
return fmt.Errorf("failed to unmarshal transaction argument: %w", err)
}
tx, err = client.SignTransaction(context.Background(), tx)
if err != nil {
return err
}
result, _ := tx.MarshalJSON()
fmt.Println(string(result))
return nil
}
}

View File

@ -0,0 +1,91 @@
package client
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-service/signer"
optls "github.com/ethereum-optimism/optimism/op-service/tls"
)
type SignerClient struct {
client *rpc.Client
status string
logger log.Logger
}
func NewSignerClient(logger log.Logger, endpoint string, tlsConfig optls.CLIConfig) (*SignerClient, error) {
caCert, err := os.ReadFile(tlsConfig.TLSCaCert)
if err != nil {
return nil, fmt.Errorf("failed to read tls.ca: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
cert, err := tls.LoadX509KeyPair(tlsConfig.TLSCert, tlsConfig.TLSKey)
if err != nil {
return nil, fmt.Errorf("failed to read tls.cert or tls.key: %w", err)
}
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
},
},
}
rpcClient, err := rpc.DialOptions(context.Background(), endpoint, rpc.WithHTTPClient(httpClient))
if err != nil {
return nil, err
}
signerClient := &SignerClient{logger: logger, client: rpcClient}
// Check if reachable
version, err := signerClient.pingVersion()
if err != nil {
return nil, err
}
signerClient.status = fmt.Sprintf("ok [version=%v]", version)
return signerClient, nil
}
func (s *SignerClient) pingVersion() (string, error) {
var v string
if err := s.client.Call(&v, "health_status"); err != nil {
return "", err
}
return v, nil
}
func (s *SignerClient) SignTransaction(
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error) {
args := signer.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
var result hexutil.Bytes
if err := s.client.Call(&result, "eth_signTransaction", args); err != nil {
return nil, fmt.Errorf("eth_signTransaction failed: %w", err)
}
signed := &types.Transaction{}
if err := signed.UnmarshalBinary(result); err != nil {
return nil, err
}
return signed, nil
}

51
op-signer/cmd/main.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/ethereum/go-ethereum/log"
signer "github.com/ethereum-optimism/infra/op-signer"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
)
var (
Version = ""
GitCommit = ""
GitDate = ""
)
func main() {
oplog.SetupDefaults()
app := cli.NewApp()
app.Flags = cliapp.ProtectFlags(signer.CLIFlags("OP_SIGNER"))
app.Version = fmt.Sprintf("%s-%s-%s", Version, GitCommit, GitDate)
app.Name = "op-signer"
app.Usage = "OP Signing Service"
app.Description = ""
app.Commands = []*cli.Command{
{
Name: "client",
Usage: "test client for signer service",
Subcommands: []*cli.Command{
{
Name: "sign",
Usage: "sign a transaction",
Action: signer.ClientSign(Version),
Flags: cliapp.ProtectFlags(signer.ClientSignCLIFlags("SIGNER")),
},
},
},
}
app.Action = cliapp.LifecycleCmd(signer.MainAppAction(Version))
err := app.Run(os.Args)
if err != nil {
log.Crit("Application failed", "message", err)
}
}

85
op-signer/config.go Normal file
View File

@ -0,0 +1,85 @@
package app
import (
"github.com/urfave/cli/v2"
opservice "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
optls "github.com/ethereum-optimism/optimism/op-service/tls"
)
const (
ServiceConfigPathFlagName = "config"
ClientEndpointFlagName = "endpoint"
)
func CLIFlags(envPrefix string) []cli.Flag {
flags := []cli.Flag{
&cli.StringFlag{
Name: ServiceConfigPathFlagName,
Usage: "Signer service configuration file path",
Value: "config.yaml",
EnvVars: opservice.PrefixEnvVar(envPrefix, "SERVICE_CONFIG"),
},
}
flags = append(flags, oprpc.CLIFlags(envPrefix)...)
flags = append(flags, oplog.CLIFlags(envPrefix)...)
flags = append(flags, opmetrics.CLIFlags(envPrefix)...)
flags = append(flags, oppprof.CLIFlags(envPrefix)...)
flags = append(flags, optls.CLIFlags(envPrefix)...)
return flags
}
func ClientSignCLIFlags(envPrefix string) []cli.Flag {
flags := []cli.Flag{
&cli.StringFlag{
Name: ClientEndpointFlagName,
Usage: "Signer endpoint the client will connect to",
Value: "http://localhost:8080",
EnvVars: opservice.PrefixEnvVar(envPrefix, "CLIENT_ENDPOINT"),
},
}
return flags
}
type Config struct {
ClientEndpoint string
ServiceConfigPath string
TLSConfig optls.CLIConfig
RPCConfig oprpc.CLIConfig
LogConfig oplog.CLIConfig
MetricsConfig opmetrics.CLIConfig
PprofConfig oppprof.CLIConfig
}
func (c Config) Check() error {
if err := c.RPCConfig.Check(); err != nil {
return err
}
if err := c.MetricsConfig.Check(); err != nil {
return err
}
if err := c.PprofConfig.Check(); err != nil {
return err
}
if err := c.TLSConfig.Check(); err != nil {
return err
}
return nil
}
func NewConfig(ctx *cli.Context) *Config {
return &Config{
ClientEndpoint: ctx.String(ClientEndpointFlagName),
ServiceConfigPath: ctx.String(ServiceConfigPathFlagName),
TLSConfig: optls.ReadCLIConfig(ctx),
RPCConfig: oprpc.ReadCLIConfig(ctx),
LogConfig: oplog.ReadCLIConfig(ctx),
MetricsConfig: opmetrics.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx),
}
}

3
op-signer/config.yaml Normal file
View File

@ -0,0 +1,3 @@
auth:
- name: localhost
key: projects/my-gcp-project/locations/my-region/keyRings/my-ring/cryptoKeys/my-key/cryptoKeyVersions/1

51
op-signer/gen-local-tls.sh Executable file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
TLS_DIR=$SCRIPT_DIR/tls
version=$(openssl version)
if [[ "$version" != "LibreSSL"* ]] && [[ "$version" != "OpenSSL 1.1"* ]]; then
echo "openssl version: $version"
echo "script only works with LibreSSL (darwin) or OpenSSL 1.1*"
exit 1
fi
echo "Generating mTLS credentials for local development..."
echo ""
mkdir -p "$TLS_DIR"
if [ ! -f "$TLS_DIR/ca.crt" ]; then
echo 'Generating CA'
openssl req -newkey rsa:2048 \
-new -nodes -x509 \
-days 365 \
-sha256 \
-out "$TLS_DIR/ca.crt" \
-keyout "$TLS_DIR/ca.key" \
-subj "/O=OP Labs/CN=root"
fi
echo 'Generating TLS certificate request'
openssl genrsa -out "$TLS_DIR/tls.key" 2048
openssl req -new -key "$TLS_DIR/tls.key" \
-days 1 \
-sha256 \
-out "$TLS_DIR/tls.csr" \
-keyout "$TLS_DIR/tls.key" \
-subj "/O=OP Labs/CN=localhost" \
-extensions san \
-config <(echo '[req]'; echo 'distinguished_name=req'; \
echo '[san]'; echo 'subjectAltName=DNS:localhost')
openssl x509 -req -in "$TLS_DIR/tls.csr" \
-sha256 \
-CA "$TLS_DIR/ca.crt" \
-CAkey "$TLS_DIR/ca.key" \
-CAcreateserial \
-out "$TLS_DIR/tls.crt" \
-days 3 \
-extfile <(echo 'subjectAltName=DNS:localhost')

115
op-signer/go.mod Normal file
View File

@ -0,0 +1,115 @@
module github.com/ethereum-optimism/infra/op-signer
go 1.21
require (
cloud.google.com/go/kms v1.10.1
github.com/ethereum-optimism/optimism v1.4.3-0.20240125020216-6a3b1ec12399
github.com/ethereum/go-ethereum v1.13.5
github.com/golang/mock v1.6.0
github.com/googleapis/gax-go v1.0.3
github.com/googleapis/gax-go/v2 v2.11.0
github.com/holiman/uint256 v1.2.3
github.com/prometheus/client_golang v1.18.0
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.27.1
google.golang.org/protobuf v1.33.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cloud.google.com/go/compute v1.20.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/DataDog/zstd v1.5.2 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.7.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/errors v1.11.1 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/pebble v0.0.0-20231018212520-f6cde3fc2fa4 // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240123193359-a5fc767e225a // indirect
github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
github.com/fjl/memsize v0.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/getsentry/sentry-go v0.18.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/go-bexpr v0.1.11 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/pointerstructure v1.2.1 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/cors v1.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/supranational/blst v0.3.11 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/api v0.126.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.55.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
replace github.com/ethereum/go-ethereum v1.13.5 => github.com/ethereum-optimism/op-geth v1.101305.3-rc.1.0.20240124221225-5c6f10d449ab

496
op-signer/go.sum Normal file
View File

@ -0,0 +1,496 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g=
cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40=
github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo=
github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo=
github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8=
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v0.0.0-20231018212520-f6cde3fc2fa4 h1:PuHFhOUMnD62r80dN+Ik5qco2drekgsUSVdcHsvllec=
github.com/cockroachdb/pebble v0.0.0-20231018212520-f6cde3fc2fa4/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA=
github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI=
github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ethereum-optimism/op-geth v1.101305.3-rc.1.0.20240124221225-5c6f10d449ab h1:SpoQiEG5mteZnaPCo5Pm5QrberdODUefwLjdAtL9LAE=
github.com/ethereum-optimism/op-geth v1.101305.3-rc.1.0.20240124221225-5c6f10d449ab/go.mod h1:/0cPafbmmt3JR0yiuoBJWTRCx8briXUd/mtXr+GJ3Zw=
github.com/ethereum-optimism/optimism v1.4.3-0.20240125020216-6a3b1ec12399 h1:b3I8SKmsLaAJn4TzGpKLzCGBIRsB82f1B7trMZk2+qU=
github.com/ethereum-optimism/optimism v1.4.3-0.20240125020216-6a3b1ec12399/go.mod h1:VGQGQ38ORldOlb3ifKYacmaCLQuVToCj3jax7RjeXys=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240123193359-a5fc767e225a h1:mWIRpGyrAlWHUznUHKgAJUafkNGfO7VmeLjilhVhB80=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240123193359-a5fc767e225a/go.mod h1:/70H/KqrtKcvWvNGVj6S3rAcLC+kUPr3t2aDmYIS+Xk=
github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY=
github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/fjl/memsize v0.0.1 h1:+zhkb+dhUgx0/e+M8sF0QqiouvMQUiKR+QYvdxIOKcQ=
github.com/fjl/memsize v0.0.1/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go v1.0.3 h1:9dMLqhaibYONnDRcnHdUs9P8Mw64jLlZTYlDe3leBtQ=
github.com/googleapis/gax-go v1.0.3/go.mod h1:QyXYajJFdARxGzjwUfbDFIse7Spkw81SJ4LrBJXtlQ8=
github.com/googleapis/gax-go/v2 v2.0.2/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-bexpr v0.1.11 h1:6DqdA/KBjurGby9yTY0bmkathya0lfwF2SeuubCI7dY=
github.com/hashicorp/go-bexpr v0.1.11/go.mod h1:f03lAo0duBlDIUMGCuad8oLcgejw4m7U+N8T+6Kz1AE=
github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw=
github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o=
github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=
github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4=
github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190221220918-438050ddec5e/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o=
google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM=
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

44
op-signer/service/auth.go Normal file
View File

@ -0,0 +1,44 @@
package service
import (
"context"
"net/http"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
optls "github.com/ethereum-optimism/optimism/op-service/tls"
)
type ClientInfo struct {
ClientName string
}
type clientInfoContextKey struct{}
func NewAuthMiddleware() oprpc.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientInfo := ClientInfo{}
// PeerTLSInfo is attached to context by upstream op-service middleware
peerTlsInfo := optls.PeerTLSInfoFromContext(r.Context())
if peerTlsInfo.LeafCertificate == nil {
http.Error(w, "client certificate was not provided", 401)
return
}
// Note that the certificate is already verified by http server if we get here
if len(peerTlsInfo.LeafCertificate.DNSNames) < 1 {
http.Error(w, "client certificate verified but did not contain DNS SAN extension", 401)
return
}
clientInfo.ClientName = peerTlsInfo.LeafCertificate.DNSNames[0]
ctx := context.WithValue(r.Context(), clientInfoContextKey{}, clientInfo)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func ClientInfoFromContext(ctx context.Context) ClientInfo {
info, _ := ctx.Value(clientInfoContextKey{}).(ClientInfo)
return info
}

View File

@ -0,0 +1,63 @@
package service
import (
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"testing"
optls "github.com/ethereum-optimism/optimism/op-service/tls"
"github.com/stretchr/testify/assert"
)
func TestAuthMiddleware_NoCertificate(t *testing.T) {
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.NotNil(t, nil, "handler should not have been invoked")
})
handler = NewAuthMiddleware()(handler)
handler = optls.NewPeerTLSMiddleware(handler)
assert.HTTPStatusCode(t, handler.ServeHTTP, "GET", "/", nil, 401)
}
func TestAuthMiddleware_CertificateMissingDNS(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.TLS = &tls.ConnectionState{PeerCertificates: []*x509.Certificate{}}
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.NotNil(t, nil, "handler should not have been invoked")
})
handler = NewAuthMiddleware()(handler)
handler = optls.NewPeerTLSMiddleware(handler)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
result := rr.Result()
defer result.Body.Close()
assert.Equal(t, 401, result.StatusCode)
}
func TestAuthMiddleware_HappyPath(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{{DNSNames: []string{"client.oplabs.co"}}},
}
handlerInvoked := false
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientInfo := ClientInfoFromContext(r.Context())
assert.Equal(t, "client.oplabs.co", clientInfo.ClientName)
handlerInvoked = true
})
handler = NewAuthMiddleware()(handler)
handler = optls.NewPeerTLSMiddleware(handler)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
result := rr.Result()
defer result.Body.Close()
assert.Equal(t, 200, result.StatusCode)
assert.True(t, handlerInvoked)
}

View File

@ -0,0 +1,62 @@
package service
import (
"errors"
"fmt"
"math/big"
"os"
"github.com/ethereum/go-ethereum/common/hexutil"
"gopkg.in/yaml.v3"
)
type AuthConfig struct {
ClientName string `yaml:"name"`
KeyName string `yaml:"key"`
ToAddresses []string `yaml:"toAddresses"`
MaxValue string `yaml:"maxValue"`
}
func (c AuthConfig) MaxValueToInt() *big.Int {
return hexutil.MustDecodeBig(c.MaxValue)
}
type SignerServiceConfig struct {
Auth []AuthConfig `yaml:"auth"`
}
func ReadConfig(path string) (SignerServiceConfig, error) {
config := SignerServiceConfig{}
data, err := os.ReadFile(path)
if err != nil {
return config, err
}
if err := yaml.Unmarshal(data, &config); err != nil {
return config, err
}
for _, authConfig := range config.Auth {
for _, toAddress := range authConfig.ToAddresses {
if _, err := hexutil.Decode(toAddress); err != nil {
return config, fmt.Errorf("invalid toAddress '%s' in auth config: %w", toAddress, err)
}
if authConfig.MaxValue != "" {
if _, err := hexutil.DecodeBig(authConfig.MaxValue); err != nil {
return config, fmt.Errorf("invalid maxValue '%s' in auth config: %w", toAddress, err)
}
}
}
}
return config, err
}
func (s SignerServiceConfig) GetAuthConfigForClient(clientName string) (*AuthConfig, error) {
if clientName == "" {
return nil, errors.New("client name is empty")
}
for _, ac := range s.Auth {
if ac.ClientName == clientName {
return &ac, nil
}
}
return nil, fmt.Errorf("client '%s' is not authorized to use any keys", clientName)
}

View File

@ -0,0 +1,11 @@
package service
type InvalidTransactionError struct{ message string }
func (e *InvalidTransactionError) Error() string { return e.message }
func (e *InvalidTransactionError) ErrorCode() int { return -32010 }
type UnauthorizedTransactionError struct{ message string }
func (e *UnauthorizedTransactionError) Error() string { return e.message }
func (e *UnauthorizedTransactionError) ErrorCode() int { return -32011 }

View File

@ -0,0 +1,12 @@
package service
import "github.com/prometheus/client_golang/prometheus"
var (
MetricSignTransactionTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "signer_signtransaction_total",
Help: ""},
[]string{"client", "status", "error"},
)
)

View File

@ -0,0 +1,275 @@
//go:generate mockgen -destination=mock_kms.go -package=provider github.com/ethereum-optimism/infra/op-signer/service/provider CloudKMSClient
package provider
import (
"bytes"
"context"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"hash/crc32"
"math/big"
kms "cloud.google.com/go/kms/apiv1"
"cloud.google.com/go/kms/apiv1/kmspb"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/ethereum/go-ethereum/log"
gax "github.com/googleapis/gax-go"
"google.golang.org/protobuf/types/known/wrapperspb"
)
var (
oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
oidNamedCurveSECP256K1 = asn1.ObjectIdentifier{1, 3, 132, 0, 10}
)
type publicKeyInfo struct {
Raw asn1.RawContent
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}
type CloudKMSClient interface {
GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error)
AsymmetricSign(context context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error)
}
type CloudKMSSignatureProvider struct {
logger log.Logger
client CloudKMSClient
}
func NewCloudKMSSignatureProvider(logger log.Logger) SignatureProvider {
ctx := context.Background()
client, err := kms.NewKeyManagementClient(ctx)
if err != nil {
logger.Error("failed to initialize kms client", "error", err)
panic(err)
}
return &CloudKMSSignatureProvider{logger, client}
}
func NewCloudKMSSignatureProviderWithClient(logger log.Logger, client CloudKMSClient) SignatureProvider {
return &CloudKMSSignatureProvider{logger, client}
}
func crc32c(data []byte) uint32 {
t := crc32.MakeTable(crc32.Castagnoli)
return crc32.Checksum(data, t)
}
func createSignRequestFromDigest(keyName string, digest []byte) *kmspb.AsymmetricSignRequest {
digestCRC32C := crc32c(digest)
return &kmspb.AsymmetricSignRequest{
Name: keyName,
Digest: &kmspb.Digest{
Digest: &kmspb.Digest_Sha256{
Sha256: digest,
},
},
DigestCrc32C: wrapperspb.Int64(int64(digestCRC32C)),
}
}
// SignDigest signs the digest with a given Cloud KMS keyname and returns a compact recoverable signature.
// If the keyName provided is not a EC_SIGN_SECP256K1_SHA256 key, the result will be an error.
func (c *CloudKMSSignatureProvider) SignDigest(
ctx context.Context,
keyName string,
digest []byte,
) ([]byte, error) {
publicKey, err := c.GetPublicKey(ctx, keyName)
if err != nil {
return nil, fmt.Errorf("failed to get public key: %w", err)
}
request := createSignRequestFromDigest(keyName, digest)
result, err := c.client.AsymmetricSign(ctx, request)
if err != nil {
return nil, fmt.Errorf("cloud kms sign request failed: %w", err)
}
if result.Name != request.Name {
return nil, errors.New("cloud kms sign request corrupted in transit")
}
if !result.VerifiedDigestCrc32C {
return nil, errors.New("cloud kms sign request corrupted in transit")
}
if int64(crc32c(result.Signature)) != result.SignatureCrc32C.Value {
return nil, errors.New("cloud kms sign response corrupted in transit")
}
c.logger.Debug(fmt.Sprintf("der signature: %s", hexutil.Encode(result.Signature)))
return convertToCompactRecoverableSignature(result.Signature, digest, publicKey)
}
func convertToCompactRecoverableSignature(derSignature, digest, publicKey []byte) ([]byte, error) {
signature, err := convertToCompactSignature(derSignature)
if err != nil {
// should never happen
return nil, fmt.Errorf("failed to convert to compact signature: %w", err)
}
// NOTE: so far I haven't seen CloudKMS produce a malleable signature
// but if it does happen, this can be handled as a retryable error by the client
if err := compactSignatureMalleabilityCheck(signature); err != nil {
// should never happen
return nil, fmt.Errorf("signature failed malleability check: %w", err)
}
if !secp256k1.VerifySignature(publicKey, digest, signature) {
// should never happen
return nil, errors.New("signature could not be verified with public key")
}
recId, err := calculateRecoveryID(signature, digest, publicKey)
if err != nil {
// should never happen
return nil, fmt.Errorf("failed to calculate recovery id: %w", err)
}
signature = append(signature, byte(recId))
return signature, nil
}
// convertToCompactSignature compacts a DER signature output from kms (>70 bytes) into 64 bytes
func convertToCompactSignature(derSignature []byte) ([]byte, error) {
var parsedSig struct{ R, S *big.Int }
if _, err := asn1.Unmarshal(derSignature, &parsedSig); err != nil {
return nil, fmt.Errorf("asn1.Unmarshal error: %w", err)
}
curveOrderLen := 32
signature := make([]byte, 2*curveOrderLen)
// if S is non-canonical, lower it
curveOrder := secp256k1.S256().Params().Params().N
if parsedSig.S.Cmp(new(big.Int).Div(curveOrder, big.NewInt(2))) > 0 {
parsedSig.S = new(big.Int).Sub(curveOrder, parsedSig.S)
}
// left pad R and S with zeroes
rBytes := parsedSig.R.Bytes()
sBytes := parsedSig.S.Bytes()
copy(signature[curveOrderLen-len(rBytes):], rBytes)
copy(signature[len(signature)-len(sBytes):], sBytes)
return signature, nil
}
// calculateRecoveryID calculates the signature recovery id (65th byte, [0-3])
func calculateRecoveryID(signature, digest, pubKey []byte) (int, error) {
recId := -1
var errorRes error
for i := 0; i < 4; i++ {
recSig := append(signature, byte(i))
publicKey, err := secp256k1.RecoverPubkey(digest, recSig)
if err != nil {
errorRes = err
continue
}
if bytes.Equal(publicKey, pubKey) {
recId = i
break
}
}
if recId == -1 {
return recId, fmt.Errorf("failed to calculate recovery id, should never happen: %w", errorRes)
}
return recId, nil
}
// compactSignatureMalleabilityCheck checks if signature can be used to produce a new valid signature
// pulled from go-ethereum/crypto/secp256k1/secp256_test.go
// see: http://coders-errand.com/malleability-ecdsa-signatures/
func compactSignatureMalleabilityCheck(sig []byte) error {
b := int(sig[32])
if b < 0 {
return fmt.Errorf("highest bit is negative: %d", b)
}
if ((b >> 7) == 1) != ((b & 0x80) == 0x80) {
return fmt.Errorf("highest bit: %d bit >> 7: %d", b, b>>7)
}
if (b & 0x80) == 0x80 {
return fmt.Errorf("highest bit: %d bit & 0x80: %d", b, b&0x80)
}
return nil
}
// GetPublicKey returns a decoded secp256k1 public key.
func (c *CloudKMSSignatureProvider) GetPublicKey(
ctx context.Context,
keyName string,
) ([]byte, error) {
request := kmspb.GetPublicKeyRequest{
Name: keyName,
}
result, err := c.client.GetPublicKey(ctx, &request)
if err != nil {
return nil, fmt.Errorf("kms get public key request failed: %w", err)
}
key := []byte(result.Pem)
if int64(crc32c(key)) != result.PemCrc32C.Value {
return nil, errors.New("cloud kms public key response corrupted in transit")
}
return decodePublicKeyPEM(key)
}
// decodePublicKeyPEM decodes a PEM ECDSA public key with secp256k1 curve
func decodePublicKeyPEM(key []byte) ([]byte, error) {
block, rest := pem.Decode([]byte(key))
if len(rest) > 0 {
return nil, fmt.Errorf("crypto: failed to parse PEM string, not all bytes in PEM key were decoded: %x", rest)
}
pkBytes, err := x509ParseECDSAPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("crypto: failed to parse PEM string: %w", err)
}
return pkBytes, err
}
// x509ParseECDSAPublicKey parses a DER-encoded public key and ensures secp256k1 curve
func x509ParseECDSAPublicKey(derBytes []byte) ([]byte, error) {
var pki publicKeyInfo
if rest, err := asn1.Unmarshal(derBytes, &pki); err != nil {
return nil, err
} else if len(rest) != 0 {
return nil, errors.New("x509: trailing data after ASN.1 of public-key")
}
if !pki.Algorithm.Algorithm.Equal(oidPublicKeyECDSA) {
return nil, errors.New("x509: unknown public key algorithm")
}
asn1Data := pki.PublicKey.RightAlign()
paramsData := pki.Algorithm.Parameters.FullBytes
namedCurveOID := new(asn1.ObjectIdentifier)
rest, err := asn1.Unmarshal(paramsData, namedCurveOID)
if err != nil {
return nil, fmt.Errorf("x509: failed to parse ECDSA parameters as named curve: %w", err)
}
if len(rest) != 0 {
return nil, errors.New("x509: trailing data after ECDSA parameters")
}
if !namedCurveOID.Equal(oidNamedCurveSECP256K1) {
return nil, errors.New("x509: unsupported elliptic curve")
}
if asn1Data[0] != 4 { // uncompressed form
return nil, errors.New("x509: only uncompressed keys are supported")
}
return asn1Data, nil
}

View File

@ -0,0 +1,194 @@
package provider
import (
"context"
"crypto/ecdsa"
"crypto/rand"
"io"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cloud.google.com/go/kms/apiv1/kmspb"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/ethereum/go-ethereum/log"
)
const (
CloudKMSPemPublicKey = `-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEQpdToIk9lwjBdl0VcqXl7AwqhB9NwRf+
IHRNqIUNa8vAH/5l5MGXO/qVT5D/4sOTfpd29BQAkDVOgTAneA2Vrg==
-----END PUBLIC KEY-----`
)
func generateKey() *ecdsa.PrivateKey {
key, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
if err != nil {
panic(err)
}
return key
}
// pulled from go-ethereum/crypto/secp256k1/secp256_test.go
func csprngEntropy(n int) []byte {
buf := make([]byte, n)
if _, err := io.ReadFull(rand.Reader, buf); err != nil {
panic("reading from crypto/rand failed: " + err.Error())
}
return buf
}
func TestCloudKMS_SignDigest(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := NewMockCloudKMSClient(ctrl)
provider := NewCloudKMSSignatureProviderWithClient(log.Root(), mockClient)
keyName := "keyName"
digest, _ := hexutil.Decode("0x8dabbae6d856bb7ab93bc35b74c1303975a3f70f942d033e8591a9f8c897ae42")
derSignature, _ := hexutil.Decode("0x30450221008680faa49fd6653d273fb34393a47efac44b8f4a4de62bbe11a65ee53739e9bb0220350897677c32d67dc1e520d7458c5cca4a7fe49a3e9d74bdef1ec96836148661")
pemPublicKey := []byte(CloudKMSPemPublicKey)
var tests = []struct {
testName string
keyName string
digest []byte
respError error
respVerifiedDigestCrc32C bool
respSignatureCrc32 uint32
wantErr bool
}{
{"happy path", keyName, digest, nil, true, crc32c(derSignature), false},
{"req failure", keyName, digest, assert.AnError, true, crc32c(derSignature), true},
{"invalid req keyName", "wrongKeyName", digest, nil, true, crc32c(derSignature), true},
{"invalid req crc32", keyName, digest, nil, false, crc32c(derSignature), true},
{"invalid resp crc32", keyName, digest, nil, true, crc32c([]byte("")), true},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
signRequest := createSignRequestFromDigest(tt.keyName, tt.digest)
mockClient.EXPECT().AsymmetricSign(gomock.Any(), signRequest).Return(
&kmspb.AsymmetricSignResponse{
Name: keyName,
Signature: derSignature,
VerifiedDigestCrc32C: tt.respVerifiedDigestCrc32C,
SignatureCrc32C: wrapperspb.Int64(int64(tt.respSignatureCrc32)),
},
tt.respError,
)
getPublicKeyRequest := &kmspb.GetPublicKeyRequest{Name: tt.keyName}
mockClient.EXPECT().GetPublicKey(gomock.Any(), getPublicKeyRequest).Return(
&kmspb.PublicKey{
Pem: string(pemPublicKey),
PemCrc32C: wrapperspb.Int64(int64(crc32c(pemPublicKey))),
},
nil,
)
publicKey, _ := decodePublicKeyPEM(pemPublicKey)
wantSignature, _ := convertToCompactRecoverableSignature(derSignature, tt.digest, publicKey)
signature, err := provider.SignDigest(context.TODO(), tt.keyName, tt.digest)
if !tt.wantErr {
assert.Nil(t, err)
assert.Equal(t, wantSignature, signature)
// make sure recoverable pubkey is as expected
recoveredPublicKey, err := crypto.Ecrecover(tt.digest, signature)
assert.Nil(t, err)
assert.Equal(t, publicKey, recoveredPublicKey)
} else {
assert.Error(t, err)
assert.Nil(t, signature)
}
})
}
}
func TestCloudKMS_GetPublicKey(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := NewMockCloudKMSClient(ctrl)
provider := NewCloudKMSSignatureProviderWithClient(log.Root(), mockClient)
keyName := "keyName"
pemPublicKey := []byte(CloudKMSPemPublicKey)
wantPublicKey, _ := hexutil.Decode("0x04429753a0893d9708c1765d1572a5e5ec0c2a841f4dc117fe20744da8850d6bcbc01ffe65e4c1973bfa954f90ffe2c3937e9776f4140090354e813027780d95ae")
var tests = []struct {
testName string
keyName string
respError error
respPemCrc32 uint32
wantErr bool
}{
{"happy path", keyName, nil, crc32c(pemPublicKey), false},
{"req failure", keyName, assert.AnError, crc32c(pemPublicKey), false},
{"invalid resp crc32", keyName, assert.AnError, crc32c([]byte("")), true},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
request := &kmspb.GetPublicKeyRequest{Name: tt.keyName}
mockClient.EXPECT().GetPublicKey(gomock.Any(), request).Return(
&kmspb.PublicKey{
Pem: string(pemPublicKey),
PemCrc32C: wrapperspb.Int64(int64(tt.respPemCrc32)),
},
nil,
)
publicKey, err := provider.GetPublicKey(context.TODO(), tt.keyName)
if !tt.wantErr {
assert.Nil(t, err)
assert.Equal(t, wantPublicKey, publicKey)
} else {
assert.Error(t, err)
assert.Nil(t, publicKey)
}
})
}
}
// TestVerifySignatureFromRecoveredPublicKey tests that the compact signature can
// recover a publicKey that can be used to verify the original DER signature.
// Since all other reference implementations produce compact, recoverable signatures already,
// this serves as the test that converToCompactSignture and calculateRecoveryID produce expected results,
func TestVerifySignatureFromRecoveredPublicKey(t *testing.T) {
key := generateKey()
pubKey := crypto.FromECDSAPub(&key.PublicKey)
const TestCount = 1000
for i := 0; i < TestCount; i++ {
digest := csprngEntropy(32)
derSig, err := ecdsa.SignASN1(rand.Reader, key, digest)
if err != nil {
panic(err)
}
sig, err := convertToCompactSignature(derSig)
assert.Nil(t, err)
assert.Len(t, sig, 64)
assert.Nil(t, compactSignatureMalleabilityCheck(sig))
recId, err := calculateRecoveryID(sig, digest, pubKey)
assert.Nil(t, err)
assert.GreaterOrEqual(t, recId, 0)
assert.Less(t, recId, 4)
sig = append(sig, byte(recId))
recoveredRawPubKey, err := secp256k1.RecoverPubkey(digest, sig)
assert.Nil(t, err)
recoveredPubKey, err := crypto.UnmarshalPubkey(recoveredRawPubKey)
require.NoError(t, err)
assert.True(t, ecdsa.VerifyASN1(recoveredPubKey, digest, derSig))
}
}

View File

@ -0,0 +1,77 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ethereum-optimism/infra/op-signer/service/provider (interfaces: CloudKMSClient)
// Package provider is a generated GoMock package.
package provider
import (
context "context"
reflect "reflect"
kmspb "cloud.google.com/go/kms/apiv1/kmspb"
gomock "github.com/golang/mock/gomock"
gax "github.com/googleapis/gax-go/v2"
)
// MockCloudKMSClient is a mock of CloudKMSClient interface.
type MockCloudKMSClient struct {
ctrl *gomock.Controller
recorder *MockCloudKMSClientMockRecorder
}
// MockCloudKMSClientMockRecorder is the mock recorder for MockCloudKMSClient.
type MockCloudKMSClientMockRecorder struct {
mock *MockCloudKMSClient
}
// NewMockCloudKMSClient creates a new mock instance.
func NewMockCloudKMSClient(ctrl *gomock.Controller) *MockCloudKMSClient {
mock := &MockCloudKMSClient{ctrl: ctrl}
mock.recorder = &MockCloudKMSClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCloudKMSClient) EXPECT() *MockCloudKMSClientMockRecorder {
return m.recorder
}
// AsymmetricSign mocks base method.
func (m *MockCloudKMSClient) AsymmetricSign(arg0 context.Context, arg1 *kmspb.AsymmetricSignRequest, arg2 ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AsymmetricSign", varargs...)
ret0, _ := ret[0].(*kmspb.AsymmetricSignResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AsymmetricSign indicates an expected call of AsymmetricSign.
func (mr *MockCloudKMSClientMockRecorder) AsymmetricSign(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsymmetricSign", reflect.TypeOf((*MockCloudKMSClient)(nil).AsymmetricSign), varargs...)
}
// GetPublicKey mocks base method.
func (m *MockCloudKMSClient) GetPublicKey(arg0 context.Context, arg1 *kmspb.GetPublicKeyRequest, arg2 ...gax.CallOption) (*kmspb.PublicKey, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetPublicKey", varargs...)
ret0, _ := ret[0].(*kmspb.PublicKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPublicKey indicates an expected call of GetPublicKey.
func (mr *MockCloudKMSClientMockRecorder) GetPublicKey(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKey", reflect.TypeOf((*MockCloudKMSClient)(nil).GetPublicKey), varargs...)
}

View File

@ -0,0 +1,65 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ethereum-optimism/infra/op-signer/service/provider (interfaces: SignatureProvider)
// Package provider is a generated GoMock package.
package provider
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockSignatureProvider is a mock of SignatureProvider interface.
type MockSignatureProvider struct {
ctrl *gomock.Controller
recorder *MockSignatureProviderMockRecorder
}
// MockSignatureProviderMockRecorder is the mock recorder for MockSignatureProvider.
type MockSignatureProviderMockRecorder struct {
mock *MockSignatureProvider
}
// NewMockSignatureProvider creates a new mock instance.
func NewMockSignatureProvider(ctrl *gomock.Controller) *MockSignatureProvider {
mock := &MockSignatureProvider{ctrl: ctrl}
mock.recorder = &MockSignatureProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSignatureProvider) EXPECT() *MockSignatureProviderMockRecorder {
return m.recorder
}
// GetPublicKey mocks base method.
func (m *MockSignatureProvider) GetPublicKey(arg0 context.Context, arg1 string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPublicKey", arg0, arg1)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPublicKey indicates an expected call of GetPublicKey.
func (mr *MockSignatureProviderMockRecorder) GetPublicKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKey", reflect.TypeOf((*MockSignatureProvider)(nil).GetPublicKey), arg0, arg1)
}
// SignDigest mocks base method.
func (m *MockSignatureProvider) SignDigest(arg0 context.Context, arg1 string, arg2 []byte) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SignDigest", arg0, arg1, arg2)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SignDigest indicates an expected call of SignDigest.
func (mr *MockSignatureProviderMockRecorder) SignDigest(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignDigest", reflect.TypeOf((*MockSignatureProvider)(nil).SignDigest), arg0, arg1, arg2)
}

View File

@ -0,0 +1,9 @@
//go:generate mockgen -destination=mock_provider.go -package=provider github.com/ethereum-optimism/infra/op-signer/service/provider SignatureProvider
package provider
import "context"
type SignatureProvider interface {
SignDigest(ctx context.Context, keyName string, digest []byte) ([]byte, error)
GetPublicKey(ctx context.Context, keyName string) ([]byte, error)
}

View File

@ -0,0 +1,148 @@
package service
import (
"context"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/ethereum-optimism/infra/op-signer/service/provider"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/signer"
)
type SignerService struct {
logger log.Logger
config SignerServiceConfig
provider provider.SignatureProvider
}
func NewSignerService(logger log.Logger, config SignerServiceConfig) *SignerService {
return NewSignerServiceWithProvider(logger, config, provider.NewCloudKMSSignatureProvider(logger))
}
func NewSignerServiceWithProvider(
logger log.Logger,
config SignerServiceConfig,
provider provider.SignatureProvider,
) *SignerService {
return &SignerService{logger, config, provider}
}
func (s *SignerService) RegisterAPIs(server *oprpc.Server) {
server.AddAPI(rpc.API{
Namespace: "eth",
Service: s,
})
}
func containsNormalized(s []string, e string) bool {
for _, a := range s {
if strings.EqualFold(a, e) {
return true
}
}
return false
}
// SignTransaction will sign the given transaction with the key configured for the authenticated client
func (s *SignerService) SignTransaction(ctx context.Context, args signer.TransactionArgs) (hexutil.Bytes, error) {
clientInfo := ClientInfoFromContext(ctx)
authConfig, err := s.config.GetAuthConfigForClient(clientInfo.ClientName)
if err != nil {
return nil, rpc.HTTPError{StatusCode: 403, Status: "Forbidden", Body: []byte(err.Error())}
}
labels := prometheus.Labels{"client": clientInfo.ClientName, "status": "error", "error": ""}
defer func() {
MetricSignTransactionTotal.With(labels).Inc()
}()
if err := args.Check(); err != nil {
s.logger.Warn("invalid signing arguments", "err", err)
labels["error"] = "invalid_transaction"
return nil, &InvalidTransactionError{message: err.Error()}
}
if len(authConfig.ToAddresses) > 0 && !containsNormalized(authConfig.ToAddresses, args.To.Hex()) {
return nil, &UnauthorizedTransactionError{"to address not authorized"}
}
if len(authConfig.MaxValue) > 0 && args.Value.ToInt().Cmp(authConfig.MaxValueToInt()) > 0 {
return nil, &UnauthorizedTransactionError{"value exceeds maximum"}
}
txData, err := args.ToTransactionData()
if err != nil {
labels["error"] = "transaction_args_error"
return nil, &InvalidTransactionError{err.Error()}
}
tx := types.NewTx(txData)
txSigner := types.LatestSignerForChainID(tx.ChainId())
digest := txSigner.Hash(tx)
signature, err := s.provider.SignDigest(ctx, authConfig.KeyName, digest.Bytes())
if err != nil {
labels["error"] = "sign_error"
return nil, &InvalidTransactionError{err.Error()}
}
signed, err := tx.WithSignature(txSigner, signature)
if err != nil {
labels["error"] = "invalid_transaction_error"
return nil, &InvalidTransactionError{err.Error()}
}
signerFrom, err := txSigner.Sender(signed)
if err != nil {
labels["error"] = "sign_error"
return nil, &InvalidTransactionError{err.Error()}
}
// sanity check that we used the right account
if args.From != nil && *args.From != signerFrom {
s.logger.Warn("user is trying to sign with different account than actual signer-provider",
"provider", signerFrom, "request", *args.From)
labels["error"] = "sign_error"
return nil, &InvalidTransactionError{"unexpected from address"}
}
txraw, err := signed.MarshalBinary()
if err != nil {
labels["error"] = "transaction_marshal_error"
return nil, &InvalidTransactionError{err.Error()}
}
labels["status"] = "success"
txTo := ""
if tx.To() != nil {
txTo = tx.To().Hex()
}
s.logger.Info(
"Signed transaction",
"digest", hexutil.Encode(digest.Bytes()),
"client.name", clientInfo.ClientName,
"client.keyname", authConfig.KeyName,
"tx.type", tx.Type(),
"tx.raw", hexutil.Encode(txraw),
"tx.value", tx.Value(),
"tx.to", txTo,
"tx.nonce", tx.Nonce(),
"tx.gas", tx.Gas(),
"tx.gasprice", tx.GasPrice(),
"tx.gastipcap", tx.GasTipCap(),
"tx.gasfeecap", tx.GasFeeCap(),
"tx.type", tx.Type(),
"tx.hash", tx.Hash().Hex(),
"tx.chainid", tx.ChainId(),
"tx.blobhashes", tx.BlobHashes(),
"tx.blobfeecap", tx.BlobGasFeeCap(),
"signature", hexutil.Encode(signature),
)
return hexutil.Bytes(txraw), nil
}

View File

@ -0,0 +1,182 @@
package service
import (
"context"
"errors"
"math/big"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/infra/op-signer/service/provider"
clientSigner "github.com/ethereum-optimism/optimism/op-service/signer"
)
func createEIP1559Tx() *types.Transaction {
aa := common.HexToAddress("0x000000000000000000000000000000000000aaaa")
accesses := types.AccessList{types.AccessTuple{
Address: aa,
StorageKeys: []common.Hash{{0}},
}}
txdata := &types.DynamicFeeTx{
ChainID: params.AllEthashProtocolChanges.ChainID,
Nonce: 0,
To: &aa,
Gas: 30000,
GasFeeCap: big.NewInt(1),
GasTipCap: big.NewInt(1),
AccessList: accesses,
Data: []byte{},
Value: big.NewInt(1),
}
tx := types.NewTx(txdata)
return tx
}
func createBlobTx() *types.Transaction {
aa := common.HexToAddress("0x000000000000000000000000000000000000aaaa")
accesses := types.AccessList{types.AccessTuple{
Address: aa,
StorageKeys: []common.Hash{{0}},
}}
txdata := &types.BlobTx{
ChainID: uint256.MustFromBig(params.AllEthashProtocolChanges.ChainID),
Nonce: 0,
To: aa,
Gas: 30000,
GasFeeCap: uint256.NewInt(1),
GasTipCap: uint256.NewInt(1),
AccessList: accesses,
Data: []byte{},
Value: uint256.NewInt(1),
BlobFeeCap: uint256.NewInt(1),
BlobHashes: []common.Hash{common.HexToHash("c0ffee")},
}
tx := types.NewTx(txdata)
return tx
}
var config = SignerServiceConfig{
Auth: []AuthConfig{
{ClientName: "client.oplabs.co", KeyName: "keyName"},
{ClientName: "alt-client.oplabs.co", KeyName: "altKeyName"},
{ClientName: "authorized-to.oplabs.co", KeyName: "keyName", ToAddresses: []string{"0x000000000000000000000000000000000000Aaaa"}},
{ClientName: "unauthorized-to.oplabs.co", KeyName: "keyName", ToAddresses: []string{"0x000000000000000000000000000000000000bbbb"}},
{ClientName: "within-max-value.oplabs.co", KeyName: "keyName", MaxValue: hexutil.EncodeBig(big.NewInt(2))},
{ClientName: "exceeds-max-value.oplabs.co", KeyName: "keyName", MaxValue: hexutil.EncodeBig(big.NewInt(0))},
},
}
type testCase struct {
name string
template func() *types.Transaction
}
var testTxs = []testCase{
{"regular", createEIP1559Tx},
{"blob-tx", createBlobTx},
}
func TestSignTransaction(t *testing.T) {
for _, tc := range testTxs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
testSignTransaction(t, tc.template())
})
}
}
func testSignTransaction(t *testing.T, tx *types.Transaction) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
signer := types.LatestSignerForChainID(tx.ChainId())
digest := signer.Hash(tx).Bytes()
priv, err := crypto.GenerateKey()
require.NoError(t, err)
sender := crypto.PubkeyToAddress(priv.PublicKey)
signature, err := crypto.Sign(digest, priv)
require.NoError(t, err)
args := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
missingNonce := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
missingNonce.Nonce = nil
validFrom := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
validFrom.From = &sender
invalidFrom := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
random := common.HexToAddress("1234")
invalidFrom.From = &random
// signature, _ := hexutil.Decode("0x5392c93b50eb9e3412ab43d378048d4f7d644f3cea02acb529f07e2babba1d3a332377f4abe24a40030b3ff6bff3413a44364aad4665f4e24117466328ce8d3600")
tests := []struct {
testName string
args clientSigner.TransactionArgs
digest []byte
clientName string
wantKeyName string
wantErrCode int
}{
{"happy path", *args, digest, "client.oplabs.co", "keyName", 0},
{"nonce not specified", *missingNonce, digest, "client.oplabs.co", "keyName", -32010},
{"happy path - different client and key", *args, digest, "alt-client.oplabs.co", "altKeyName", 0},
{"client not authorized", *args, digest, "forbidden-client.oplabs.co", "keyName", 403},
{"client empty", *args, digest, "", "", 403},
{"authorized to address", *args, digest, "authorized-to.oplabs.co", "keyName", 0},
{"unauthorized to address", *args, digest, "unauthorized-to.oplabs.co", "keyName", -32011},
{"within max value", *args, digest, "within-max-value.oplabs.co", "keyName", 0},
{"exceeds max value", *args, digest, "exceeds-max-value.oplabs.co", "keyName", -32011},
{"valid from", *validFrom, digest, "client.oplabs.co", "keyName", 0},
{"invalid from", *invalidFrom, digest, "client.oplabs.co", "keyName", -32010},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
mockSignatureProvider := provider.NewMockSignatureProvider(ctrl)
service := NewSignerServiceWithProvider(log.Root(), config, mockSignatureProvider)
ctx := context.WithValue(context.TODO(), clientInfoContextKey{}, ClientInfo{ClientName: tt.clientName})
if tt.wantErrCode == 0 || tt.testName == "invalid from" {
mockSignatureProvider.EXPECT().
SignDigest(ctx, tt.wantKeyName, tt.digest).
Return(signature, nil)
}
resp, err := service.SignTransaction(ctx, tt.args)
if tt.wantErrCode == 0 {
assert.Nil(t, err)
if assert.NotNil(t, resp) {
assert.NotEmpty(t, resp)
}
} else {
assert.NotNil(t, err)
assert.Nil(t, resp)
var rpcErr rpc.Error
var httpErr rpc.HTTPError
if errors.As(err, &rpcErr) {
assert.Equal(t, tt.wantErrCode, rpcErr.ErrorCode())
} else if errors.As(err, &httpErr) {
assert.Equal(t, tt.wantErrCode, httpErr.StatusCode)
} else {
assert.Fail(t, "returned error is not an rpc.Error or rpc.HTTPError")
}
}
})
}
}

53
op-signer/test-rpc.json Normal file
View File

@ -0,0 +1,53 @@
[
{
"id": "1",
"jsonrpc": "2.0",
"method": "eth_signTransaction",
"params": [
{
"to": "0x000000000000000000000000000000000000aaaa",
"gas": "0x7530",
"gasPrice": null,
"maxFeePerGas": "0x1",
"maxPriorityFeePerGas": "0x1",
"value": "0x1",
"nonce": "0x0",
"data": "0x",
"accessList": [
{
"address": "0x000000000000000000000000000000000000aaaa",
"storageKeys": [
"0x0000000000000000000000000000000000000000000000000000000000000000"
]
}
],
"chainId": "0x539"
}
]
},
{
"id": "1",
"jsonrpc": "2.0",
"method": "eth_signTransaction",
"params": [
{
"to": "0x000000000000000000000000000000000000aaaa",
"gas": "0x7530",
"gasPrice": null,
"maxFeePerGas": "0x1",
"maxPriorityFeePerGas": "0x1",
"value": "0x1",
"data": "0x",
"accessList": [
{
"address": "0x000000000000000000000000000000000000aaaa",
"storageKeys": [
"0x0000000000000000000000000000000000000000000000000000000000000000"
]
}
],
"chainId": "0x539"
}
]
}
]

1
op-txproxy/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
bin

18
op-txproxy/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM golang:1.21.3-alpine3.18 as builder
COPY ./op-txproxy /app
WORKDIR /app
RUN apk --no-cache add make jq bash git alpine-sdk
RUN make build
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
COPY --from=builder /app/bin/op-txproxy /app
ENTRYPOINT ["/app/op-txproxy"]

28
op-txproxy/Makefile Normal file
View File

@ -0,0 +1,28 @@
GIT_COMMIT := $(shell git rev-parse HEAD)
GIT_DATE := $(shell git show -s --format='%ct')
LDFLAGSSTRING +=-X main.GitCommit=$(GIT_COMMIT)
LDFLAGSSTRING +=-X main.GitDate=$(GITDGIT_DATEATE)
LDFLAGSSTRING +=-X main.Version=$(OP_CONDUCTOR_MON_VERSION)
LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
all: build
build:
env GO111MODULE=on go build -v $(LDFLAGS) -o ./bin/op-txproxy ./cmd
clean:
rm ./bin/op-txproxy
test:
go test -v ./...
lint:
golangci-lint run ./...
.PHONY: \
build \
clean \
test \
lint

36
op-txproxy/README.md Normal file
View File

@ -0,0 +1,36 @@
# op-txproxy
A supplemental passthrough proxy for some execution engine endpoints. This proxy does not forward all rpc traffic and only exposes a specific set of endpoints.
Operationally, the public ingress proxy should only re-route requests for these endpoints.
```mermaid
stateDiagram-v2
proxyd --> txproxy: intercepted methods
proxyd --> backend: unintercepted methods
txproxy --> backend
```
## Setup
Install go 1.21
```
make build
./bin/op-txproxy --help
```
## Endpoints
### eth_sendRawTransactionConditional
An outcome of how to integrate this [spec](https://notes.ethereum.org/@yoav/SkaX2lS9j) safely for permissionless 4337 bundler participation. This solution in the design doc [proposal](https://github.com/ethereum-optimism/design-docs/blob/main/ecosystem/sendRawTransactionConditional/proposal.md)
requires a validating proxy that can be horizontally scaled and pre-emptively reject invalid conditional transaction. The implemented endpoint covers
these objectives:
1. **Auth**. preemptively put in place to enable a variety of auth policies (allowlist, rate limits, etc).
The caller authenticates themselves with any valid ECDSA-secp256k1 key, like an Ethereum key. The computed signature is over the [EIP-191](https://eips.ethereum.org/EIPS/eip-191) hash of the request body (up to the 5MB request body limit).
With the signature and signing address, the request is authenticated via the `X-Optimism-Signature` header of the request with the value `<public key address>: <signature>`.
2. **Rate Limits**. global rate limits on the endpoint are applied here.
2. **Rejection Switch**. this proxy can be rolled with a flag/env switch to reject conditional transaction without needing to interrupt the execution engine.
3. **Basic Validation**. stateless validation is done in the endpoint to reject invalid conditional transactions and apply additional restricts on the usage (only 4337 entrypoint tx target support).
4. **Metrics**. performance of this endpoint can be observed in order to inform adjustments to rate limits, shutoff, or auth policies to implement.

101
op-txproxy/auth_handler.go Normal file
View File

@ -0,0 +1,101 @@
package op_txproxy
import (
"bytes"
"context"
"io"
"net/http"
"strings"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
var (
defaultBodyLimit = 5 * 1024 * 1024 // default in op-geth
DefaultAuthHeaderKey = "X-Optimism-Signature"
)
type authHandler struct {
headerKey string
next http.Handler
}
// This middleware detects when authentication information is present on the request. If
// so, it will validate and set the caller in the request context. It does not reject
// if authentication information is missing. It is up to the request handler to do so via
// the missing `AuthContext`
// - NOTE: only up to the default body limit (5MB) is read when constructing the text hash
// that is signed over by the caller
func AuthMiddleware(headerKey string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return &authHandler{headerKey, next}
}
}
type authContextKey struct{}
type AuthContext struct {
Caller common.Address
}
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get(h.headerKey)
if authHeader == "" {
h.next.ServeHTTP(w, r)
return
}
authElems := strings.Split(authHeader, ":")
if len(authElems) != 2 {
http.Error(w, "misformatted auth header", http.StatusBadRequest)
return
}
if r.Body == nil {
// edge case from unit tests
r.Body = io.NopCloser(bytes.NewBuffer(nil))
}
// Since this middleware runs prior to the server, we need to manually apply the body limit when reading.
bodyBytes, err := io.ReadAll(io.LimitReader(r.Body, int64(defaultBodyLimit)))
if err != nil {
http.Error(w, "unable to parse request body", http.StatusInternalServerError)
return
}
r.Body = struct {
io.Reader
io.Closer
}{
io.MultiReader(bytes.NewReader(bodyBytes), r.Body),
r.Body,
}
txtHash := accounts.TextHash(bodyBytes)
caller, signature := common.HexToAddress(authElems[0]), common.FromHex(authElems[1])
sigPubKey, err := crypto.SigToPub(txtHash, signature)
if err != nil {
http.Error(w, "invalid authentication signature", http.StatusBadRequest)
return
}
if caller != crypto.PubkeyToAddress(*sigPubKey) {
http.Error(w, "mismatched recovered signer", http.StatusBadRequest)
return
}
// Set the authenticated caller in the context
newCtx := context.WithValue(r.Context(), authContextKey{}, &AuthContext{caller})
h.next.ServeHTTP(w, r.WithContext(newCtx))
}
func AuthFromContext(ctx context.Context) *AuthContext {
auth, ok := ctx.Value(authContextKey{}).(*AuthContext)
if !ok {
return nil
}
return auth
}

View File

@ -0,0 +1,148 @@
package op_txproxy
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
)
var pingHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ping")
})
func TestAuthHandlerMissingAuth(t *testing.T) {
handler := authHandler{next: pingHandler}
rr := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
handler.ServeHTTP(rr, r)
// simply forwards the request
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "ping", rr.Body.String())
}
func TestAuthHandlerBadHeader(t *testing.T) {
handler := authHandler{headerKey: "auth", next: pingHandler}
rr := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("auth", "foobarbaz")
handler.ServeHTTP(rr, r)
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestAuthHandlerBadSignature(t *testing.T) {
handler := authHandler{headerKey: "auth", next: pingHandler}
rr := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("auth", fmt.Sprintf("%s:%s", common.HexToAddress("0xa"), "foobar"))
handler.ServeHTTP(rr, r)
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestAuthHandlerMismatchedCaller(t *testing.T) {
handler := authHandler{headerKey: "auth", next: pingHandler}
rr := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", strings.NewReader("body"))
privKey, _ := crypto.GenerateKey()
sig, _ := crypto.Sign(accounts.TextHash([]byte("body")), privKey)
r.Header.Set("auth", fmt.Sprintf("%s:%s", common.HexToAddress("0xa"), sig))
handler.ServeHTTP(rr, r)
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestAuthHandlerSetContext(t *testing.T) {
var ctx *AuthContext
ctxHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx = AuthFromContext(r.Context())
w.WriteHeader(http.StatusOK)
})
handler := authHandler{headerKey: "auth", next: ctxHandler}
rr := httptest.NewRecorder()
body := bytes.NewBufferString("body")
r, _ := http.NewRequest("GET", "/", body)
privKey, _ := crypto.GenerateKey()
sig, _ := crypto.Sign(accounts.TextHash(body.Bytes()), privKey)
addr := crypto.PubkeyToAddress(privKey.PublicKey)
r.Header.Set("auth", fmt.Sprintf("%s:%s", addr, common.Bytes2Hex(sig)))
handler.ServeHTTP(rr, r)
require.Equal(t, http.StatusOK, rr.Code)
require.NotNil(t, ctx)
require.Equal(t, addr, ctx.Caller)
}
func TestAuthHandlerRpcMiddleware(t *testing.T) {
rpcServer := oprpc.NewServer("127.0.0.1", 0, "", oprpc.WithMiddleware(AuthMiddleware("auth")))
require.NoError(t, rpcServer.Start())
t.Cleanup(func() { _ = rpcServer.Stop() })
url := fmt.Sprintf("http://%s", rpcServer.Endpoint())
clnt, err := rpc.Dial(url)
require.NoError(t, err)
defer clnt.Close()
// pass without auth (default handler does not deny)
err = clnt.CallContext(context.Background(), nil, "rpc_modules")
require.Nil(t, err)
// denied with bad auth header
clnt.SetHeader("auth", "foobar")
err = clnt.CallContext(context.Background(), nil, "rpc_modules")
require.NotNil(t, err)
}
func TestAuthHandlerRequestBodyLimit(t *testing.T) {
var body []byte
bodyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
})
handler := authHandler{headerKey: "auth", next: bodyHandler}
// only up to limit is read when validating the request body
authBody := strings.Repeat("*", defaultBodyLimit)
excess := strings.Repeat("-", 10)
rr := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", strings.NewReader(authBody+excess))
// sign over just the auth body
privKey, _ := crypto.GenerateKey()
sig, _ := crypto.Sign(accounts.TextHash([]byte(authBody)), privKey)
addr := crypto.PubkeyToAddress(privKey.PublicKey)
r.Header.Set("auth", fmt.Sprintf("%s:%s", addr, common.Bytes2Hex(sig)))
// Auth handler successfully only parses through the max body limit
handler.ServeHTTP(rr, r)
require.Equal(t, http.StatusOK, rr.Code, rr.Body)
// The next handler has the full request body present
require.Len(t, body, len(authBody)+len(excess))
}

49
op-txproxy/cli.go Normal file
View File

@ -0,0 +1,49 @@
package op_txproxy
import (
opservice "github.com/ethereum-optimism/optimism/op-service"
"github.com/urfave/cli/v2"
)
const (
SendRawTransactionConditionalEnabledFlagName = "sendRawTxConditional.enabled"
SendRawTransactionConditionalBackendFlagName = "sendRawTxConditional.backend"
SendRawTransactionConditionalRateLimitFlagName = "sendRawTxConditional.ratelimit"
)
type CLIConfig struct {
SendRawTransactionConditionalEnabled bool
SendRawTransactionConditionalBackend string
SendRawTransactionConditionalRateLimit uint64
}
func CLIFlags(envPrefix string) []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: SendRawTransactionConditionalEnabledFlagName,
Usage: "Decider if eth_sendRawTransactionConditional requests should passthrough or be rejected",
Value: true,
EnvVars: opservice.PrefixEnvVar(envPrefix, "SENDRAWTXCONDITIONAL_ENABLED"),
},
&cli.StringSliceFlag{
Name: SendRawTransactionConditionalBackendFlagName,
Usage: "block builder to broadcast conditional transactions",
EnvVars: opservice.PrefixEnvVar(envPrefix, "SENDRAWTXCONDITIONAL_BACKENDS"),
},
&cli.Uint64Flag{
Name: SendRawTransactionConditionalRateLimitFlagName,
Usage: "Maximum cost -- storage lookups -- allowed for conditional transactions in a given second",
Value: 5000,
EnvVars: opservice.PrefixEnvVar(envPrefix, "SENDRAWTXCONDITIONAL_RATELIMIT"),
},
}
}
func ReadCLIConfig(ctx *cli.Context) CLIConfig {
return CLIConfig{
SendRawTransactionConditionalEnabled: ctx.Bool(SendRawTransactionConditionalEnabledFlagName),
SendRawTransactionConditionalBackend: ctx.String(SendRawTransactionConditionalBackendFlagName),
SendRawTransactionConditionalRateLimit: ctx.Uint64(SendRawTransactionConditionalRateLimitFlagName),
}
}

68
op-txproxy/cmd/main.go Normal file
View File

@ -0,0 +1,68 @@
package main
import (
"context"
"fmt"
"os"
optxproxy "github.com/ethereum-optimism/infra/op-txproxy"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/opio"
"github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/urfave/cli/v2"
)
var (
GitCommit = ""
GitDate = ""
EnvVarPrefix = "OP_TXPROXY"
)
func main() {
oplog.SetupDefaults()
app := cli.NewApp()
app.Version = params.VersionWithCommit(GitCommit, GitDate)
app.Name = "op-txproxy"
app.Usage = "Optimism TxProxy Service"
app.Description = "Auxiliary service to supplement op-stack transaction pool management"
app.Action = cliapp.LifecycleCmd(TxProxyMain)
logFlags := oplog.CLIFlags(EnvVarPrefix)
rpcFlags := rpc.CLIFlags(EnvVarPrefix)
backendFlags := optxproxy.CLIFlags(EnvVarPrefix)
app.Flags = append(append(backendFlags, rpcFlags...), logFlags...)
ctx := opio.WithInterruptBlocker(context.Background())
if err := app.RunContext(ctx, os.Args); err != nil {
log.Crit("Application Failed", "err", err)
}
}
func TxProxyMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) {
log := oplog.NewLogger(oplog.AppOut(ctx), oplog.ReadCLIConfig(ctx))
m := metrics.With(metrics.NewRegistry())
cfg := optxproxy.ReadCLIConfig(ctx)
txproxy, err := optxproxy.NewTxProxy(ctx.Context, log, m, &cfg)
if err != nil {
return nil, fmt.Errorf("unable to start superchain backend: %w", err)
}
rpcConfig := rpc.ReadCLIConfig(ctx)
rpcOpts := []rpc.ServerOption{
rpc.WithAPIs(txproxy.GetAPIs()),
rpc.WithLogger(log),
rpc.WithMiddleware(optxproxy.AuthMiddleware(optxproxy.DefaultAuthHeaderKey)),
}
rpcServer := rpc.NewServer(rpcConfig.ListenAddr, rpcConfig.ListenPort, ctx.App.Version, rpcOpts...)
return rpc.NewService(log, rpcServer), nil
}

View File

@ -0,0 +1,140 @@
package op_txproxy
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/predeploys"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/time/rate"
)
var (
// errs
rateLimitErr = &rpc.JsonError{Message: "rate limited", Code: params.TransactionConditionalCostExceededMaxErrCode}
endpointDisabledErr = &rpc.JsonError{Message: "endpoint disabled", Code: params.TransactionConditionalRejectedErrCode}
missingAuthenticationErr = &rpc.JsonError{Message: "missing authentication", Code: params.TransactionConditionalRejectedErrCode}
entrypointSupportErr = &rpc.JsonError{Message: "only 4337 Entrypoint contract support", Code: params.TransactionConditionalRejectedErrCode}
)
type ConditionalTxService struct {
log log.Logger
cfg *CLIConfig
limiter *rate.Limiter
backend client.RPC
entrypointAddresses map[common.Address]bool
costSummary prometheus.Summary
requests prometheus.Counter
failures *prometheus.CounterVec
}
func NewConditionalTxService(ctx context.Context, log log.Logger, m metrics.Factory, cfg *CLIConfig) (*ConditionalTxService, error) {
rpc, err := client.NewRPC(ctx, log, cfg.SendRawTransactionConditionalBackend)
if err != nil {
return nil, fmt.Errorf("failed to dial backend %s: %w", cfg.SendRawTransactionConditionalBackend, err)
}
rpcMetrics := metrics.MakeRPCClientMetrics("backend", m)
backend := client.NewInstrumentedRPC(rpc, &rpcMetrics)
limiter := rate.NewLimiter(rate.Limit(cfg.SendRawTransactionConditionalRateLimit), params.TransactionConditionalMaxCost)
entrypointAddresses := map[common.Address]bool{predeploys.EntryPoint_v060Addr: true, predeploys.EntryPoint_v070Addr: true}
return &ConditionalTxService{
log: log,
cfg: cfg,
limiter: limiter,
backend: backend,
entrypointAddresses: entrypointAddresses,
costSummary: m.NewSummary(prometheus.SummaryOpts{
Namespace: MetricsNameSpace,
Name: "txconditional_cost",
Help: "summary of cost observed by *accepted* conditional txs",
}),
requests: m.NewCounter(prometheus.CounterOpts{
Namespace: MetricsNameSpace,
Name: "txconditional_requests",
Help: "number of conditional transaction requests",
}),
failures: m.NewCounterVec(prometheus.CounterOpts{
Namespace: MetricsNameSpace,
Name: "txconditional_failures",
Help: "number of conditional transaction failures",
}, []string{"err"}),
}, nil
}
func (s *ConditionalTxService) SendRawTransactionConditional(ctx context.Context, txBytes hexutil.Bytes, cond types.TransactionConditional) (common.Hash, error) {
s.requests.Inc()
if !s.cfg.SendRawTransactionConditionalEnabled {
s.failures.WithLabelValues("disabled").Inc()
return common.Hash{}, endpointDisabledErr
}
// Ensure the request is authenticated
authInfo := AuthFromContext(ctx)
if authInfo == nil {
s.failures.WithLabelValues("missing auth").Inc()
return common.Hash{}, missingAuthenticationErr
}
// Handle the request. For now, we do nothing with the authenticated signer
hash, err := s.sendCondTx(ctx, authInfo.Caller, txBytes, &cond)
if err != nil {
s.failures.WithLabelValues(err.Error()).Inc()
s.log.Error("failed transaction conditional", "caller", authInfo.Caller.String(), "hash", hash.String(), "err", err)
return common.Hash{}, err
}
return hash, err
}
func (s *ConditionalTxService) sendCondTx(ctx context.Context, caller common.Address, txBytes hexutil.Bytes, cond *types.TransactionConditional) (common.Hash, error) {
tx := new(types.Transaction)
if err := tx.UnmarshalBinary(txBytes); err != nil {
return common.Hash{}, fmt.Errorf("failed to unmarshal tx: %w", err)
}
txHash, cost := tx.Hash(), cond.Cost()
// external checks (tx target, conditional cost & validation)
if tx.To() == nil || !s.entrypointAddresses[*tx.To()] {
return txHash, entrypointSupportErr
}
if err := cond.Validate(); err != nil {
return txHash, &rpc.JsonError{
Message: fmt.Sprintf("failed conditional validation: %s", err),
Code: params.TransactionConditionalRejectedErrCode,
}
}
if cost > params.TransactionConditionalMaxCost {
return txHash, &rpc.JsonError{
Message: fmt.Sprintf("conditional cost, %d, exceeded max: %d", cost, params.TransactionConditionalMaxCost),
Code: params.TransactionConditionalCostExceededMaxErrCode,
}
}
// enforce rate limit on the cost to be observed
if err := s.limiter.WaitN(ctx, cost); err != nil {
return txHash, rateLimitErr
}
s.costSummary.Observe(float64(cost))
s.log.Info("broadcasting conditional transaction", "caller", caller.String(), "hash", txHash.String())
return txHash, s.backend.CallContext(ctx, nil, "eth_sendRawTransactionConditional", txBytes, cond)
}

View File

@ -0,0 +1,135 @@
package op_txproxy
import (
"context"
"math/big"
"net/http/httptest"
"testing"
"github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/predeploys"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
)
type testBackend struct{}
func (b *testBackend) SendRawTransactionConditional(ctx context.Context, txBytes hexutil.Bytes, cond types.TransactionConditional) (common.Hash, error) {
return common.Hash{}, nil
}
func setupSvc(t *testing.T) *ConditionalTxService {
// setup no-op backend
srv := rpc.NewServer()
t.Cleanup(func() { srv.Stop() })
require.NoError(t, srv.RegisterName("eth", new(testBackend)))
httpSrv := httptest.NewServer(srv)
t.Cleanup(func() { httpSrv.Close() })
log := testlog.Logger(t, log.LevelInfo)
cfg := &CLIConfig{
SendRawTransactionConditionalEnabled: true,
SendRawTransactionConditionalBackend: httpSrv.URL,
SendRawTransactionConditionalRateLimit: 10_000,
}
svc, err := NewConditionalTxService(context.Background(), log, metrics.With(metrics.NewRegistry()), cfg)
require.NoError(t, err)
return svc
}
func TestSendRawTransactionConditionalDisabled(t *testing.T) {
svc := setupSvc(t)
svc.cfg.SendRawTransactionConditionalEnabled = false
hash, err := svc.SendRawTransactionConditional(context.Background(), nil, types.TransactionConditional{})
require.Zero(t, hash)
require.Equal(t, endpointDisabledErr, err)
}
func TestSendRawTransactionConditionalMissingAuth(t *testing.T) {
svc := setupSvc(t)
hash, err := svc.SendRawTransactionConditional(context.Background(), nil, types.TransactionConditional{})
require.Zero(t, hash)
require.Equal(t, missingAuthenticationErr, err)
}
func TestSendRawTransactionConditionalInvalidTxTarget(t *testing.T) {
svc := setupSvc(t)
txBytes, err := rlp.EncodeToBytes(types.NewTransaction(0, common.Address{19: 1}, big.NewInt(0), 0, big.NewInt(0), nil))
require.NoError(t, err)
// setup auth
ctx := context.WithValue(context.Background(), authContextKey{}, &AuthContext{Caller: common.HexToAddress("0xa")})
hash, err := svc.SendRawTransactionConditional(ctx, txBytes, types.TransactionConditional{})
require.Zero(t, hash)
require.Equal(t, entrypointSupportErr, err)
}
func TestSendRawTransactionConditionals(t *testing.T) {
costExcessiveCond := types.TransactionConditional{KnownAccounts: make(types.KnownAccounts)}
for i := 0; i < (params.TransactionConditionalMaxCost + 1); i++ {
iBig := big.NewInt(int64(i))
root := common.BigToHash(iBig)
costExcessiveCond.KnownAccounts[common.BigToAddress(iBig)] = types.KnownAccount{StorageRoot: &root}
}
uint64Ptr := func(num uint64) *uint64 { return &num }
tests := []struct {
name string
cond types.TransactionConditional
mustFail bool
}{
{
name: "passes",
cond: types.TransactionConditional{BlockNumberMin: big.NewInt(1), BlockNumberMax: big.NewInt(2), TimestampMin: uint64Ptr(1), TimestampMax: uint64Ptr(2)},
mustFail: false,
},
{
name: "validation. block min greater than max",
cond: types.TransactionConditional{BlockNumberMin: big.NewInt(2), BlockNumberMax: big.NewInt(1)},
mustFail: true,
},
{
name: "validation. timestamp min greater than max",
cond: types.TransactionConditional{TimestampMin: uint64Ptr(2), TimestampMax: uint64Ptr(1)},
mustFail: true,
},
{
name: "excessive cost",
cond: costExcessiveCond,
mustFail: true,
},
}
svc := setupSvc(t)
txBytes, err := rlp.EncodeToBytes(types.NewTransaction(0, predeploys.EntryPoint_v060Addr, big.NewInt(0), 0, big.NewInt(0), nil))
require.NoError(t, err)
for _, test := range tests {
ctx := context.Background()
if !test.mustFail {
ctx = context.WithValue(ctx, authContextKey{}, &AuthContext{Caller: common.HexToAddress("0xa")})
}
_, err := svc.SendRawTransactionConditional(ctx, txBytes, test.cond)
if test.mustFail && err == nil {
t.Errorf("Test %s should fail", test.name)
}
if !test.mustFail && err != nil {
t.Errorf("Test %s should pass but got err: %v", test.name, err)
}
}
}

92
op-txproxy/go.mod Normal file
View File

@ -0,0 +1,92 @@
module github.com/ethereum-optimism/infra/op-txproxy
go 1.21
require (
github.com/ethereum-optimism/optimism v1.9.1-0.20240827163921-45e129c8ca4b
github.com/ethereum/go-ethereum v1.14.8
github.com/prometheus/client_golang v1.20.2
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.4
golang.org/x/time v0.6.0
)
require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/pebble v1.1.2 // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240828144951-4e6edcb7d36c // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-bexpr v0.1.11 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/pointerstructure v1.2.1 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/cors v1.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/supranational/blst v0.3.11 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101408.1-dev.1
//replace github.com/ethereum/go-ethereum => ../../op-geth

334
op-txproxy/go.sum Normal file
View File

@ -0,0 +1,334 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR8jAwb1Ie9GyehWjVcGh32Y2MznE=
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88=
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I=
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs=
github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/ethereum-optimism/op-geth v1.101408.1-dev.1 h1:cFJevJXLnTMRLeX7vP4ykbFNnk6sZ6WncEzg3nkz2tM=
github.com/ethereum-optimism/op-geth v1.101408.1-dev.1/go.mod h1:boCyfYcCK/lDcL1JA5daLc2qgvULU1zKcVtUJ605eGc=
github.com/ethereum-optimism/optimism v1.9.1-0.20240827163921-45e129c8ca4b h1:w7aA6V7bDr2Vbe8p4llG0/FkF7nsfASb3wMq63/qaz0=
github.com/ethereum-optimism/optimism v1.9.1-0.20240827163921-45e129c8ca4b/go.mod h1:pn4QCTrtpBnYySBLTPwJz+985qbTday98bnLCkx76K4=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240828144951-4e6edcb7d36c h1:wI6W6CimquWKoD6eZ0GhULXmiZynynzLBCPnsFKt+Y0=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240828144951-4e6edcb7d36c/go.mod h1:XaVXL9jg8BcyOeugECgIUGa9Y3DjYJj71RHmb5qon6M=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 h1:KrE8I4reeVvf7C1tm8elRjj4BdscTYzz/WAbYyf/JI4=
github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0/go.mod h1:D9AJLVXSyZQXJQVk8oh1EwjISE+sJTn2duYIZC0dy3w=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays=
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 h1:Ep/joEub9YwcjRY6ND3+Y/w0ncE540RtGatVhtZL0/Q=
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.11 h1:6DqdA/KBjurGby9yTY0bmkathya0lfwF2SeuubCI7dY=
github.com/hashicorp/go-bexpr v0.1.11/go.mod h1:f03lAo0duBlDIUMGCuad8oLcgejw4m7U+N8T+6Kz1AE=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=
github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4=
github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

32
op-txproxy/txproxy.go Normal file
View File

@ -0,0 +1,32 @@
package op_txproxy
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum/go-ethereum/log"
gethrpc "github.com/ethereum/go-ethereum/rpc"
)
var (
MetricsNameSpace = "op_txproxy"
)
type TxProxy struct {
conditionalTxService *ConditionalTxService
}
func NewTxProxy(ctx context.Context, log log.Logger, m metrics.Factory, cfg *CLIConfig) (*TxProxy, error) {
conditionalTxService, err := NewConditionalTxService(ctx, log, m, cfg)
if err != nil {
return nil, fmt.Errorf("failed to create conditional tx service: %w", err)
}
return &TxProxy{conditionalTxService}, nil
}
func (txp *TxProxy) GetAPIs() []gethrpc.API {
return []gethrpc.API{{Namespace: "eth", Service: txp.conditionalTxService}}
}

View File

@ -11,6 +11,7 @@ import semver
# Minimum version numbers for packages migrating from legacy versioning. # Minimum version numbers for packages migrating from legacy versioning.
MIN_VERSIONS = { MIN_VERSIONS = {
'proxyd': '4.6.1', 'proxyd': '4.6.1',
'op-signer': '0.0.1',
} }
VALID_BUMPS = ('major', 'minor', 'patch', 'prerelease', 'finalize-prerelease') VALID_BUMPS = ('major', 'minor', 'patch', 'prerelease', 'finalize-prerelease')

View File

@ -6,7 +6,9 @@ import semver
SERVICES = [ SERVICES = [
'proxyd', 'proxyd',
'op-ufm', 'op-ufm',
'op-signer',
'op-conductor-mon', 'op-conductor-mon',
'op-txproxy',
] ]
VERSION_PATTERN = '^{service}/v\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?$' VERSION_PATTERN = '^{service}/v\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?$'
GIT_TAG_COMMAND = 'git tag -a {tag} -m "{message}"' GIT_TAG_COMMAND = 'git tag -a {tag} -m "{message}"'

View File

@ -17,7 +17,7 @@ import (
"sync" "sync"
"time" "time"
sw "github.com/ethereum-optimism/optimism/proxyd/pkg/avg-sliding-window" sw "github.com/ethereum-optimism/infra/proxyd/pkg/avg-sliding-window"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
@ -56,7 +56,7 @@ var (
} }
ErrNoBackends = &RPCErr{ ErrNoBackends = &RPCErr{
Code: JSONRPCErrorInternal - 11, Code: JSONRPCErrorInternal - 11,
Message: "no backends available for method", Message: "no backend is currently healthy to serve traffic",
HTTPErrorCode: 503, HTTPErrorCode: 503,
} }
ErrBackendOverCapacity = &RPCErr{ ErrBackendOverCapacity = &RPCErr{

View File

@ -45,13 +45,14 @@ func (c *cache) Put(ctx context.Context, key string, value string) error {
} }
type redisCache struct { type redisCache struct {
rdb *redis.Client redisClient *redis.Client
prefix string redisReadClient *redis.Client
ttl time.Duration prefix string
ttl time.Duration
} }
func newRedisCache(rdb *redis.Client, prefix string, ttl time.Duration) *redisCache { func newRedisCache(redisClient *redis.Client, redisReadClient *redis.Client, prefix string, ttl time.Duration) *redisCache {
return &redisCache{rdb, prefix, ttl} return &redisCache{redisClient, redisReadClient, prefix, ttl}
} }
func (c *redisCache) namespaced(key string) string { func (c *redisCache) namespaced(key string) string {
@ -63,7 +64,7 @@ func (c *redisCache) namespaced(key string) string {
func (c *redisCache) Get(ctx context.Context, key string) (string, error) { func (c *redisCache) Get(ctx context.Context, key string) (string, error) {
start := time.Now() start := time.Now()
val, err := c.rdb.Get(ctx, c.namespaced(key)).Result() val, err := c.redisReadClient.Get(ctx, c.namespaced(key)).Result()
redisCacheDurationSumm.WithLabelValues("GET").Observe(float64(time.Since(start).Milliseconds())) redisCacheDurationSumm.WithLabelValues("GET").Observe(float64(time.Since(start).Milliseconds()))
if err == redis.Nil { if err == redis.Nil {
@ -77,7 +78,7 @@ func (c *redisCache) Get(ctx context.Context, key string) (string, error) {
func (c *redisCache) Put(ctx context.Context, key string, value string) error { func (c *redisCache) Put(ctx context.Context, key string, value string) error {
start := time.Now() start := time.Now()
err := c.rdb.SetEx(ctx, c.namespaced(key), value, c.ttl).Err() err := c.redisClient.SetEx(ctx, c.namespaced(key), value, c.ttl).Err()
redisCacheDurationSumm.WithLabelValues("SETEX").Observe(float64(time.Since(start).Milliseconds())) redisCacheDurationSumm.WithLabelValues("SETEX").Observe(float64(time.Since(start).Milliseconds()))
if err != nil { if err != nil {

View File

@ -16,7 +16,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
) )
var ( var (

View File

@ -43,6 +43,7 @@ type CacheConfig struct {
type RedisConfig struct { type RedisConfig struct {
URL string `toml:"url"` URL string `toml:"url"`
Namespace string `toml:"namespace"` Namespace string `toml:"namespace"`
ReadURL string `toml:"read_url"`
} }
type MetricsConfig struct { type MetricsConfig struct {

View File

@ -1,4 +1,4 @@
module github.com/ethereum-optimism/optimism/proxyd module github.com/ethereum-optimism/infra/proxyd
go 1.21 go 1.21
@ -15,7 +15,7 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.17.0 github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.2.1 github.com/redis/go-redis/v9 v9.2.1
github.com/rs/cors v1.10.1 github.com/rs/cors v1.11.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/xaionaro-go/weightedshuffle v0.0.0-20211213010739-6a74fbc7d24a github.com/xaionaro-go/weightedshuffle v0.0.0-20211213010739-6a74fbc7d24a

View File

@ -179,8 +179,8 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -5,7 +5,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -66,8 +66,8 @@ func TestBatching(t *testing.T) {
NewRPCReq("2", "eth_chainId", nil), NewRPCReq("2", "eth_chainId", nil),
}, },
expectedRes: asArray( expectedRes: asArray(
`{"error":{"code":-32011,"message":"no backends available for method"},"id":1,"jsonrpc":"2.0"}`, `{"error":{"code":-32011,"message":"no backend is currently healthy to serve traffic"},"id":1,"jsonrpc":"2.0"}`,
`{"error":{"code":-32011,"message":"no backends available for method"},"id":2,"jsonrpc":"2.0"}`, `{"error":{"code":-32011,"message":"no backend is currently healthy to serve traffic"},"id":2,"jsonrpc":"2.0"}`,
), ),
maxUpstreamBatchSize: 10, maxUpstreamBatchSize: 10,
numExpectedForwards: 1, numExpectedForwards: 1,
@ -80,8 +80,8 @@ func TestBatching(t *testing.T) {
NewRPCReq("2", "eth_chainId", nil), NewRPCReq("2", "eth_chainId", nil),
}, },
expectedRes: asArray( expectedRes: asArray(
`{"error":{"code":-32011,"message":"no backends available for method"},"id":1,"jsonrpc":"2.0"}`, `{"error":{"code":-32011,"message":"no backend is currently healthy to serve traffic"},"id":1,"jsonrpc":"2.0"}`,
`{"error":{"code":-32011,"message":"no backends available for method"},"id":2,"jsonrpc":"2.0"}`, `{"error":{"code":-32011,"message":"no backend is currently healthy to serve traffic"},"id":2,"jsonrpc":"2.0"}`,
), ),
maxUpstreamBatchSize: 1, maxUpstreamBatchSize: 1,
numExpectedForwards: 2, numExpectedForwards: 2,

View File

@ -9,9 +9,9 @@ import (
"time" "time"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
sw "github.com/ethereum-optimism/optimism/proxyd/pkg/avg-sliding-window" sw "github.com/ethereum-optimism/infra/proxyd/pkg/avg-sliding-window"
ms "github.com/ethereum-optimism/optimism/proxyd/tools/mockserver/handler" ms "github.com/ethereum-optimism/infra/proxyd/tools/mockserver/handler"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/alicebob/miniredis" "github.com/alicebob/miniredis"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -264,6 +264,63 @@ func TestBatchCaching(t *testing.T) {
require.Equal(t, 1, countRequests(backend, "eth_call")) require.Equal(t, 1, countRequests(backend, "eth_call"))
} }
func TestCachingWithReadReplica(t *testing.T) {
primary, err := miniredis.Run()
require.NoError(t, err)
defer primary.Close()
replica, err := miniredis.Run()
require.NoError(t, err)
defer replica.Close()
hdlr := NewBatchRPCResponseRouter()
hdlr.SetRoute("eth_getBlockByHash", "999", "eth_getBlockByHash")
backend := NewMockBackend(hdlr)
defer backend.Close()
require.NoError(t, os.Setenv("GOOD_BACKEND_RPC_URL", backend.URL()))
require.NoError(t, os.Setenv("REDIS_URL", fmt.Sprintf("redis://%s", primary.Addr())))
require.NoError(t, os.Setenv("REDIS_READ_URL", fmt.Sprintf("redis://%s", replica.Addr())))
config := ReadConfig("caching_replica")
client := NewProxydClient("http://127.0.0.1:8545")
_, shutdown, err := proxyd.Start(config)
require.NoError(t, err)
defer shutdown()
// allow time for the block number fetcher to fire
time.Sleep(1500 * time.Millisecond)
params := []interface{}{"0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b", "false"}
response := "{\"jsonrpc\": \"2.0\", \"result\": \"eth_getBlockByHash\", \"id\": 999}"
resRaw, _, err := client.SendRPC("eth_getBlockByHash", params)
require.NoError(t, err)
// because the cache is not replicated to the replica, count request must be increased
resCache, _, err := client.SendRPC("eth_getBlockByHash", params)
require.NoError(t, err)
RequireEqualJSON(t, []byte(response), resCache)
RequireEqualJSON(t, resRaw, resCache)
require.Equal(t, 2, countRequests(backend, "eth_getBlockByHash"))
// replicate cache data
for _, key := range primary.Keys() {
value, err := primary.Get(key)
require.NoError(t, err)
err = replica.Set(key, value)
require.NoError(t, err)
}
// now cache hit. count request must be same
resCache, _, err = client.SendRPC("eth_getBlockByHash", params)
require.NoError(t, err)
RequireEqualJSON(t, []byte(response), resCache)
RequireEqualJSON(t, resRaw, resCache)
require.Equal(t, 2, countRequests(backend, "eth_getBlockByHash"))
}
func countRequests(backend *MockBackend, name string) int { func countRequests(backend *MockBackend, name string) int {
var count int var count int
for _, req := range backend.Requests() { for _, req := range backend.Requests() {

View File

@ -12,8 +12,8 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
ms "github.com/ethereum-optimism/optimism/proxyd/tools/mockserver/handler" ms "github.com/ethereum-optimism/infra/proxyd/tools/mockserver/handler"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -9,13 +9,13 @@ import (
"time" "time"
"github.com/alicebob/miniredis" "github.com/alicebob/miniredis"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const ( const (
goodResponse = `{"jsonrpc": "2.0", "result": "hello", "id": 999}` goodResponse = `{"jsonrpc": "2.0", "result": "hello", "id": 999}`
noBackendsResponse = `{"error":{"code":-32011,"message":"no backends available for method"},"id":999,"jsonrpc":"2.0"}` noBackendsResponse = `{"error":{"code":-32011,"message":"no backend is currently healthy to serve traffic"},"id":999,"jsonrpc":"2.0"}`
unexpectedResponse = `{"error":{"code":-32011,"message":"some error"},"id":999,"jsonrpc":"2.0"}` unexpectedResponse = `{"error":{"code":-32011,"message":"some error"},"id":999,"jsonrpc":"2.0"}`
) )
@ -110,7 +110,7 @@ func TestFailover(t *testing.T) {
})) }))
res, statusCode, _ := client.SendRPC("eth_chainId", nil) res, statusCode, _ := client.SendRPC("eth_chainId", nil)
require.Equal(t, 503, statusCode) require.Equal(t, 503, statusCode)
RequireEqualJSON(t, []byte(noBackendsResponse), res) // return no backend available since both failed RequireEqualJSON(t, []byte(noBackendsResponse), res) // return currently not healthy since both failed
require.Equal(t, 1, len(goodBackend.Requests())) require.Equal(t, 1, len(goodBackend.Requests()))
require.Equal(t, 1, len(badBackend.Requests())) // bad backend is still called require.Equal(t, 1, len(badBackend.Requests())) // bad backend is still called
goodBackend.Reset() goodBackend.Reset()

View File

@ -11,8 +11,8 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
ms "github.com/ethereum-optimism/optimism/proxyd/tools/mockserver/handler" ms "github.com/ethereum-optimism/infra/proxyd/tools/mockserver/handler"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -10,7 +10,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )

View File

@ -3,7 +3,6 @@ package integration_tests
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -12,8 +11,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
ms "github.com/ethereum-optimism/optimism/proxyd/tools/mockserver/handler" ms "github.com/ethereum-optimism/infra/proxyd/tools/mockserver/handler"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -58,7 +57,6 @@ func setupMulticall(t *testing.T) (map[string]nodeContext, *proxyd.BackendGroup,
// setup proxyd // setup proxyd
config := ReadConfig("multicall") config := ReadConfig("multicall")
fmt.Printf("[SetupMulticall] Using Timeout of %d \n", config.Server.TimeoutSeconds)
svr, shutdown, err := proxyd.Start(config) svr, shutdown, err := proxyd.Start(config)
require.NoError(t, err) require.NoError(t, err)
@ -93,6 +91,7 @@ func setupMulticall(t *testing.T) (map[string]nodeContext, *proxyd.BackendGroup,
handlers := []*ms.MockedHandler{&h1, &h2, &h3} handlers := []*ms.MockedHandler{&h1, &h2, &h3}
// Default Handler configurations
nodes["node1"].mockBackend.SetHandler(SingleResponseHandler(200, txAccepted)) nodes["node1"].mockBackend.SetHandler(SingleResponseHandler(200, txAccepted))
nodes["node2"].mockBackend.SetHandler(http.HandlerFunc(handlers[1].Handler)) nodes["node2"].mockBackend.SetHandler(http.HandlerFunc(handlers[1].Handler))
//Node 3 has no handler empty handler never respondes should always context timeout //Node 3 has no handler empty handler never respondes should always context timeout
@ -194,16 +193,30 @@ func TestMulticall(t *testing.T) {
defer nodes["node3"].mockBackend.Close() defer nodes["node3"].mockBackend.Close()
defer shutdown() defer shutdown()
nodes["node1"].mockBackend.SetHandler(SingleResponseHandlerWithSleep(200, txAccepted, 3*time.Second)) triggerBackend1 := make(chan struct{})
nodes["node2"].mockBackend.SetHandler(SingleResponseHandler(200, txAccepted)) triggerBackend2 := make(chan struct{})
triggerBackend3 := make(chan struct{})
nodes["node1"].mockBackend.SetHandler(TriggerResponseHandler(200, txAccepted, triggerBackend1))
nodes["node2"].mockBackend.SetHandler(TriggerResponseHandler(200, txAccepted, triggerBackend2))
nodes["node3"].mockBackend.SetHandler(TriggerResponseHandler(200, txAccepted, triggerBackend3))
localSvr := setServerBackend(svr, nodes) localSvr := setServerBackend(svr, nodes)
body := makeSendRawTransaction(txHex1) body := makeSendRawTransaction(txHex1)
req, _ := http.NewRequest("POST", "https://1.1.1.1:8080", bytes.NewReader(body)) req, _ := http.NewRequest("POST", "https://1.1.1.1:8080", bytes.NewReader(body))
req.Header.Set("X-Forwarded-For", "203.0.113.1") req.Header.Set("X-Forwarded-For", "203.0.113.1")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
var wg sync.WaitGroup
wg.Add(1)
go func() {
triggerBackend2 <- struct{}{}
time.Sleep(2 * time.Second)
triggerBackend1 <- struct{}{}
triggerBackend3 <- struct{}{}
wg.Done()
}()
localSvr.HandleRPC(rr, req) localSvr.HandleRPC(rr, req)
resp := rr.Result() resp := rr.Result()
@ -219,6 +232,7 @@ func TestMulticall(t *testing.T) {
require.Equal(t, resp.Header["X-Served-By"], []string{"node/node2"}) require.Equal(t, resp.Header["X-Served-By"], []string{"node/node2"})
require.False(t, rpcRes.IsError()) require.False(t, rpcRes.IsError())
wg.Wait()
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1"))
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2"))
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node3")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node3"))
@ -232,21 +246,13 @@ func TestMulticall(t *testing.T) {
defer shutdown() defer shutdown()
shutdownChan1 := make(chan struct{}) triggerBackend1 := make(chan struct{})
shutdownChan2 := make(chan struct{}) triggerBackend2 := make(chan struct{})
nodes["node1"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, nonceErrorResponse, shutdownChan1, 4*time.Second)) nodes["node1"].mockBackend.SetHandler(TriggerResponseHandler(200, nonceErrorResponse, triggerBackend1))
nodes["node2"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, nonceErrorResponse, shutdownChan2, 1*time.Second)) nodes["node2"].mockBackend.SetHandler(TriggerResponseHandler(200, nonceErrorResponse, triggerBackend2))
nodes["node3"].mockBackend.SetHandler(SingleResponseHandler(403, dummyRes)) nodes["node3"].mockBackend.SetHandler(SingleResponseHandler(403, dummyRes))
var wg sync.WaitGroup
wg.Add(1)
go func() {
shutdownChan2 <- struct{}{}
shutdownChan1 <- struct{}{}
wg.Done()
}()
localSvr := setServerBackend(svr, nodes) localSvr := setServerBackend(svr, nodes)
body := makeSendRawTransaction(txHex1) body := makeSendRawTransaction(txHex1)
@ -254,6 +260,15 @@ func TestMulticall(t *testing.T) {
req.Header.Set("X-Forwarded-For", "203.0.113.1") req.Header.Set("X-Forwarded-For", "203.0.113.1")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
var wg sync.WaitGroup
wg.Add(1)
go func() {
triggerBackend2 <- struct{}{}
time.Sleep(3 * time.Second)
triggerBackend1 <- struct{}{}
wg.Done()
}()
localSvr.HandleRPC(rr, req) localSvr.HandleRPC(rr, req)
resp := rr.Result() resp := rr.Result()
@ -281,8 +296,10 @@ func TestMulticall(t *testing.T) {
defer nodes["node3"].mockBackend.Close() defer nodes["node3"].mockBackend.Close()
defer shutdown() defer shutdown()
triggerBackend1 := make(chan struct{})
// We should ignore node2 first response cause 429, and return node 1 because 200 // We should ignore node2 first response cause 429, and return node 1 because 200
nodes["node1"].mockBackend.SetHandler(SingleResponseHandlerWithSleep(200, txAccepted, 3*time.Second)) nodes["node1"].mockBackend.SetHandler(TriggerResponseHandler(200, txAccepted, triggerBackend1))
nodes["node2"].mockBackend.SetHandler(SingleResponseHandler(429, txAccepted)) nodes["node2"].mockBackend.SetHandler(SingleResponseHandler(429, txAccepted))
localSvr := setServerBackend(svr, nodes) localSvr := setServerBackend(svr, nodes)
@ -292,6 +309,14 @@ func TestMulticall(t *testing.T) {
req.Header.Set("X-Forwarded-For", "203.0.113.1") req.Header.Set("X-Forwarded-For", "203.0.113.1")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(2 * time.Second)
triggerBackend1 <- struct{}{}
wg.Done()
}()
localSvr.HandleRPC(rr, req) localSvr.HandleRPC(rr, req)
resp := rr.Result() resp := rr.Result()
@ -305,6 +330,7 @@ func TestMulticall(t *testing.T) {
require.Equal(t, "2.0", rpcRes.JSONRPC) require.Equal(t, "2.0", rpcRes.JSONRPC)
require.Equal(t, resp.Header["X-Served-By"], []string{"node/node1"}) require.Equal(t, resp.Header["X-Served-By"], []string{"node/node1"})
wg.Wait()
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1"))
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2"))
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node3")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node3"))
@ -317,9 +343,9 @@ func TestMulticall(t *testing.T) {
defer nodes["node3"].mockBackend.Close() defer nodes["node3"].mockBackend.Close()
defer shutdown() defer shutdown()
shutdownChan := make(chan struct{}) triggerBackend := make(chan struct{})
nodes["node1"].mockBackend.SetHandler(SingleResponseHandler(200, dummyRes)) nodes["node1"].mockBackend.SetHandler(SingleResponseHandler(200, dummyRes))
nodes["node2"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, dummyRes, shutdownChan, 7*time.Second)) nodes["node2"].mockBackend.SetHandler(TriggerResponseHandler(200, dummyRes, triggerBackend))
localSvr := setServerBackend(svr, nodes) localSvr := setServerBackend(svr, nodes)
@ -329,8 +355,14 @@ func TestMulticall(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
localSvr.HandleRPC(rr, req) localSvr.HandleRPC(rr, req)
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(7 * time.Second)
triggerBackend <- struct{}{}
wg.Done()
}()
resp := rr.Result() resp := rr.Result()
shutdownChan <- struct{}{}
defer resp.Body.Close() defer resp.Body.Close()
require.NotNil(t, resp.Body) require.NotNil(t, resp.Body)
@ -342,6 +374,7 @@ func TestMulticall(t *testing.T) {
require.NoError(t, json.NewDecoder(resp.Body).Decode(rpcRes)) require.NoError(t, json.NewDecoder(resp.Body).Decode(rpcRes))
require.False(t, rpcRes.IsError()) require.False(t, rpcRes.IsError())
wg.Wait()
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1"))
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2"))
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node3")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node3"))
@ -355,10 +388,10 @@ func TestMulticall(t *testing.T) {
defer nodes["node3"].mockBackend.Close() defer nodes["node3"].mockBackend.Close()
defer shutdown() defer shutdown()
shutdownChan1 := make(chan struct{}) triggerBackend1 := make(chan struct{})
shutdownChan2 := make(chan struct{}) triggerBackend2 := make(chan struct{})
nodes["node1"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, dummyRes, shutdownChan1, 7*time.Second)) nodes["node1"].mockBackend.SetHandler(TriggerResponseHandler(200, dummyRes, triggerBackend1))
nodes["node2"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, dummyRes, shutdownChan2, 7*time.Second)) nodes["node2"].mockBackend.SetHandler(TriggerResponseHandler(200, dummyRes, triggerBackend2))
localSvr := setServerBackend(svr, nodes) localSvr := setServerBackend(svr, nodes)
@ -370,12 +403,12 @@ func TestMulticall(t *testing.T) {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
shutdownChan1 <- struct{}{} time.Sleep(7 * time.Second)
shutdownChan2 <- struct{}{} triggerBackend1 <- struct{}{}
triggerBackend2 <- struct{}{}
wg.Done() wg.Done()
}() }()
fmt.Println("sending request")
localSvr.HandleRPC(rr, req) localSvr.HandleRPC(rr, req)
resp := rr.Result() resp := rr.Result()
@ -388,7 +421,6 @@ func TestMulticall(t *testing.T) {
require.True(t, rpcRes.IsError()) require.True(t, rpcRes.IsError())
require.Equal(t, rpcRes.Error.Code, proxyd.ErrNoBackends.Code) require.Equal(t, rpcRes.Error.Code, proxyd.ErrNoBackends.Code)
// Wait for test response to complete before checking query count
wg.Wait() wg.Wait()
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node1"))
require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2")) require.Equal(t, 1, nodeBackendRequestCount(nodes, "node2"))
@ -403,24 +435,24 @@ func TestMulticall(t *testing.T) {
defer shutdown() defer shutdown()
for i := 1; i < 4; i++ { for i := 1; i < 4; i++ {
shutdownChan1 := make(chan struct{}) triggerBackend1 := make(chan struct{})
shutdownChan2 := make(chan struct{}) triggerBackend2 := make(chan struct{})
shutdownChan3 := make(chan struct{}) triggerBackend3 := make(chan struct{})
switch { switch {
case i == 1: case i == 1:
nodes["node1"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, txAccepted, shutdownChan1, 1*time.Second)) nodes["node1"].mockBackend.SetHandler(TriggerResponseHandler(200, txAccepted, triggerBackend1))
nodes["node2"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(429, dummyRes, shutdownChan2, 1*time.Second)) nodes["node2"].mockBackend.SetHandler(TriggerResponseHandler(429, dummyRes, triggerBackend2))
nodes["node3"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(503, dummyRes, shutdownChan3, 1*time.Second)) nodes["node3"].mockBackend.SetHandler(TriggerResponseHandler(503, dummyRes, triggerBackend3))
case i == 2: case i == 2:
nodes["node1"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(404, dummyRes, shutdownChan1, 1*time.Second)) nodes["node1"].mockBackend.SetHandler(TriggerResponseHandler(404, dummyRes, triggerBackend1))
nodes["node2"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, nonceErrorResponse, shutdownChan2, 1*time.Second)) nodes["node2"].mockBackend.SetHandler(TriggerResponseHandler(200, nonceErrorResponse, triggerBackend2))
nodes["node3"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(405, dummyRes, shutdownChan3, 1*time.Second)) nodes["node3"].mockBackend.SetHandler(TriggerResponseHandler(405, dummyRes, triggerBackend3))
case i == 3: case i == 3:
// Return the quickest response // Return the quickest response
nodes["node1"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(404, dummyRes, shutdownChan1, 1*time.Second)) nodes["node1"].mockBackend.SetHandler(TriggerResponseHandler(404, dummyRes, triggerBackend1))
nodes["node2"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(500, dummyRes, shutdownChan2, 1*time.Second)) nodes["node2"].mockBackend.SetHandler(TriggerResponseHandler(500, dummyRes, triggerBackend2))
nodes["node3"].mockBackend.SetHandler(SingleResponseHandlerWithSleepShutdown(200, nonceErrorResponse, shutdownChan3, 1*time.Second)) nodes["node3"].mockBackend.SetHandler(TriggerResponseHandler(200, nonceErrorResponse, triggerBackend3))
} }
localSvr := setServerBackend(svr, nodes) localSvr := setServerBackend(svr, nodes)
@ -433,9 +465,9 @@ func TestMulticall(t *testing.T) {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
shutdownChan1 <- struct{}{} triggerBackend1 <- struct{}{}
shutdownChan2 <- struct{}{} triggerBackend2 <- struct{}{}
shutdownChan3 <- struct{}{} triggerBackend3 <- struct{}{}
wg.Done() wg.Done()
}() }()
@ -475,25 +507,13 @@ func TestMulticall(t *testing.T) {
require.Equal(t, i, nodeBackendRequestCount(nodes, "node3")) require.Equal(t, i, nodeBackendRequestCount(nodes, "node3"))
} }
}) })
} }
func SingleResponseHandlerWithSleep(code int, response string, duration time.Duration) http.HandlerFunc { // TriggerResponseHandler uses a channel to control when a backend returns
// test cases can add an element to the triggerResponse channel to control the when a specific backend returns
func TriggerResponseHandler(code int, response string, triggerResponse chan struct{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("sleeping") <-triggerResponse
time.Sleep(duration)
fmt.Println("Shutting down Single Response Handler")
w.WriteHeader(code)
_, _ = w.Write([]byte(response))
}
}
func SingleResponseHandlerWithSleepShutdown(code int, response string, shutdownServer chan struct{}, duration time.Duration) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("sleeping")
time.Sleep(duration)
<-shutdownServer
fmt.Println("Shutting down Single Response Handler")
w.WriteHeader(code) w.WriteHeader(code)
_, _ = w.Write([]byte(response)) _, _ = w.Write([]byte(response))
} }

View File

@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -0,0 +1,37 @@
[server]
rpc_port = 8545
[backend]
response_timeout_seconds = 1
[redis]
url = "$REDIS_URL"
read_url = "$REDIS_READ_URL"
namespace = "proxyd"
[cache]
enabled = true
[backends]
[backends.good]
rpc_url = "$GOOD_BACKEND_RPC_URL"
ws_url = "$GOOD_BACKEND_RPC_URL"
[backend_groups]
[backend_groups.main]
backends = ["good"]
[rpc_method_mappings]
eth_chainId = "main"
net_version = "main"
eth_getBlockByNumber = "main"
eth_blockNumber = "main"
eth_call = "main"
eth_getBlockTransactionCountByHash = "main"
eth_getUncleCountByBlockHash = "main"
eth_getBlockByHash = "main"
eth_getTransactionByHash = "main"
eth_getTransactionByBlockHashAndIndex = "main"
eth_getUncleByBlockHashAndIndex = "main"
eth_getTransactionReceipt = "main"
debug_getRawReceipts = "main"

View File

@ -17,7 +17,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
) )
type ProxydHTTPClient struct { type ProxydHTTPClient struct {

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View File

@ -39,6 +39,7 @@ func Start(config *Config) (*Server, func(), error) {
} }
} }
// redis primary client
var redisClient *redis.Client var redisClient *redis.Client
if config.Redis.URL != "" { if config.Redis.URL != "" {
rURL, err := ReadFromEnvOrConfig(config.Redis.URL) rURL, err := ReadFromEnvOrConfig(config.Redis.URL)
@ -51,6 +52,23 @@ func Start(config *Config) (*Server, func(), error) {
} }
} }
// redis read replica client
// if read endpoint is not set, use primary endpoint
var redisReadClient = redisClient
if config.Redis.ReadURL != "" {
if redisClient == nil {
return nil, nil, errors.New("must specify a Redis primary URL. only read endpoint is set")
}
rURL, err := ReadFromEnvOrConfig(config.Redis.ReadURL)
if err != nil {
return nil, nil, err
}
redisReadClient, err = NewRedisClient(rURL)
if err != nil {
return nil, nil, err
}
}
if redisClient == nil && config.RateLimit.UseRedis { if redisClient == nil && config.RateLimit.UseRedis {
return nil, nil, errors.New("must specify a Redis URL if UseRedis is true in rate limit config") return nil, nil, errors.New("must specify a Redis URL if UseRedis is true in rate limit config")
} }
@ -276,7 +294,7 @@ func Start(config *Config) (*Server, func(), error) {
if config.Cache.TTL != 0 { if config.Cache.TTL != 0 {
ttl = time.Duration(config.Cache.TTL) ttl = time.Duration(config.Cache.TTL)
} }
cache = newRedisCache(redisClient, config.Redis.Namespace, ttl) cache = newRedisCache(redisClient, redisReadClient, config.Redis.Namespace, ttl)
} }
rpcCache = newRPCCache(newCacheWithCompression(cache)) rpcCache = newRPCCache(newCacheWithCompression(cache))
} }

View File

@ -8,7 +8,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/ethereum-optimism/optimism/proxyd" "github.com/ethereum-optimism/infra/proxyd"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors" "github.com/pkg/errors"

View File

@ -6,7 +6,7 @@ import (
"path" "path"
"strconv" "strconv"
"github.com/ethereum-optimism/optimism/proxyd/tools/mockserver/handler" "github.com/ethereum-optimism/infra/proxyd/tools/mockserver/handler"
) )
func main() { func main() {