Compare commits
16 Commits
9555e15352
...
ca4e4b24e0
Author | SHA1 | Date | |
---|---|---|---|
ca4e4b24e0 | |||
1ad517098a | |||
|
d8cec08bd9 | ||
|
ac9ae52d52 | ||
|
dff24e9fca | ||
|
88521bef29 | ||
|
75b02dff3d | ||
|
7f8095c9ad | ||
|
bfdc66fa28 | ||
|
3b420a5c41 | ||
|
c03568e408 | ||
|
88c767e316 | ||
|
ad47122943 | ||
|
9a36d674b5 | ||
|
1cacaab6b5 | ||
|
98e261e7f9 |
@ -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
|
||||||
|
@ -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
2
.github/CODEOWNERS
vendored
@ -1 +1,3 @@
|
|||||||
* @ethereum-optimism/infra-reviewers
|
* @ethereum-optimism/infra-reviewers
|
||||||
|
|
||||||
|
/op-txproxy @ethereum-optimism/devxpod
|
||||||
|
5
.github/workflows/tag-service.yml
vendored
5
.github/workflows/tag-service.yml
vendored
@ -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?
|
||||||
|
@ -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.
|
||||||
|
51
op-conductor-ops/README.md
Normal file
51
op-conductor-ops/README.md
Normal 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.
|
32
op-conductor-ops/config.py
Normal file
32
op-conductor-ops/config.py
Normal 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
|
62
op-conductor-ops/example.config.toml
Normal file
62
op-conductor-ops/example.config.toml
Normal 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
|
35
op-conductor-ops/network.py
Normal file
35
op-conductor-ops/network.py
Normal 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,
|
||||||
|
)
|
7
op-conductor-ops/op-conductor-ops
Executable file
7
op-conductor-ops/op-conductor-ops
Executable 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 "${@}"
|
372
op-conductor-ops/op-conductor-ops.py
Executable file
372
op-conductor-ops/op-conductor-ops.py
Executable 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
307
op-conductor-ops/poetry.lock
generated
Normal 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"
|
17
op-conductor-ops/pyproject.toml
Normal file
17
op-conductor-ops/pyproject.toml
Normal 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"
|
105
op-conductor-ops/sequencer.py
Normal file
105
op-conductor-ops/sequencer.py
Normal 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
26
op-conductor-ops/utils.py
Normal 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
37
op-signer/.air.toml
Normal 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
4
op-signer/.envrc
Normal 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
2
op-signer/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
bin/
|
||||||
|
tls/
|
1
op-signer/CHANGELOG.md
Normal file
1
op-signer/CHANGELOG.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @eth-optimism/signer
|
18
op-signer/Dockerfile
Normal file
18
op-signer/Dockerfile
Normal 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
37
op-signer/Makefile
Normal 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
14
op-signer/README.md
Normal 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
239
op-signer/app.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
91
op-signer/client/client.go
Normal file
91
op-signer/client/client.go
Normal 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
51
op-signer/cmd/main.go
Normal 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
85
op-signer/config.go
Normal 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
3
op-signer/config.yaml
Normal 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
51
op-signer/gen-local-tls.sh
Executable 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
115
op-signer/go.mod
Normal 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
496
op-signer/go.sum
Normal 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
44
op-signer/service/auth.go
Normal 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
|
||||||
|
}
|
63
op-signer/service/auth_test.go
Normal file
63
op-signer/service/auth_test.go
Normal 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)
|
||||||
|
}
|
62
op-signer/service/config.go
Normal file
62
op-signer/service/config.go
Normal 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)
|
||||||
|
}
|
11
op-signer/service/errors.go
Normal file
11
op-signer/service/errors.go
Normal 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 }
|
12
op-signer/service/metrics.go
Normal file
12
op-signer/service/metrics.go
Normal 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"},
|
||||||
|
)
|
||||||
|
)
|
275
op-signer/service/provider/cloudkms.go
Normal file
275
op-signer/service/provider/cloudkms.go
Normal 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
|
||||||
|
}
|
194
op-signer/service/provider/cloudkms_test.go
Normal file
194
op-signer/service/provider/cloudkms_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
77
op-signer/service/provider/mock_kms.go
Normal file
77
op-signer/service/provider/mock_kms.go
Normal 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...)
|
||||||
|
}
|
65
op-signer/service/provider/mock_provider.go
Normal file
65
op-signer/service/provider/mock_provider.go
Normal 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)
|
||||||
|
}
|
9
op-signer/service/provider/provider.go
Normal file
9
op-signer/service/provider/provider.go
Normal 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)
|
||||||
|
}
|
148
op-signer/service/service.go
Normal file
148
op-signer/service/service.go
Normal 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
|
||||||
|
}
|
182
op-signer/service/service_test.go
Normal file
182
op-signer/service/service_test.go
Normal 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
53
op-signer/test-rpc.json
Normal 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
1
op-txproxy/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
bin
|
18
op-txproxy/Dockerfile
Normal file
18
op-txproxy/Dockerfile
Normal 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
28
op-txproxy/Makefile
Normal 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
36
op-txproxy/README.md
Normal 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
101
op-txproxy/auth_handler.go
Normal 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
|
||||||
|
}
|
148
op-txproxy/auth_handler_test.go
Normal file
148
op-txproxy/auth_handler_test.go
Normal 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
49
op-txproxy/cli.go
Normal 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
68
op-txproxy/cmd/main.go
Normal 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
|
||||||
|
}
|
140
op-txproxy/conditional_txs.go
Normal file
140
op-txproxy/conditional_txs.go
Normal 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)
|
||||||
|
}
|
135
op-txproxy/conditional_txs_test.go
Normal file
135
op-txproxy/conditional_txs_test.go
Normal 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
92
op-txproxy/go.mod
Normal 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
334
op-txproxy/go.sum
Normal 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
32
op-txproxy/txproxy.go
Normal 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}}
|
||||||
|
}
|
@ -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')
|
||||||
|
@ -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}"'
|
||||||
|
@ -6,7 +6,7 @@ ARG GITVERSION=docker
|
|||||||
|
|
||||||
RUN apk add make jq git gcc musl-dev linux-headers
|
RUN apk add make jq git gcc musl-dev linux-headers
|
||||||
|
|
||||||
COPY ./proxyd /app
|
COPY . /app
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ FROM alpine:3.18
|
|||||||
|
|
||||||
RUN apk add bind-tools jq curl bash git redis
|
RUN apk add bind-tools jq curl bash git redis
|
||||||
|
|
||||||
COPY ./proxyd/entrypoint.sh /bin/entrypoint.sh
|
COPY ./entrypoint.sh /bin/entrypoint.sh
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add ca-certificates && \
|
apk add ca-certificates && \
|
||||||
@ -24,9 +24,11 @@ RUN apk update && \
|
|||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
VOLUME /etc/proxyd
|
# VOLUME /etc/proxyd
|
||||||
|
|
||||||
|
ADD ./docker.toml /proxyd.toml
|
||||||
|
|
||||||
COPY --from=builder /app/bin/proxyd /bin/proxyd
|
COPY --from=builder /app/bin/proxyd /bin/proxyd
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/entrypoint.sh"]
|
ENTRYPOINT ["/bin/entrypoint.sh"]
|
||||||
CMD ["/bin/proxyd", "/etc/proxyd/proxyd.toml"]
|
CMD ["/bin/proxyd", "/proxyd.toml"]
|
@ -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{
|
||||||
|
@ -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
|
||||||
|
redisReadClient *redis.Client
|
||||||
prefix string
|
prefix string
|
||||||
ttl time.Duration
|
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 {
|
||||||
|
@ -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 (
|
||||||
|
@ -13,12 +13,16 @@ import (
|
|||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
RPCHost string `toml:"rpc_host"`
|
RPCHost string `toml:"rpc_host"`
|
||||||
RPCPort int `toml:"rpc_port"`
|
RPCPort int `toml:"rpc_port"`
|
||||||
|
EnableWS bool `toml:"enable_ws"`
|
||||||
WSHost string `toml:"ws_host"`
|
WSHost string `toml:"ws_host"`
|
||||||
WSPort int `toml:"ws_port"`
|
WSPort int `toml:"ws_port"`
|
||||||
MaxBodySizeBytes int64 `toml:"max_body_size_bytes"`
|
MaxBodySizeBytes int64 `toml:"max_body_size_bytes"`
|
||||||
MaxConcurrentRPCs int64 `toml:"max_concurrent_rpcs"`
|
MaxConcurrentRPCs int64 `toml:"max_concurrent_rpcs"`
|
||||||
LogLevel string `toml:"log_level"`
|
LogLevel string `toml:"log_level"`
|
||||||
|
|
||||||
|
// Allow direct client connection without x_forwarded_for header for local tests
|
||||||
|
AllowDirect bool `toml:"allow_direct"`
|
||||||
|
|
||||||
// TimeoutSeconds specifies the maximum time spent serving an HTTP request. Note that isn't used for websocket connections
|
// TimeoutSeconds specifies the maximum time spent serving an HTTP request. Note that isn't used for websocket connections
|
||||||
TimeoutSeconds int `toml:"timeout_seconds"`
|
TimeoutSeconds int `toml:"timeout_seconds"`
|
||||||
|
|
||||||
@ -39,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 {
|
||||||
|
20
proxyd/docker-compose.yml
Normal file
20
proxyd/docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
redis:
|
||||||
|
container_name: redis
|
||||||
|
image: redis:6.2-alpine
|
||||||
|
restart: always
|
||||||
|
#networks:
|
||||||
|
# - tornado_net
|
||||||
|
command: ["redis-server"]
|
||||||
|
proxyd:
|
||||||
|
container_name: proxyd
|
||||||
|
image: proxyd
|
||||||
|
restart: always
|
||||||
|
#networks:
|
||||||
|
# - tornado_net
|
||||||
|
environment:
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
- RPC_URL=
|
||||||
|
- WS_URL=
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:8544:8544'
|
171
proxyd/docker.toml
Normal file
171
proxyd/docker.toml
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
ws_method_whitelist = [
|
||||||
|
"eth_subscribe",
|
||||||
|
"eth_unsubscribe",
|
||||||
|
"eth_blobBaseFee",
|
||||||
|
"eth_blockNumber",
|
||||||
|
"eth_call",
|
||||||
|
"eth_chainId",
|
||||||
|
"eth_estimateGas",
|
||||||
|
"eth_feeHistory",
|
||||||
|
"eth_gasPrice",
|
||||||
|
"eth_getAccount",
|
||||||
|
"eth_getBalance",
|
||||||
|
"eth_getBlockByHash",
|
||||||
|
"eth_getBlockByNumber",
|
||||||
|
"eth_getBlockReceipts",
|
||||||
|
"eth_getBlockTransactionCountByHash",
|
||||||
|
"eth_getBlockTransactionCountByNumber",
|
||||||
|
"eth_getCode",
|
||||||
|
"eth_getFilterChanges",
|
||||||
|
"eth_getFilterLogs",
|
||||||
|
"eth_getLogs",
|
||||||
|
"eth_getProof",
|
||||||
|
"eth_getStorageAt",
|
||||||
|
"eth_getTransactionByBlockHashAndIndex",
|
||||||
|
"eth_getTransactionByBlockNumberAndIndex",
|
||||||
|
"eth_getTransactionByHash",
|
||||||
|
"eth_getTransactionCount",
|
||||||
|
"eth_getTransactionReceipt",
|
||||||
|
"eth_getUncleCountByBlockHash",
|
||||||
|
"eth_getUncleCountByBlockNumber",
|
||||||
|
"eth_maxPriorityFeePerGas",
|
||||||
|
"eth_newBlockFilter",
|
||||||
|
"eth_newFilter",
|
||||||
|
"eth_newPendingTransactionFilter",
|
||||||
|
"eth_syncing",
|
||||||
|
"eth_uninstallFilter",
|
||||||
|
"eth_sendRawTransaction",
|
||||||
|
"net_version",
|
||||||
|
"web3_clientVersion",
|
||||||
|
"web3_sha3",
|
||||||
|
# tracers for archive nodes
|
||||||
|
"trace_block",
|
||||||
|
"trace_call",
|
||||||
|
"trace_callMany",
|
||||||
|
"trace_filter",
|
||||||
|
"trace_rawTransaction",
|
||||||
|
"trace_replayBlockTransactions",
|
||||||
|
"trace_replayTransaction",
|
||||||
|
"trace_transaction",
|
||||||
|
"debug_getBadBlocks",
|
||||||
|
"debug_storageRangeAt",
|
||||||
|
"debug_getTrieFlushInterval",
|
||||||
|
"debug_traceBlock",
|
||||||
|
"debug_traceBlockByHash",
|
||||||
|
"debug_traceBlockByNumber",
|
||||||
|
"debug_traceCall",
|
||||||
|
"debug_traceTransaction",
|
||||||
|
]
|
||||||
|
ws_backend_group = "main"
|
||||||
|
|
||||||
|
[rpc_method_mappings]
|
||||||
|
eth_blobBaseFee = "main"
|
||||||
|
eth_blockNumber = "main"
|
||||||
|
eth_call = "main"
|
||||||
|
eth_chainId = "main"
|
||||||
|
eth_estimateGas = "main"
|
||||||
|
eth_feeHistory = "main"
|
||||||
|
eth_gasPrice = "main"
|
||||||
|
eth_getAccount = "main"
|
||||||
|
eth_getBalance = "main"
|
||||||
|
eth_getBlockByHash = "main"
|
||||||
|
eth_getBlockByNumber = "main"
|
||||||
|
eth_getBlockReceipts = "main"
|
||||||
|
eth_getBlockTransactionCountByHash = "main"
|
||||||
|
eth_getBlockTransactionCountByNumber = "main"
|
||||||
|
eth_getCode = "main"
|
||||||
|
eth_getFilterChanges = "main"
|
||||||
|
eth_getFilterLogs = "main"
|
||||||
|
eth_getLogs = "main"
|
||||||
|
eth_getProof = "main"
|
||||||
|
eth_getStorageAt = "main"
|
||||||
|
eth_getTransactionByBlockHashAndIndex = "main"
|
||||||
|
eth_getTransactionByBlockNumberAndIndex = "main"
|
||||||
|
eth_getTransactionByHash = "main"
|
||||||
|
eth_getTransactionCount = "main"
|
||||||
|
eth_getTransactionReceipt = "main"
|
||||||
|
eth_getUncleCountByBlockHash = "main"
|
||||||
|
eth_getUncleCountByBlockNumber = "main"
|
||||||
|
eth_maxPriorityFeePerGas = "main"
|
||||||
|
eth_newBlockFilter = "main"
|
||||||
|
eth_newFilter = "main"
|
||||||
|
eth_newPendingTransactionFilter = "main"
|
||||||
|
eth_syncing = "main"
|
||||||
|
eth_uninstallFilter = "main"
|
||||||
|
eth_sendRawTransaction = "main"
|
||||||
|
net_version = "main"
|
||||||
|
web3_clientVersion = "main"
|
||||||
|
web3_sha3 = "main"
|
||||||
|
trace_block = "main"
|
||||||
|
trace_call = "main"
|
||||||
|
trace_callMany = "main"
|
||||||
|
trace_filter = "main"
|
||||||
|
trace_rawTransaction = "main"
|
||||||
|
trace_replayBlockTransactions = "main"
|
||||||
|
trace_replayTransaction = "main"
|
||||||
|
trace_transaction = "main"
|
||||||
|
debug_getBadBlocks = "main"
|
||||||
|
debug_storageRangeAt = "main"
|
||||||
|
debug_getTrieFlushInterval = "main"
|
||||||
|
debug_traceBlock = "main"
|
||||||
|
debug_traceBlockByHash = "main"
|
||||||
|
debug_traceBlockByNumber = "main"
|
||||||
|
debug_traceCall = "main"
|
||||||
|
debug_traceTransaction = "main"
|
||||||
|
|
||||||
|
[server]
|
||||||
|
rpc_host = "0.0.0.0"
|
||||||
|
rpc_port = 8544
|
||||||
|
enable_ws = true
|
||||||
|
max_body_size_bytes = 10485760
|
||||||
|
max_concurrent_rpcs = 1000
|
||||||
|
log_level = "info"
|
||||||
|
allow_direct = true
|
||||||
|
|
||||||
|
[cache]
|
||||||
|
enabled = true
|
||||||
|
ttl = "14s"
|
||||||
|
|
||||||
|
[redis]
|
||||||
|
url = "$REDIS_URL"
|
||||||
|
|
||||||
|
[metrics]
|
||||||
|
enabled = true
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 9761
|
||||||
|
|
||||||
|
[rate_limit]
|
||||||
|
use_redis = true
|
||||||
|
base_rate = 2000
|
||||||
|
base_interval = "60s"
|
||||||
|
|
||||||
|
[backend]
|
||||||
|
response_timeout_seconds = 120
|
||||||
|
max_response_size_bytes = 5242880
|
||||||
|
max_retries = 3
|
||||||
|
out_of_service_seconds = 600
|
||||||
|
max_latency_threshold = "30s"
|
||||||
|
max_degraded_latency_threshold = "10s"
|
||||||
|
max_error_rate_threshold = 0.3
|
||||||
|
|
||||||
|
[backends]
|
||||||
|
[backends.main]
|
||||||
|
rpc_url = "$RPC_URL"
|
||||||
|
ws_url = "$WS_URL"
|
||||||
|
max_rps = 100
|
||||||
|
max_ws_conns = 100
|
||||||
|
consensus_skip_peer_count = true
|
||||||
|
consensus_receipts_target = "eth_getBlockReceipts"
|
||||||
|
|
||||||
|
[backend_groups]
|
||||||
|
[backend_groups.main]
|
||||||
|
backends = ["main"]
|
||||||
|
consensus_aware = false
|
||||||
|
# consensus_aware = true
|
||||||
|
consensus_ban_period = "1m"
|
||||||
|
consensus_max_update_threshold = "20s"
|
||||||
|
consensus_max_block_lag = 16
|
||||||
|
# Maximum block range (for eth_getLogs method), no default
|
||||||
|
# consensus_max_block_range = 20000
|
||||||
|
# Minimum peer count, default 3
|
||||||
|
# consensus_min_peer_count = 4
|
@ -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
|
||||||
|
@ -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=
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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() {
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
37
proxyd/integration_tests/testdata/caching_replica.toml
vendored
Normal file
37
proxyd/integration_tests/testdata/caching_replica.toml
vendored
Normal 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"
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
@ -328,7 +346,7 @@ func Start(config *Config) (*Server, func(), error) {
|
|||||||
|
|
||||||
if config.Server.RPCPort != 0 {
|
if config.Server.RPCPort != 0 {
|
||||||
go func() {
|
go func() {
|
||||||
if err := srv.RPCListenAndServe(config.Server.RPCHost, config.Server.RPCPort); err != nil {
|
if err := srv.RPCListenAndServe(config.Server); err != nil {
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Info("RPC server shut down")
|
log.Info("RPC server shut down")
|
||||||
return
|
return
|
||||||
@ -348,7 +366,7 @@ func Start(config *Config) (*Server, func(), error) {
|
|||||||
log.Crit("error starting WS server", "err", err)
|
log.Crit("error starting WS server", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
} else {
|
} else if !config.Server.EnableWS {
|
||||||
log.Info("WS server not enabled (ws_port is set to 0)")
|
log.Info("WS server not enabled (ws_port is set to 0)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,9 @@ func RewriteRequest(rctx RewriteContext, req *RPCReq, res *RPCRes) (RewriteResul
|
|||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "eth_getLogs",
|
case "eth_getLogs",
|
||||||
"eth_newFilter":
|
"eth_newFilter":
|
||||||
return rewriteRange(rctx, req, res, 0)
|
// return rewriteRange(rctx, req, res, 0)
|
||||||
|
// Tornado: disable range check unti the UI is fixed
|
||||||
|
return RewriteNone, nil
|
||||||
case "debug_getRawReceipts", "consensus_getReceipts":
|
case "debug_getRawReceipts", "consensus_getReceipts":
|
||||||
return rewriteParam(rctx, req, res, 0, true, false)
|
return rewriteParam(rctx, req, res, 0, true, false)
|
||||||
case "eth_getBalance",
|
case "eth_getBalance",
|
||||||
|
@ -200,12 +200,23 @@ func NewServer(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) RPCListenAndServe(host string, port int) error {
|
func (s *Server) RPCListenAndServe(serverConfig ServerConfig) error {
|
||||||
|
host := serverConfig.RPCHost
|
||||||
|
port := serverConfig.RPCPort
|
||||||
|
enableWS := serverConfig.EnableWS
|
||||||
|
|
||||||
|
var handleRpc ReqHandle = s.GetRPCHandle(serverConfig)
|
||||||
|
|
||||||
s.srvMu.Lock()
|
s.srvMu.Lock()
|
||||||
hdlr := mux.NewRouter()
|
hdlr := mux.NewRouter()
|
||||||
hdlr.HandleFunc("/healthz", s.HandleHealthz).Methods("GET")
|
hdlr.HandleFunc("/healthz", s.HandleHealthz).Methods("GET")
|
||||||
hdlr.HandleFunc("/", s.HandleRPC).Methods("POST")
|
hdlr.HandleFunc("/", handleRpc).Methods("POST")
|
||||||
hdlr.HandleFunc("/{authorization}", s.HandleRPC).Methods("POST")
|
hdlr.HandleFunc("/{authorization}", handleRpc).Methods("POST")
|
||||||
|
if enableWS {
|
||||||
|
var handleWS ReqHandle = s.GetWSHandle(true)
|
||||||
|
hdlr.HandleFunc("/", handleWS)
|
||||||
|
hdlr.HandleFunc("/{authorization}", handleWS)
|
||||||
|
}
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
})
|
})
|
||||||
@ -215,15 +226,20 @@ func (s *Server) RPCListenAndServe(host string, port int) error {
|
|||||||
Addr: addr,
|
Addr: addr,
|
||||||
}
|
}
|
||||||
log.Info("starting HTTP server", "addr", addr)
|
log.Info("starting HTTP server", "addr", addr)
|
||||||
|
if enableWS {
|
||||||
|
log.Info("starting WS server", "addr", addr)
|
||||||
|
}
|
||||||
s.srvMu.Unlock()
|
s.srvMu.Unlock()
|
||||||
return s.rpcServer.ListenAndServe()
|
return s.rpcServer.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) WSListenAndServe(host string, port int) error {
|
func (s *Server) WSListenAndServe(host string, port int) error {
|
||||||
s.srvMu.Lock()
|
s.srvMu.Lock()
|
||||||
|
var handleWS ReqHandle = s.GetWSHandle(false)
|
||||||
|
|
||||||
hdlr := mux.NewRouter()
|
hdlr := mux.NewRouter()
|
||||||
hdlr.HandleFunc("/", s.HandleWS)
|
hdlr.HandleFunc("/", handleWS)
|
||||||
hdlr.HandleFunc("/{authorization}", s.HandleWS)
|
hdlr.HandleFunc("/{authorization}", handleWS)
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
})
|
})
|
||||||
@ -255,7 +271,15 @@ func (s *Server) HandleHealthz(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = w.Write([]byte("OK"))
|
_, _ = w.Write([]byte("OK"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
|
type ReqHandle func(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
func (s *Server) GetRPCHandle(serverConfig ServerConfig) ReqHandle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.HandleRPC(w, r, serverConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request, serverConfig ServerConfig) {
|
||||||
ctx := s.populateContext(w, r)
|
ctx := s.populateContext(w, r)
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return
|
return
|
||||||
@ -272,9 +296,14 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
|
|||||||
isUnlimitedUserAgent := s.isUnlimitedUserAgent(userAgent)
|
isUnlimitedUserAgent := s.isUnlimitedUserAgent(userAgent)
|
||||||
|
|
||||||
if xff == "" {
|
if xff == "" {
|
||||||
|
// Just use remote addr from socket when the request doesn't have x_forwarded_for header
|
||||||
|
if (serverConfig.AllowDirect) {
|
||||||
|
xff = r.RemoteAddr
|
||||||
|
} else {
|
||||||
writeRPCError(ctx, w, nil, ErrInvalidRequest("request does not include a remote IP"))
|
writeRPCError(ctx, w, nil, ErrInvalidRequest("request does not include a remote IP"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isLimited := func(method string) bool {
|
isLimited := func(method string) bool {
|
||||||
isGloballyLimitedMethod := s.isGlobalLimit(method)
|
isGloballyLimitedMethod := s.isGlobalLimit(method)
|
||||||
@ -354,7 +383,7 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
batchRes, batchContainsCached, servedBy, err := s.handleBatchRPC(ctx, reqs, isLimited, true)
|
batchRes, batchContainsCached, servedBy, err := s.handleBatchRPC(xff, r, ctx, reqs, isLimited, true)
|
||||||
if err == context.DeadlineExceeded {
|
if err == context.DeadlineExceeded {
|
||||||
writeRPCError(ctx, w, nil, ErrGatewayTimeout)
|
writeRPCError(ctx, w, nil, ErrGatewayTimeout)
|
||||||
return
|
return
|
||||||
@ -377,7 +406,7 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rawBody := json.RawMessage(body)
|
rawBody := json.RawMessage(body)
|
||||||
backendRes, cached, servedBy, err := s.handleBatchRPC(ctx, []json.RawMessage{rawBody}, isLimited, false)
|
backendRes, cached, servedBy, err := s.handleBatchRPC(xff, r, ctx, []json.RawMessage{rawBody}, isLimited, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) ||
|
if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) ||
|
||||||
errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) {
|
errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) {
|
||||||
@ -394,7 +423,7 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeRPCRes(ctx, w, backendRes[0])
|
writeRPCRes(ctx, w, backendRes[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isLimited limiterFunc, isBatch bool) ([]*RPCRes, bool, string, error) {
|
func (s *Server) handleBatchRPC(xff string, r *http.Request, ctx context.Context, reqs []json.RawMessage, isLimited limiterFunc, isBatch bool) ([]*RPCRes, bool, string, error) {
|
||||||
// A request set is transformed into groups of batches.
|
// A request set is transformed into groups of batches.
|
||||||
// Each batch group maps to a forwarded JSON-RPC batch request (subject to maxUpstreamBatchSize constraints)
|
// Each batch group maps to a forwarded JSON-RPC batch request (subject to maxUpstreamBatchSize constraints)
|
||||||
// A groupID is used to decouple Requests that have duplicate ID so they're not part of the same batch that's
|
// A groupID is used to decouple Requests that have duplicate ID so they're not part of the same batch that's
|
||||||
@ -406,6 +435,10 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL
|
|||||||
backendGroup string
|
backendGroup string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve info from header
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
userAgent := r.Header.Get("User-Agent")
|
||||||
|
|
||||||
responses := make([]*RPCRes, len(reqs))
|
responses := make([]*RPCRes, len(reqs))
|
||||||
batches := make(map[batchGroup][]batchElem)
|
batches := make(map[batchGroup][]batchElem)
|
||||||
ids := make(map[string]int, len(reqs))
|
ids := make(map[string]int, len(reqs))
|
||||||
@ -418,6 +451,15 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug(
|
||||||
|
"received RPC method",
|
||||||
|
"req_id", GetReqID(ctx),
|
||||||
|
"method", parsedReq.Method,
|
||||||
|
"user_agent", userAgent,
|
||||||
|
"origin", origin,
|
||||||
|
"remote_ip", xff,
|
||||||
|
)
|
||||||
|
|
||||||
// Simple health check
|
// Simple health check
|
||||||
if len(reqs) == 1 && parsedReq.Method == proxydHealthzMethod {
|
if len(reqs) == 1 && parsedReq.Method == proxydHealthzMethod {
|
||||||
res := &RPCRes{
|
res := &RPCRes{
|
||||||
@ -449,6 +491,9 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL
|
|||||||
"source", "rpc",
|
"source", "rpc",
|
||||||
"req_id", GetReqID(ctx),
|
"req_id", GetReqID(ctx),
|
||||||
"method", parsedReq.Method,
|
"method", parsedReq.Method,
|
||||||
|
"user_agent", userAgent,
|
||||||
|
"origin", origin,
|
||||||
|
"remote_ip", xff,
|
||||||
)
|
)
|
||||||
RecordRPCError(ctx, BackendProxyd, MethodUnknown, ErrMethodNotWhitelisted)
|
RecordRPCError(ctx, BackendProxyd, MethodUnknown, ErrMethodNotWhitelisted)
|
||||||
responses[i] = NewRPCErrorRes(parsedReq.ID, ErrMethodNotWhitelisted)
|
responses[i] = NewRPCErrorRes(parsedReq.ID, ErrMethodNotWhitelisted)
|
||||||
@ -475,6 +520,9 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL
|
|||||||
"source", "rpc",
|
"source", "rpc",
|
||||||
"req_id", GetReqID(ctx),
|
"req_id", GetReqID(ctx),
|
||||||
"method", parsedReq.Method,
|
"method", parsedReq.Method,
|
||||||
|
"user_agent", userAgent,
|
||||||
|
"origin", origin,
|
||||||
|
"remote_ip", xff,
|
||||||
)
|
)
|
||||||
RecordRPCError(ctx, BackendProxyd, parsedReq.Method, ErrOverRateLimit)
|
RecordRPCError(ctx, BackendProxyd, parsedReq.Method, ErrOverRateLimit)
|
||||||
responses[i] = NewRPCErrorRes(parsedReq.ID, ErrOverRateLimit)
|
responses[i] = NewRPCErrorRes(parsedReq.ID, ErrOverRateLimit)
|
||||||
@ -579,12 +627,31 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL
|
|||||||
return responses, cached, servedByString, nil
|
return responses, cached, servedByString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) GetWSHandle(fromRpc bool) ReqHandle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.HandleWS(w, r, fromRpc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request, fromRpc bool) {
|
||||||
ctx := s.populateContext(w, r)
|
ctx := s.populateContext(w, r)
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle upgrade header request
|
||||||
|
upgrade := false
|
||||||
|
for _, header := range r.Header["Upgrade"] {
|
||||||
|
if header == "websocket" {
|
||||||
|
upgrade = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter out non websocket requests
|
||||||
|
if fromRpc && !upgrade {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("received WS connection", "req_id", GetReqID(ctx))
|
log.Info("received WS connection", "req_id", GetReqID(ctx))
|
||||||
|
|
||||||
clientConn, err := s.upgrader.Upgrade(w, r, nil)
|
clientConn, err := s.upgrader.Upgrade(w, r, nil)
|
||||||
|
@ -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"
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user