add baseline pubsub test plan (#6)

* refactor baseline test out of private repo

* rm references to attackers

* fix default plan name & add widget to override

* add configs for local runners

* update runner notebook intro text

* fix build tags

* increase setup time in saved configs
This commit is contained in:
Yusef Napora 2020-05-15 14:09:41 -04:00 committed by GitHub
parent 8f4fb1bfca
commit f0762b1814
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 5738 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# ignore transient paths for pubsub tests
pubsub/venv/
pubsub/scripts/output/
pubsub/scripts/config-snapshot.json
pubsub/scripts/configs/snapshot.json
__pycache__/
.ipynb_checkpoints/
# Created by https://www.gitignore.io/api/go,visualstudiocode
# Edit at https://www.gitignore.io/?templates=go,visualstudiocode
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
### Go Patch ###
/vendor/
/Godeps/
### VisualStudioCode ###
.vscode/*
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
# End of https://www.gitignore.io/api/go,visualstudiocode

319
pubsub/README.md Normal file
View File

@ -0,0 +1,319 @@
# `Plan:` gossipsub performs at scale
This test plan evaluates the performance of gossipsub, the gossiping mesh routing protocol
implemented by [go-libp2p-pubsub](https://github.com/libp2p/go-libp2p-pubsub).
## Installation & Setup
We're using python to generate testground composition files, and we shell out to a few
external commands, so there's some environment setup to do.
### Requirements
#### Hardware
While no special hardware is needed to run the tests, running with a lot of test instances requires considerable CPU and
RAM, and will likely exceed the capabilities of a single machine.
A large workstation with many CPU cores can reasonably run a few hundred instances using the testground
`local:docker` runner, although the exact limit will require some trial and error. In early testing we were able to
run 500 containers on a 56 core Xeon W-3175X with 124 GiB of RAM, although it's possible we could run more
now that we've optimized things a bit.
It's useful to run with the `local:docker` or `local:exec` runners during test development, so long as you use
fairly small instance counts (~25 or so works fine on a 2018 13" MacBook Pro with 16 GB RAM).
When using the `local:docker` runner, it's a good idea to periodically garbage collect the docker images created by
testground using `docker system prune` to reclaim disk space.
To run larger tests like the ones in the [saved configurations](#saved-test-configurations), you'll need a
kubernetes cluster.
To create your own cluster, follow the directions in the [testground/infra repo](https://github.com/testground/infra)
to provision a cluster on AWS, and configure your tests to use the `cluster:k8s` test runner.
The testground daemon process can be running on your local machine, as long as it has access to the k8s cluster.
The machine running the daemon must have Docker installed, and the user account must have permission to use
docker and have the correct AWS credentials to connect to the cluster.
When running tests on k8s, the machine running the testground daemon doesn't need a ton of resources,
but ideally it should have a fast internet connection to push the Docker images to the cluster.
Running the analysis notebooks benefits from multiple cores and consumes quite a bit of RAM, especially on the first
run when it's converting data to pandas format. It's best to have at least 8 GB of RAM free when running the analysis
notebook for the first time.
Also note that closing the browser tab containing a running Jupyter notebook does not stop the python kernel and reclaim
the memory used. It's best to select `Close and Halt` from the Jupyter `File` menu when you're done with the analysis
notebook instead of just closing the tab.
#### Testground
You'll need to have the [testground](https://github.com/testground/testground) binary built and accessible
on your `$PATH`. Testground must be version 0.5.0 or newer.
After running `testground list` for the first time, you should have a `~/testground` directory. You can change this
to another location by setting the `TESTGROUND_HOME` environment variable.
#### Cloning this repo
The testground client will look for test plans in `$TESTGROUND_HOME/plans`, so this repo should be cloned or
symlinked into there:
```shell
cd ~/testground/plans # if this dir doesn't exist, run 'testground list' once first to create it
git clone git@github.com:libp2p/test-plans libp2p-plans
```
#### Python
We need python 3.7 or later, ideally in a virtual environment. If you have python3 installed, you can create
a virtual environment named `venv` in this repo and it will be ignored by git:
```shell
python3 -m venv venv
```
After creating the virtual environment, you need to "activate" it for each shell session:
```shell
# bash / zsh:
source ./venv/bin/activate
# fish:
source ./venv/bin/activate.fish
```
You'll also need to install the python packages used by the scripts:
```shell
pip install -r scripts/requirements.txt
```
#### External binaries
The run scripts rely on a few commands being present on the `PATH`:
- the `testground` binary
- `go`
## Running Tests
### Running using the Runner Jupyter notebook
With the python virtualenv active, run
```shell
jupyter notebook
```
This will start a Jupyter notebook server and open a browser to the Jupyter file navigator.
In the Jupyter UI, navigate to the `scripts` dir and open `Runner.ipynb`.
This will open the runner notebook, which lets you configure the test parameters using a
configuration UI.
You'll need to run all the cells to prepare the notebook UI using `Cell menu > Run All`. You can reset
the notebook state using the `Kernel Menu > Restart and Run All` command.
The cell at the bottom of the notebook has a "Run Test" button that will convert the configured parameters
to a composition file and start running the test. It will shell out to the `testground` client binary,
so if you get an error about a missing executable, make sure `testground` is on your `PATH` and restart
the Jupyter server.
At the end of a successful test, there will be a new `output/pubsub-test-$timestamp` directory (relative to
the `scripts` dir) containing the composition file, the full `test-output.tgz` file collected from testground,
and an `analysis` directory.
The `analysis` directory has relevant files that were extracted from the `test-output.tgz` archive, along with a
new Jupyter notebook, `Analysis.ipynb`. See below for more details about the analysis notebook.
If the test fails (`testground` returns a non-zero exit code), the runner script will move the `pubsub-test-$timestamp`
dir to `./output/failed`.
The "Test Execution" section of the config UI will let you override the output path, for example if you want
to give your test a meaningful name.
### Targeting a specific version of go-libp2p-pubsub
The default configuration is to test against the current `master` branch of go-libp2p-pubsub,
but you can change that in the `Pubsub` panel of the configuration UI. You can enter the name
of a branch or tag, or the full SHA-1 hash of a specific commit.
**Important:** if you target a version before [the Gossipsub v1.1 PR](https://github.com/libp2p/go-libp2p-pubsub/pull/273)
was merged, you must uncheck the "target hardening branch API" checkbox to avoid build failures due to
missing methods.
#### Saved test configurations
You can save configuration snapshots to JSON files and load them again using the buttons at the bottom
of the configuration panel. The snapshots contain the state of all the configuration widgets, so can
only be used with the Runner notebook, not the command line `run.py` script.
There are several saved configs in `scripts/configs` that we've been using to evaluate different scenarios.
There are subdirectories inside of `scripts/configs` corresponding to different `testground` Runners, and
there are a few configurations for each runner with various node counts. For example, `configs/local-exec/25-peers.json`
will create a composition for the test using the `exec:go` builder and `local:exec` runner, with 25 pubsub peers,
while `configs/local-docker/25-peers.json` will use the `docker:go` and `local:docker` runner.
The saved configs all expect to find the `testground` daemon on a non-standard port (8080 instead of 8042).
If you're not running the daemon on port 8080, you can change the endpoint in the `Testground` section of
the config UI, or tell the daemon to listen on 8080 by editing `~/testground/.env.toml`.
### Running using the cli scripts
Inside the `scripts` directory, the `run.py` script will generate a composition and run it by shelling out to
`testground`. If you just want it to generate the composition, you can skip the test run by passing the `--dry-run`
flag.
You can get the full usage by running `./run.py --help`.
To run a test with baseline parameters (as defined in `scripts/templates/baseline/params/_base.toml`), run:
```shell
./run.py
```
By default, this will create a directory called `./output/pubsub-test-$timestamp`, which will have a `composition.toml`
file inside, as well as a `template-params.toml` that contains the params used to generate the composition.
You can control the output location with the `-o` and `--name` flags, for example:
```shell
./run.py -o /tmp --name 'foo'
# creates directory at /tmp/pubsub-test-$timestamp-foo
```
Note that the params defined in `scripts/templates/baseline/params/_base.toml` have very low instance counts and
are likely useless for real-world evaluation of gossipsub.
You can override individual template parameters using the `-D` flag, for example, `./run.py -D T_RUN=5m`.
There's no exhaustive list of template parameters, so check the template at `scripts/templates/baseline/template.toml.j2`
to see what's defined.
Alternatively, you can create a new toml file containing the parameters you want to set, and it will override
any parameters defined in `scripts/templates/baseline/params/_base.toml`
By default, the `run.py` script will extract the test data from the collected test output archive and copy the
analysis notebook to the `analysis` subdirectory of the test output dir. If you want to skip this step,
you can pass the `--skip-analysis` flag.
## Analyzing Test Outputs
After running a test, there should be a directory full of test outputs, with an `analysis` dir containing
an `Analysis.ipynb` Jupyter notebook. If you're not already running the Jupyter server, start it with
`jupyter notebook`, and use the Jupyter UI to navigate to the analysis notebook and open it.
Running all the cells in the analysis notebook will convert the extracted test data to
[pandas](https://pandas.pydata.org/) `DataFrame`s. This conversion takes a minute or two depending on the
size of the test and your hardware, but the results are cached to disk, so future runs should be pretty fast.
Once everything is loaded, you'll see some charts and tables, and there will be a new `figures` directory inside the
`analysis` dir containing the charts in a few image formats. There's also a `figures.zip` with the same contents
for easier downloading / storage.
### Running the analysis notebook from the command line
If you just want to generate the charts and don't care about interacting with the notebook, you can execute
the analysis notebook using a cli script.
Change to the `scripts` directory, then run
```shell
./analyze.py run_notebook ./output/pubsub-test-$timestamp
```
This will copy the latest analysis notebook template into the `analysis` directory and execute the notebook, which
will generate the chart images.
This command is useful if you've made changes to the analysis notebook template and want to re-run it against a
bunch of existing test outputs. In that case, you can pass multiple paths to the `run_notebook` subcommand:
```shell
./analyze.py run_notebook ./output/pubsub-test-*
# will run the latest notebook against everything in `./output
```
## Storing & Fetching Test Outputs in S3
The `scripts/sync_outputs.py` script is a wrapper around the [rclone](https://rclone.org) command that
helps backup test outputs to an s3 bucket, or fetch a previously stored output directory to the local filesystem.
The AWS credentials are pulled from the environment - see [the AWS cli docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)
if you haven't already configured the aws cli to use your credentials.
The configured user must have permission to access the bucket used to sync.
`rclone` must be installed and on the `$PATH` to use the `sync_outputs.py` script.
By default, it uses the S3 bucket `gossipsub-test-outputs` in `eu-central-1`, but you can control this with the
`--bucket` and `--region` flags.
To backup all the test outputs in `./output`:
```shell
./sync_outputs.py store-all ./output
```
It will ignore the `failed` subdirectory automatically, but if you want to ignore more, you can pass in a flag:
```shell
./sync_outputs.py store-all ./output --ignore some-dir-you-dont-want-to-store
```
Alternatively, you can selectively store one or more test outputs with the `store` subcommand:
```shell
./sync_outputs.py store ./output/pubsub-test-20200409-152658 ./output/pubsub-test-20200409-152983 # etc...
```
You can also fetch test outputs from S3 to the local filesystem.
To fetch everything from the bucket into `./output`:
```shell
./sync_outputs.py fetch-all ./output
```
Or, to fetch one or more tests from the bucket instead of everything:
```shell
./sync_outputs.py fetch --dest=./output pubsub-test-20200409-152658
```
You can list all the top-level directories in the S3 bucket (so you know what to fetch) using the `list` command:
```shell
./sync_outputs.py list
```
## Code Overview
The test code all lives in the `test` directory.
`main.go` is the actual entry point, but it just calls into the "real" main function, `RunSimulation`, which is defined
in `run.go`.
`params.go` contains the parameter parsing code. The `parseParams` function will return a `testParams` struct with
all test parameters.
The set of params provided to each test instance depends on which composition group they're in. The composition
template we're using defines two groups: `publishers`, and `lurkers`. The lurkers and publishers have
identical params with the exception of the boolean `publisher` param, which controls whether they will publish messages
or just consume them.
After parsing the params, `RunSimulation` will prepare the libp2p `Host`s, do some network setup and then
call `runPubsubNode` to begin the test.
`discovery.go` contains a `SyncDiscovery` component that uses the testground sync service to broadcast information about
the test peers (e.g. addreses, whether they're honest, etc) with every other peer. It uses this information to connect
nodes to each other in various topologies.
The honest node implementation is in `node.go`, and there are also `node_v10.go` and `node_v11.go` files
that allow us to target the new gossipsub v1.1 API or the old v1.0 API by setting a build tag. If the v1.0 API
is used, the test will not produce any peer score information, since that was added in v1.1.
The `tracer.go` file implements the `pubsub.EventTracer` interface to capture pubsub events and produce test metrics.
Because the full tracer output is quite large (several GB for a few minutes of test execution with lots of nodes),
we aggregate the trace events at runtime and spit out a json file with aggregated metrics at the end of the test.
We also capture a filtered subset of the original traces, containing only Publish, Deliver, Graft, and Prune events.
At the end of the test, we run [tracestat](https://github.com/libp2p/go-libp2p-pubsub-tracer/blob/master/cmd/tracestat/main.go)
on the filtered traces to calculate the latency distribution and get a summary of publish and deliver counts.

View File

@ -0,0 +1,508 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Pubsub Test Analysis Notebook\n",
"\n",
"## Usage\n",
"\n",
"This notebook analyzes the output of a single pubsub test execution.\n",
"\n",
"You must run all the cells (`Cell > Run All` menu) to load the data and generate the charts.\n",
"\n",
"This may take quite some time and require considerable RAM on the first\n",
"run, but the results will be cached to `ANALYSIS_DIR/pandas` for future runs.\n",
"\n",
"<p/>\n",
"\n",
"<details>\n",
" <summary>Expand to show example data</summary>\n",
"\n",
"### `scores`\n",
"\n",
"The `scores` `DataFrame` contains peer score events, indexed by timestamp.\n",
"\n",
"**Example**:\n",
"\n",
"| timestamp | observer | peer | score |\n",
"|-------------------------------|------------------------------------------------------|------------------------------------------------------|-----------|\n",
"| 2020-03-31 16:46:22.675526100 | 12D3KooWM1Q8EazdTBaYidtmAnaEqjtAzCGkYypzjgUmDU3bNpAS | 12D3KooWGHEfmKenMpsGDu1xEuVw7itsEMMT6oBVbYx642627Etw | 0.0027 |\n",
"| 2020-03-31 16:46:22.675526100 | 12D3KooWM1Q8EazdTBaYidtmAnaEqjtAzCGkYypzjgUmDU3bNpAS | 12D3KooWN5fa46NhuVP9as8ANmZ54gAM5cPuhTuu1bMAY5wvgoPG | 16.5617 |\n",
"\n",
"\n",
"- `observer` is the peer assigning the score\n",
"- `peer` is the peer receiving the score\n",
"\n",
"### `metrics`\n",
"\n",
"The `metrics` `DataFrame` contains aggregated tracer metrics.\n",
"\n",
"**Example**:\n",
"\n",
"| | published | rejected | delivered | duplicates | droppedrpc | peersadded | peersremoved | topicsjoined | topicsleft | peer | sent_rpcs | sent_messages | sent_grafts | sent_prunes | sent_iwants | sent_ihaves | recv_rpcs | recv_messages | recv_grafts | recv_prunes | recv_iwants | recv_ihaves |\n",
"|----|-------------|------------|-------------|--------------|--------------|--------------|----------------|----------------|--------------|------------------------------------------------------|-------------|-----------------|---------------|---------------|---------------|---------------|-------------|-----------------|---------------|---------------|---------------|---------------|\n",
"| 0 | 0 | 0 | 721 | 0 | 0 | 2 | 1 | 1 | 0 | 12D3KooWM1Q8EazdTBaYidtmAnaEqjtAzCGkYypzjgUmDU3bNpAS | 722 | 721 | 0 | 0 | 0 | 0 | 727 | 721 | 2 | 0 | 0 | 0 |\n",
"| 1 | 0 | 0 | 721 | 0 | 0 | 2 | 1 | 1 | 0 | 12D3KooWN5fa46NhuVP9as8ANmZ54gAM5cPuhTuu1bMAY5wvgoPG | 724 | 721 | 1 | 0 | 0 | 0 | 726 | 721 | 1 | 0 | 0 | 0 |\n",
"\n",
" \n",
"</details>"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": [
"parameters"
]
},
"outputs": [],
"source": [
"# Parameters in this cell can be overriden using papermill\n",
"\n",
"# path to directory contaning output from the extract_test_outputs method in analyze.py\n",
"ANALYSIS_DIR=\".\"\n",
"\n",
"# dir to save figure images\n",
"FIGURE_OUT=\"./figures\"\n",
"\n",
"# path to zip file containing all figures\n",
"FIGURE_ZIP_OUT=\"./figures.zip\"\n",
"\n",
"# font sizes\n",
"SMALL_SIZE = 8\n",
"MEDIUM_SIZE = 10\n",
"BIGGER_SIZE = 18"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import toml\n",
"import ipywidgets as widgets\n",
"from pprint import pprint\n",
"import pathlib\n",
"import seaborn as sns\n",
"from durations import Duration\n",
"\n",
"import notebook_helper\n",
"from notebook_helper import no_scores_message, load_pandas, archive_figures, p25, p50, p75, p95, p99\n",
"\n",
"# load a helper to save figures to FIGURE_OUT\n",
"save_fig = notebook_helper.save_fig_fn(FIGURE_OUT)\n",
"\n",
"# render charts in a larger, zoomable style\n",
"%matplotlib notebook\n",
"\n",
"# turn off autosaving for the notebook\n",
"%autosave 0\n",
"\n",
"# prettify the colors\n",
"sns.set(color_codes=True)\n",
"\n",
"# helper to set font sizes for charts\n",
"def set_chart_fontsize(size):\n",
" plt.rc('font', size=size) # controls default text sizes\n",
" plt.rc('axes', titlesize=size) # fontsize of the axes title\n",
" plt.rc('axes', labelsize=size) # fontsize of the x and y labels\n",
" plt.rc('xtick', labelsize=size) # fontsize of the tick labels\n",
" plt.rc('ytick', labelsize=size) # fontsize of the tick labels\n",
" plt.rc('legend', fontsize=size) # legend fontsize\n",
" plt.rc('figure', titlesize=size) # fontsize of the figure title\n",
"\n",
" \n",
"# set chart fonts to BIGGER_SIZE by default\n",
"set_chart_fontsize(BIGGER_SIZE)\n",
"\n",
"# load data\n",
"print('loading test data from ' + ANALYSIS_DIR)\n",
"\n",
"tables = load_pandas(ANALYSIS_DIR)\n",
"scores = tables['scores']\n",
"metrics = tables['metrics']\n",
"cdf = tables['cdf']\n",
"pdf = tables['pdf']\n",
"peers = tables['peers']\n",
"\n",
"# resample score index into 5s windows for plotting later\n",
"if not scores.empty:\n",
" print('resampling peer scores')\n",
" resample_interval = '5s'\n",
" sampled = scores.resample(resample_interval)\n",
"\n",
"t_warm = peers['t_warm'].max()\n",
"t_run = peers['t_run'].max()\n",
"t_cool = peers['t_cool'].max()\n",
"t_complete = peers['t_complete'].max()\n",
"\n",
"params_panel, test_params = notebook_helper.test_params_panel(ANALYSIS_DIR)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Test Parameters\n",
"\n",
"The cell below shows the parameters that were used to create the composition file for this test run.\n",
"\n",
"You can access the parameter values from other cells via the `test_params` dict, e.g.:\n",
"\n",
"```python\n",
"from durations import Duration\n",
"warmup = Duration(test_params['T_WARM'])\n",
"print('warmup seconds: {}'.format(warmup.to_seconds()))\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"params_panel"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Aggregations"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Latency Distribution"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### CDF"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = notebook_helper.plot_latency_cdf(cdf)\n",
"save_fig(fig, 'latency-cdf')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### PDF"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = notebook_helper.plot_latency_pdf(pdf)\n",
"save_fig(fig, 'latency-pdf')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### PDF (above p99)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = notebook_helper.plot_latency_pdf_above_quantile(pdf, quantile=0.99)\n",
"save_fig(fig, 'latency-pdf-over-p99')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Tracestat summary\n",
"Only Publish and Deliver counts are accurate, the rest are filtered."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(notebook_helper.tracestat_summary(ANALYSIS_DIR))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Aggregated tracer metrics (per-peer)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"metrics[['published', 'delivered', 'rejected', 'duplicates', 'droppedrpc']].agg([np.min, np.max, np.median, np.mean]).rename(columns={'amax': 'max', 'amin': 'min', 'amedian': 'median', 'amean': 'mean'})\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### All peer scores, aggregated across the test runtime"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if not scores.empty:\n",
" scores['score'].agg({'min': np.min, 'max': np.max, 'median': np.median, 'mean': np.mean})\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Aggregated score values for peers with negative scores\n",
"\n",
"- `observer` is the peer assigning the score. \n",
"- `peer` is the peer receiving the score."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if not scores.empty:\n",
" neg = scores.where(scores['score'] < 0).groupby(['peer', 'observer'])\n",
" n = neg.agg({'score': [np.min, np.max, np.median, np.mean]})\n",
" n\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Show honest peers with negative scores, joined with tracer metrics"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if not scores.empty:\n",
" # select columns from metrics table\n",
" m = metrics[['peer', 'published', 'delivered', 'rejected']]\n",
"\n",
" pd.DataFrame(n).merge(m, on='peer').groupby('peer').head()\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### global min/max score over time"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"time_annotations = [\n",
" {'label': 'warmup complete', 'time': t_run},\n",
" {'label': 'cooldown begin', 'time': t_cool},\n",
"]\n",
"\n",
"def annotate_score_plot(plot, label, legend_anchor=None):\n",
" notebook_helper.annotate_score_plot(plot, label, legend_anchor=legend_anchor, time_annotations=time_annotations)\n",
"\n",
"if not scores.empty:\n",
" fig, ax = plt.subplots(2, figsize=(11, 8))\n",
" plt.subplots_adjust(hspace=0.5)\n",
" fig.suptitle(\"Min / Max Peer Scores\")\n",
" plot = sampled['score'].agg([np.min, np.max]).plot(ax=ax[0])\n",
" annotate_score_plot(plot, 'All Peers min/max', legend_anchor=(0, -0.2))\n",
" \n",
" # hide bottom plot so legend is visible\n",
" ax[1].set_visible(False)\n",
" \n",
" # write png\n",
" save_fig(fig, 'score-min-max')\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### global mean / median score over time"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"if not scores.empty: \n",
" # create 4 subplots - three for charts and one that we'll hide to make blank space\n",
" # for the legend\n",
" fig, ax = plt.subplots(2, figsize=(11, 8))\n",
" plt.subplots_adjust(hspace=0.5)\n",
" fig.suptitle('Mean / Median Peer Scores')\n",
" plot = sampled['score'].agg([np.mean, np.median]).plot(ax=ax[0])\n",
" annotate_score_plot(plot, 'All Peers mean/median', legend_anchor=(0, -0.2))\n",
"\n",
" # hide bottom plot so legend is visible\n",
" ax[1].set_visible(False)\n",
" \n",
" save_fig(fig, 'score-mean-median')\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### mean score distribution (all peers)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"if not scores.empty:\n",
" plot = scores[['peer', 'score']].groupby('peer').mean().plot.hist(bins=20)\n",
" fig = plot.get_figure()\n",
" fig.suptitle('Mean score distribution (all peers)')\n",
" save_fig(fig, 'score-global-mean-distribution')\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### score distributions (honest vs attacker)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [],
"source": [
"aggregations = [np.min, np.max, p25, p75, p95]\n",
"kwargs = {'kind': 'hist', 'subplots': True, 'sharex': True, 'sharey': True, 'figsize': (8, 10)}\n",
"\n",
"if not scores.empty:\n",
" scores_by_peer = scores.groupby('peer')\n",
" \n",
" plots = scores_by_peer['score'].agg(aggregations).plot(title='Score distributions (all peers)', **kwargs)\n",
" for p in plots:\n",
" p.set_ylabel('freq')\n",
" fig = plots[0].get_figure()\n",
" save_fig(fig, 'score-distributions-all-peers')\n",
"\n",
"else:\n",
" no_scores_message()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# zip all figure images into a bundle\n",
"archive_figures(FIGURE_OUT, FIGURE_ZIP_OUT)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
},
"pycharm": {
"stem_cell": {
"cell_type": "raw",
"metadata": {
"collapsed": false
},
"source": []
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}

179
pubsub/scripts/Runner.ipynb Normal file
View File

@ -0,0 +1,179 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Pubsub Test Runner Notebook\n",
"\n",
"This notebook helps you configure a pubsub test plan and run it on testground.\n",
"\n",
"**Important**: when you first run the notebook after checking out from git, you'll probably need to restart the Jupyter kernel and run all cells in the notebook. Use the `Kernel > Restart and Run All` Juypter menu command.\n",
"\n",
"## Configuring the Test\n",
"\n",
"Use the UI widgets to dial in a configuration for the test run.\n",
"\n",
"### Saving and loading the config\n",
"\n",
"You can save a snapshot of the current configuration by pressing the `Save Config` button below, which will dump the current value of the UI to a json file (`configs/snapshot.json` by default).\n",
"\n",
"Loading a snapshot will set the UI to the values from the snapshot file.\n",
"\n",
"This is useful if you need to restart the notebook kernel but don't want to lose your current config. You could also use it to save a baseline config that you want to make tweaks to in a future run.\n",
"\n",
"The default `config/snapshot.json` file\n",
"is ignored by git, so it can be used to quickly stash a config when reloading the notebook, etc. If you want to keep\n",
"the config and check it into the repo, just save it with a different name.\n",
"\n",
"**NOTE**: The `Test Execution` output directory param is ignored when loading saved snapshots, to avoid overwriting the output of a prior run.\n",
"\n",
"\n",
"## Running the test\n",
"\n",
"You'll need to be running the testground daemon somewhere accessible. By default, we'll attempt to use the daemon running at `localhost:8080`, but you can set the daemon endpoint param in the UI to target a different endpoint.\n",
"\n",
"Run the test by clicking the `Run Test` button below.\n",
"This will invoke testground and print the testground client output below the cell.\n",
"\n",
"You can halt the test by interrupting the Jupyter kernel, either with the square Stop button in the Jupyter toolbar or with the `Kernel > Interrupt` menu item.\n",
"\n",
"To re-run a test, you can re-run the Jupyter cell that contains the `Run Test` button, which will clear any existing printed output and re-create the run button, but won't change any of the configuration values. Note that this will overwrite any test outputs from a previous run, unless you change the output dir in the `Test Execution` config before running again.\n",
"\n",
"### Test outputs\n",
"\n",
"At the moment, test ouputs are stored only on the machine where the notebook is running. The output directory is configurable in the `Test Execution` config UI, and defaults to a timestamped subdirectory of `./output`.\n",
"\n",
"Inside that directory will be:\n",
"\n",
"- a generated `composition.toml` file that was sent to testground to execute\n",
"- a `template-params.toml` file that was used to parameterize the composition.\n",
"- a `test-output.tgz` (or `.zip`) file containing the collected testground output from all instances\n",
"- an `analysis` directory containing outputs from running the analysis script on the `test-output.tgz` archive\n",
"\n",
"The `anaylsis` directory will contain another Jupyter notebook called `Analysis.ipynb`. This contains interesting charts derived from the test output, and can be explored interactively. The path to the analysis notebook will be printed when the test completes successfully.\n",
"\n",
"\n",
"## Troubleshooting\n",
"\n",
"Expand the sections below if you're having issues running.\n",
"\n",
"<br/>\n",
"<details>\n",
" <summary>\n",
" <b>The UI Widgets aren't showing up</b>\n",
" </summary>\n",
"\n",
"If you don't see any UI widgets, select `Restart & Run All` from the `Kernel` menu. If you're viewing this notebook in JupyterLab, make sure to [install the JupyterLab widgets extension](https://ipywidgets.readthedocs.io/en/latest/user_install.html#installing-the-jupyterlab-extension).\n",
"</details>\n",
"\n",
"<br/>\n",
"\n",
"<details> \n",
" <summary>\n",
" <b>The test fails immediately</b>\n",
" </summary>\n",
" \n",
"#### can't reach the daemon\n",
"\n",
"If the output has a line like\n",
"\n",
"```\n",
"fatal error from daemon: Post http://localhost:8080/build: dial tcp [::1]:8080: connect: connection refused\n",
"```\n",
"\n",
"it means that the daemon isn't running, or we can't connect to it on the given `host:port`. Make sure we can reach the daemon, for example by running `./testground list` manually in a shell on the same machine that's running the notebook.\n",
"\n",
"\n",
"#### aws config not setup\n",
"\n",
"If you see something like:\n",
"\n",
"```\n",
"WARN\tengine build error: MissingRegion: could not find region configuration\t{\"ruid\": \"1255d41c\"}\n",
"```\n",
"\n",
"It probably means you're trying to run on kubernetes (which is the default), but your daemon isn't configured to run jobs on AWS. Consult the [infra docs](https://github.com/testground/infra) to make sure your cluster environment is\n",
"set up correctly.\n",
"\n",
"</details> \n",
"<br/>\n",
"\n",
"<details>\n",
" <summary>\n",
" <b>The test completes, but it prints errors about missing programs</b>\n",
" </summary>\n",
" \n",
"The analysis script expects `go` to be on the `PATH`. Make sure that `go` is accessible and restart the Jupyter server.\n",
" \n",
"</details>"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"init_cell": true
},
"outputs": [],
"source": [
"import ui\n",
"%autosave 0"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"init_cell": true,
"scrolled": false
},
"outputs": [],
"source": [
"config = ui.ConfigPanel()\n",
"config.ui()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"init_cell": true,
"scrolled": true
},
"outputs": [],
"source": [
"b = ui.RunButton(config)\n",
"b.wait()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

292
pubsub/scripts/analyze.py Executable file
View File

@ -0,0 +1,292 @@
#!/usr/bin/env python
import argparse
import os
import zipfile
import gzip
import tarfile
import contextlib
import tempfile
import json
import subprocess
import pathlib
import pandas as pd
from glob import glob
import multiprocessing as mp
import shutil
import re
import sys
ANALYSIS_NOTEBOOK_TEMPLATE = 'Analysis-Template.ipynb'
def mkdirp(dirpath):
pathlib.Path(dirpath).mkdir(parents=True, exist_ok=True)
def parse_args():
parser = argparse.ArgumentParser()
commands = parser.add_subparsers()
extract_cmd = commands.add_parser('extract', help='extract test outputs from testground output archive')
extract_cmd.add_argument('test_output_zip_path', nargs=1,
help='path to testground output zip or tgz file')
extract_cmd.add_argument('--output-dir', '-o', dest='output_dir', default=None,
help='path to write output files. default is to create a new dir based on zip filename')
extract_cmd.set_defaults(subcomment='extract')
run_notebook_cmd = commands.add_parser('run_notebook',
help='runs latest analysis notebook against extracted test data')
run_notebook_cmd.add_argument('test_result_dir', nargs='+',
help='directories to run against. must contain an "analysis" subdir with extracted test data')
run_notebook_cmd.set_defaults(subcommand='run_notebook')
return parser.parse_args()
def concat_files(names, outfile):
for name in names:
with open(name, 'rb') as f:
outfile.write(f.read())
# depending on which test runner was used, the collection archive may be either a zip (local docker & exec runner),
# or a tar.gz file (k8s). Unfortunately, the zipfile and tarfile modules are different species of waterfowl,
# so we duck typing doesn't help. So this method extracts whichever one we have to a temp directory and
# returns the path to the temp dir.
# use as a context manager, so the temp dir gets deleted when we're done:
# with open_archive(archive_path) as a:
# files = glob(a + '/**/tracer-output')
@contextlib.contextmanager
def open_archive(archive_path):
# zipfile and tarfile both have an extractall method, at least
if zipfile.is_zipfile(archive_path):
z = zipfile.ZipFile(archive_path)
else:
z = tarfile.open(archive_path, 'r:gz')
with tempfile.TemporaryDirectory(prefix='pubsub-tg-archive-') as d:
z.extractall(path=d)
yield d
# sugar around recursive glob search
def find_files(dirname, filename_glob):
path = '{}/**/{}'.format(dirname, filename_glob)
return glob(path, recursive=True)
PEER_INFO_PATTERN = re.compile(r'Host peer ID: ([0-9a-zA-Z]+), seq (\d+), node type: ([a-z]+), node type seq: (\d+), node index: (\d+) / (\d+)')
def extract_peer_info(run_out):
with open(run_out, 'rt') as f:
for line in f.readlines():
m = PEER_INFO_PATTERN.search(line)
if m:
pid = m.group(1)
seq = int(m.group(2))
node_type = m.group(3)
node_type_seq = int(m.group(4))
node_index = int(m.group(5))
node_index_bound = int(m.group(6))
return {'peer_id': pid,
'type': node_type,
'seq': seq,
'node_type_seq': node_type_seq,
'node_index': node_index,
'node_index_bound': node_index_bound}
print('warning: no peer info found in {}'.format(run_out))
return None
def extract_timing_info(run_out, node_type):
if node_type == 'honest':
times = dict(t_warm=0, t_connect=0, t_run=0, t_cool=0, t_complete=0)
else:
times = dict(t_connect=0)
with open(run_out, 'rt') as f:
for line in f.readlines():
try:
obj = json.loads(line)
except BaseException as err:
print("error parsing run output: ", err)
continue
if 'ts' not in obj or 'event' not in obj or obj['event'].get('type', '') != 'message':
continue
msg = obj['event']['message']
ts = obj['ts']
if re.match(r'connecting to peers.*', msg):
times['t_connect'] = ts
continue
# the rest of the times are only logged by honest peers
if node_type != 'honest':
continue
if re.match(r'Wait for .* warmup time', msg):
times['t_warm'] = ts
continue
if re.match(r'Wait for .* run time', msg):
times['t_run'] = ts
continue
if re.match(r'Run time complete, cooling down.*', msg):
times['t_cool'] = ts
continue
if msg == 'Cool down complete':
times['t_complete'] = ts
continue
for k, v in times.items():
if v == 0:
print('warning: unable to determine time value for {}'.format(k))
return times
def extract_peer_and_timing_info(run_out_files):
entries = []
for filename in run_out_files:
info = extract_peer_info(filename)
if info is None:
continue
times = extract_timing_info(filename, info.get('type', 'unknown'))
info.update(times)
entries.append(info)
return entries
def aggregate_output(output_zip_path, out_dir):
topology = dict()
with open_archive(output_zip_path) as archive:
tracefiles = find_files(archive, 'tracer-output*')
names = [f for f in tracefiles if 'full' in f]
if len(names) > 0:
with gzip.open(os.path.join(out_dir, 'full-trace.bin.gz'), 'wb') as gz:
concat_files(names, gz)
names = [f for f in tracefiles if 'filtered' in f]
if len(names) > 0:
with gzip.open(os.path.join(out_dir, 'filtered-trace.bin.gz'), 'wb') as gz:
concat_files(names, gz)
# copy aggregate metrics files
names = [f for f in tracefiles if 'aggregate' in f]
for name in names:
dest = os.path.join(out_dir, os.path.basename(name))
shutil.copyfile(name, dest)
# copy peer score files
names = find_files(archive, 'peer-scores*')
for name in names:
dest = os.path.join(out_dir, os.path.basename(name))
shutil.copyfile(name, dest)
# get peer id -> seq mapping & timing info from run.out files
names = find_files(archive, 'run.out')
info = extract_peer_and_timing_info(names)
dest = os.path.join(out_dir, 'peer-info.json')
with open(dest, 'wt') as f:
json.dump(info, f)
# Collect contents of all files of the form 'connections-honest-8-1'
names = find_files(archive, 'connections*')
for name in names:
with open(name, 'r') as infile:
name = os.path.basename(name)
_, node_type, node_type_seq, node_idx = name.split('.')[0].split('-')
conns = json.loads(infile.read())
topology[node_type + '-' + node_type_seq + '-' + node_idx] = conns or []
# Write out topology file
top_path = os.path.join(out_dir, 'topology.json')
with open(top_path, 'wt') as outfile:
outfile.write(json.dumps(topology))
def run_tracestat(tracer_output_dir):
full = os.path.join(tracer_output_dir, 'full-trace.bin.gz')
filtered = os.path.join(tracer_output_dir, 'filtered-trace.bin.gz')
if os.path.exists(full):
tracer_output = full
elif os.path.exists(filtered):
tracer_output = filtered
else:
print('no event tracer output found, skipping tracestat')
return
print('running tracestat on {}'.format(tracer_output))
try:
cmd = ['go', 'run', 'github.com/libp2p/go-libp2p-pubsub-tracer/cmd/tracestat', '-cdf', tracer_output]
p = subprocess.run(cmd, capture_output=True, text=True, check=True)
except BaseException as err:
print('error calling tracestat: ', err)
return
# split output into summary and latency CDF
[summary, cdf] = p.stdout.split('=== Propagation Delay CDF (ms) ===')
with open(os.path.join(tracer_output_dir, 'tracestat-summary.txt'), 'w', encoding='utf8') as f:
f.write(summary)
with open(os.path.join(tracer_output_dir, 'tracestat-cdf.txt'), 'w', encoding='utf8') as f:
f.write(cdf)
print(summary)
def extract_test_outputs(test_output_zip_path, output_dir=None, convert_to_pandas=False, prep_notebook=True):
if output_dir is None or output_dir == '':
output_dir = os.path.join(os.path.dirname(test_output_zip_path), 'analysis')
mkdirp(output_dir)
aggregate_output(test_output_zip_path, output_dir)
run_tracestat(output_dir)
if convert_to_pandas:
import notebook_helper
print('converting data to pandas format...')
notebook_helper.to_pandas(output_dir, os.path.join(output_dir, 'pandas'))
if prep_notebook:
prepare_analysis_notebook(analysis_dir=output_dir)
return output_dir
def prepare_analysis_notebook(analysis_dir):
notebook_out = os.path.join(analysis_dir, 'Analysis.ipynb')
shutil.copy(ANALYSIS_NOTEBOOK_TEMPLATE, notebook_out)
shutil.copy('./notebook_helper.py', os.path.join(analysis_dir, 'notebook_helper.py'))
print('saved analysis notebook to {}'.format(notebook_out))
def run_analysis_notebook(analysis_dir):
prepare_analysis_notebook(analysis_dir)
notebook_path = os.path.join(analysis_dir, 'Analysis.ipynb')
cmd = ['papermill', ANALYSIS_NOTEBOOK_TEMPLATE, notebook_path, '--cwd', analysis_dir]
try:
subprocess.run(cmd, check=True)
except BaseException as err:
print('error executing notebook: {}'.format(err), file=sys.stderr)
return
def run_notebooks(test_result_dirs):
for d in test_result_dirs:
analysis_dir = os.path.join(d, 'analysis')
if not os.path.exists(analysis_dir):
print('no analysis dir at {}, ignoring'.format(analysis_dir), file=sys.stderr)
continue
print('running analysis in {}'.format(analysis_dir))
run_analysis_notebook(analysis_dir)
def run():
args = parse_args()
if args.subcommand == 'extract':
zip_filename = args.test_output_zip_path[0]
extract_test_outputs(zip_filename, args.output_dir)
elif args.subcommand == 'run_notebook':
run_notebooks(args.test_result_dir)
else:
print('unknown subcommand', file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
run()

View File

@ -0,0 +1 @@
{"main": {"test_execution": {"output_dir": {"value": "./output/pubsub-test-20200511-162649"}, "failed_dir": {"value": "./output/failed"}}, "testground": {"daemon_endpoint": {"value": "localhost:8080"}, "builder": {"value": "docker:go"}, "runner": {"value": "cluster:k8s"}, "keep_service": {"value": false}, "log_level": {"value": "info"}}, "time": {"setup": {"value": "3m"}, "run": {"value": "3m"}, "warm": {"value": "30s"}, "cool": {"value": "30s"}}, "node_counts": {"total": {"value": 1000}, "publisher": {"value": 100}, "lurker": {"value": 900}, "honest_per_container": {"value": 1}}, "pubsub": {"branch": {"value": "master"}, "use_hardened_api": {"value": true}, "heartbeat": {"value": "1s"}, "hearbeat_delay": {"value": "100ms"}, "validate_queue_size": {"value": 1024}, "outbound_queue_size": {"value": 128}, "score_inspect_period": {"value": "5s"}, "full_traces": {"value": false}, "degree": {"value": 8}, "degree_lo": {"value": 6}, "degree_hi": {"value": 12}, "degree_score": {"value": 6}, "degree_lazy": {"value": 12}, "gossip_factor": {"value": 0.25}, "opportunistic_graft_ticks": {"value": 60}}, "network": {"latency": {"value": "25ms"}, "max_latency": {"value": "50ms"}, "jitter_pct": {"value": 10}, "bandwidth_mb": {"value": 10240}, "degree": {"value": 20}}, "honest_behavior": {"flood_publishing": {"value": true}, "connect_delay": {"value": "0s"}, "connect_jitter_pct": {"value": 5}}, "peer_score": {"gossip_threshold": {"value": -4000.0}, "publish_threshold": {"value": -5000.0}, "graylist_threshold": {"value": -10000.0}, "acceptpx_threshold": {"value": 0.0}, "opportunistic_graft_threshold": {"value": 0.0}, "ip_colocation_weight": {"value": 0.0}, "ip_colocation_threshold": {"value": 1}, "decay_interval": {"value": "1s"}, "decay_to_zero": {"value": 0.01}, "retain_score": {"value": "30s"}}}, "topic": {"topic_weight": {"value": 0.25}, "topic": {"name": {"value": "blocks"}, "message_rate": {"value": "120/s"}, "message_size": {"value": "2 KiB"}}, "score": {"time_in_mesh": {"weight": {"value": 0.0027}, "quantum": {"value": "1s"}, "cap": {"value": 3600.0}}, "first_message_deliveries": {"weight": {"value": 0.664}, "decay": {"value": 0.9916}, "cap": {"value": 1500.0}}, "mesh_message_deliveries": {"weight": {"value": -0.25}, "decay": {"value": 0.997}, "cap": {"value": 400.0}, "threshold": {"value": 10.0}, "activation": {"value": "1m"}, "window": {"value": "5ms"}}, "mesh_failure_penalty": {"weight": {"value": -0.25}, "decay": {"value": 0.997}}, "invalid_message_deliveries": {"weight": {"value": -99.0}, "decay": {"value": 0.9994}}}}}

View File

@ -0,0 +1 @@
{"main": {"test_execution": {"output_dir": {"value": "./output/pubsub-test-20200513-125310"}, "failed_dir": {"value": "./output/failed"}}, "testground": {"daemon_endpoint": {"value": "localhost:8080"}, "builder": {"value": "docker:go"}, "runner": {"value": "local:docker"}, "keep_service": {"value": false}, "log_level": {"value": "info"}}, "time": {"setup": {"value": "3m"}, "run": {"value": "3m"}, "warm": {"value": "30s"}, "cool": {"value": "30s"}}, "node_counts": {"total": {"value": 100}, "total_peers": {"value": 100}, "publisher": {"value": 10}, "lurker": {"value": 90}, "honest_per_container": {"value": 1}}, "pubsub": {"branch": {"value": "master"}, "use_hardened_api": {"value": true}, "heartbeat": {"value": "1s"}, "hearbeat_delay": {"value": "100ms"}, "validate_queue_size": {"value": 1024}, "outbound_queue_size": {"value": 128}, "score_inspect_period": {"value": "5s"}, "full_traces": {"value": false}, "degree": {"value": 8}, "degree_lo": {"value": 6}, "degree_hi": {"value": 12}, "degree_score": {"value": 6}, "degree_lazy": {"value": 12}, "gossip_factor": {"value": 0.25}, "opportunistic_graft_ticks": {"value": 60}}, "network": {"latency": {"value": "25ms"}, "max_latency": {"value": "50ms"}, "jitter_pct": {"value": 10}, "bandwidth_mb": {"value": 10240}, "degree": {"value": 20}}, "honest_behavior": {"flood_publishing": {"value": true}, "connect_delay": {"value": "0s"}, "connect_jitter_pct": {"value": 5}}, "peer_score": {"gossip_threshold": {"value": -4000.0}, "publish_threshold": {"value": -5000.0}, "graylist_threshold": {"value": -10000.0}, "acceptpx_threshold": {"value": 0.0}, "opportunistic_graft_threshold": {"value": 0.0}, "ip_colocation_weight": {"value": 0.0}, "ip_colocation_threshold": {"value": 1}, "decay_interval": {"value": "1s"}, "decay_to_zero": {"value": 0.01}, "retain_score": {"value": "30s"}}}, "topic": {"topic_weight": {"value": 0.25}, "topic": {"name": {"value": "blocks"}, "message_rate": {"value": "120/s"}, "message_size": {"value": "2 KiB"}}, "score": {"time_in_mesh": {"weight": {"value": 0.0027}, "quantum": {"value": "1s"}, "cap": {"value": 3600.0}}, "first_message_deliveries": {"weight": {"value": 0.664}, "decay": {"value": 0.9916}, "cap": {"value": 1500.0}}, "mesh_message_deliveries": {"weight": {"value": -0.25}, "decay": {"value": 0.997}, "cap": {"value": 400.0}, "threshold": {"value": 10.0}, "activation": {"value": "1m"}, "window": {"value": "5ms"}}, "mesh_failure_penalty": {"weight": {"value": -0.25}, "decay": {"value": 0.997}}, "invalid_message_deliveries": {"weight": {"value": -99.0}, "decay": {"value": 0.9994}}}}}

View File

@ -0,0 +1 @@
{"main": {"test_execution": {"output_dir": {"value": "./output/pubsub-test-20200513-125310"}, "failed_dir": {"value": "./output/failed"}}, "testground": {"daemon_endpoint": {"value": "localhost:8080"}, "builder": {"value": "docker:go"}, "runner": {"value": "local:docker"}, "keep_service": {"value": false}, "log_level": {"value": "info"}}, "time": {"setup": {"value": "3m"}, "run": {"value": "3m"}, "warm": {"value": "30s"}, "cool": {"value": "30s"}}, "node_counts": {"total": {"value": 25}, "total_peers": {"value": 25}, "publisher": {"value": 5}, "lurker": {"value": 20}, "honest_per_container": {"value": 1}}, "pubsub": {"branch": {"value": "master"}, "use_hardened_api": {"value": true}, "heartbeat": {"value": "1s"}, "hearbeat_delay": {"value": "100ms"}, "validate_queue_size": {"value": 1024}, "outbound_queue_size": {"value": 128}, "score_inspect_period": {"value": "5s"}, "full_traces": {"value": false}, "degree": {"value": 8}, "degree_lo": {"value": 6}, "degree_hi": {"value": 12}, "degree_score": {"value": 6}, "degree_lazy": {"value": 12}, "gossip_factor": {"value": 0.25}, "opportunistic_graft_ticks": {"value": 60}}, "network": {"latency": {"value": "25ms"}, "max_latency": {"value": "50ms"}, "jitter_pct": {"value": 10}, "bandwidth_mb": {"value": 10240}, "degree": {"value": 20}}, "honest_behavior": {"flood_publishing": {"value": true}, "connect_delay": {"value": "0s"}, "connect_jitter_pct": {"value": 5}}, "peer_score": {"gossip_threshold": {"value": -4000.0}, "publish_threshold": {"value": -5000.0}, "graylist_threshold": {"value": -10000.0}, "acceptpx_threshold": {"value": 0.0}, "opportunistic_graft_threshold": {"value": 0.0}, "ip_colocation_weight": {"value": 0.0}, "ip_colocation_threshold": {"value": 1}, "decay_interval": {"value": "1s"}, "decay_to_zero": {"value": 0.01}, "retain_score": {"value": "30s"}}}, "topic": {"topic_weight": {"value": 0.25}, "topic": {"name": {"value": "blocks"}, "message_rate": {"value": "120/s"}, "message_size": {"value": "2 KiB"}}, "score": {"time_in_mesh": {"weight": {"value": 0.0027}, "quantum": {"value": "1s"}, "cap": {"value": 3600.0}}, "first_message_deliveries": {"weight": {"value": 0.664}, "decay": {"value": 0.9916}, "cap": {"value": 1500.0}}, "mesh_message_deliveries": {"weight": {"value": -0.25}, "decay": {"value": 0.997}, "cap": {"value": 400.0}, "threshold": {"value": 10.0}, "activation": {"value": "1m"}, "window": {"value": "5ms"}}, "mesh_failure_penalty": {"weight": {"value": -0.25}, "decay": {"value": 0.997}}, "invalid_message_deliveries": {"weight": {"value": -99.0}, "decay": {"value": 0.9994}}}}}

View File

@ -0,0 +1 @@
{"main": {"test_execution": {"output_dir": {"value": "./output/pubsub-test-20200513-125310"}, "failed_dir": {"value": "./output/failed"}}, "testground": {"daemon_endpoint": {"value": "localhost:8080"}, "builder": {"value": "docker:go"}, "runner": {"value": "local:docker"}, "keep_service": {"value": false}, "log_level": {"value": "info"}}, "time": {"setup": {"value": "3m"}, "run": {"value": "3m"}, "warm": {"value": "30s"}, "cool": {"value": "30s"}}, "node_counts": {"total": {"value": 50}, "total_peers": {"value": 50}, "publisher": {"value": 5}, "lurker": {"value": 45}, "honest_per_container": {"value": 1}}, "pubsub": {"branch": {"value": "master"}, "use_hardened_api": {"value": true}, "heartbeat": {"value": "1s"}, "hearbeat_delay": {"value": "100ms"}, "validate_queue_size": {"value": 1024}, "outbound_queue_size": {"value": 128}, "score_inspect_period": {"value": "5s"}, "full_traces": {"value": false}, "degree": {"value": 8}, "degree_lo": {"value": 6}, "degree_hi": {"value": 12}, "degree_score": {"value": 6}, "degree_lazy": {"value": 12}, "gossip_factor": {"value": 0.25}, "opportunistic_graft_ticks": {"value": 60}}, "network": {"latency": {"value": "25ms"}, "max_latency": {"value": "50ms"}, "jitter_pct": {"value": 10}, "bandwidth_mb": {"value": 10240}, "degree": {"value": 20}}, "honest_behavior": {"flood_publishing": {"value": true}, "connect_delay": {"value": "0s"}, "connect_jitter_pct": {"value": 5}}, "peer_score": {"gossip_threshold": {"value": -4000.0}, "publish_threshold": {"value": -5000.0}, "graylist_threshold": {"value": -10000.0}, "acceptpx_threshold": {"value": 0.0}, "opportunistic_graft_threshold": {"value": 0.0}, "ip_colocation_weight": {"value": 0.0}, "ip_colocation_threshold": {"value": 1}, "decay_interval": {"value": "1s"}, "decay_to_zero": {"value": 0.01}, "retain_score": {"value": "30s"}}}, "topic": {"topic_weight": {"value": 0.25}, "topic": {"name": {"value": "blocks"}, "message_rate": {"value": "120/s"}, "message_size": {"value": "2 KiB"}}, "score": {"time_in_mesh": {"weight": {"value": 0.0027}, "quantum": {"value": "1s"}, "cap": {"value": 3600.0}}, "first_message_deliveries": {"weight": {"value": 0.664}, "decay": {"value": 0.9916}, "cap": {"value": 1500.0}}, "mesh_message_deliveries": {"weight": {"value": -0.25}, "decay": {"value": 0.997}, "cap": {"value": 400.0}, "threshold": {"value": 10.0}, "activation": {"value": "1m"}, "window": {"value": "5ms"}}, "mesh_failure_penalty": {"weight": {"value": -0.25}, "decay": {"value": 0.997}}, "invalid_message_deliveries": {"weight": {"value": -99.0}, "decay": {"value": 0.9994}}}}}

View File

@ -0,0 +1 @@
{"main": {"test_execution": {"output_dir": {"value": "./output/pubsub-test-20200513-125310"}, "failed_dir": {"value": "./output/failed"}}, "testground": {"daemon_endpoint": {"value": "localhost:8080"}, "builder": {"value": "exec:go"}, "runner": {"value": "local:exec"}, "keep_service": {"value": false}, "log_level": {"value": "info"}}, "time": {"setup": {"value": "3m"}, "run": {"value": "3m"}, "warm": {"value": "30s"}, "cool": {"value": "30s"}}, "node_counts": {"total": {"value": 25}, "total_peers": {"value": 25}, "publisher": {"value": 5}, "lurker": {"value": 20}, "honest_per_container": {"value": 1}}, "pubsub": {"branch": {"value": "master"}, "use_hardened_api": {"value": true}, "heartbeat": {"value": "1s"}, "hearbeat_delay": {"value": "100ms"}, "validate_queue_size": {"value": 1024}, "outbound_queue_size": {"value": 128}, "score_inspect_period": {"value": "5s"}, "full_traces": {"value": false}, "degree": {"value": 8}, "degree_lo": {"value": 6}, "degree_hi": {"value": 12}, "degree_score": {"value": 6}, "degree_lazy": {"value": 12}, "gossip_factor": {"value": 0.25}, "opportunistic_graft_ticks": {"value": 60}}, "network": {"latency": {"value": "25ms"}, "max_latency": {"value": "50ms"}, "jitter_pct": {"value": 10}, "bandwidth_mb": {"value": 10240}, "degree": {"value": 20}}, "honest_behavior": {"flood_publishing": {"value": true}, "connect_delay": {"value": "0s"}, "connect_jitter_pct": {"value": 5}}, "peer_score": {"gossip_threshold": {"value": -4000.0}, "publish_threshold": {"value": -5000.0}, "graylist_threshold": {"value": -10000.0}, "acceptpx_threshold": {"value": 0.0}, "opportunistic_graft_threshold": {"value": 0.0}, "ip_colocation_weight": {"value": 0.0}, "ip_colocation_threshold": {"value": 1}, "decay_interval": {"value": "1s"}, "decay_to_zero": {"value": 0.01}, "retain_score": {"value": "30s"}}}, "topic": {"topic_weight": {"value": 0.25}, "topic": {"name": {"value": "blocks"}, "message_rate": {"value": "120/s"}, "message_size": {"value": "2 KiB"}}, "score": {"time_in_mesh": {"weight": {"value": 0.0027}, "quantum": {"value": "1s"}, "cap": {"value": 3600.0}}, "first_message_deliveries": {"weight": {"value": 0.664}, "decay": {"value": 0.9916}, "cap": {"value": 1500.0}}, "mesh_message_deliveries": {"weight": {"value": -0.25}, "decay": {"value": 0.997}, "cap": {"value": 400.0}, "threshold": {"value": 10.0}, "activation": {"value": "1m"}, "window": {"value": "5ms"}}, "mesh_failure_penalty": {"weight": {"value": -0.25}, "decay": {"value": 0.997}}, "invalid_message_deliveries": {"weight": {"value": -99.0}, "decay": {"value": 0.9994}}}}}

View File

@ -0,0 +1 @@
{"main": {"test_execution": {"output_dir": {"value": "./output/pubsub-test-20200513-125310"}, "failed_dir": {"value": "./output/failed"}}, "testground": {"daemon_endpoint": {"value": "localhost:8080"}, "builder": {"value": "exec:go"}, "runner": {"value": "local:exec"}, "keep_service": {"value": false}, "log_level": {"value": "info"}}, "time": {"setup": {"value": "3m"}, "run": {"value": "3m"}, "warm": {"value": "30s"}, "cool": {"value": "30s"}}, "node_counts": {"total": {"value": 50}, "total_peers": {"value": 50}, "publisher": {"value": 5}, "lurker": {"value": 45}, "honest_per_container": {"value": 1}}, "pubsub": {"branch": {"value": "master"}, "use_hardened_api": {"value": true}, "heartbeat": {"value": "1s"}, "hearbeat_delay": {"value": "100ms"}, "validate_queue_size": {"value": 1024}, "outbound_queue_size": {"value": 128}, "score_inspect_period": {"value": "5s"}, "full_traces": {"value": false}, "degree": {"value": 8}, "degree_lo": {"value": 6}, "degree_hi": {"value": 12}, "degree_score": {"value": 6}, "degree_lazy": {"value": 12}, "gossip_factor": {"value": 0.25}, "opportunistic_graft_ticks": {"value": 60}}, "network": {"latency": {"value": "25ms"}, "max_latency": {"value": "50ms"}, "jitter_pct": {"value": 10}, "bandwidth_mb": {"value": 10240}, "degree": {"value": 20}}, "honest_behavior": {"flood_publishing": {"value": true}, "connect_delay": {"value": "0s"}, "connect_jitter_pct": {"value": 5}}, "peer_score": {"gossip_threshold": {"value": -4000.0}, "publish_threshold": {"value": -5000.0}, "graylist_threshold": {"value": -10000.0}, "acceptpx_threshold": {"value": 0.0}, "opportunistic_graft_threshold": {"value": 0.0}, "ip_colocation_weight": {"value": 0.0}, "ip_colocation_threshold": {"value": 1}, "decay_interval": {"value": "1s"}, "decay_to_zero": {"value": 0.01}, "retain_score": {"value": "30s"}}}, "topic": {"topic_weight": {"value": 0.25}, "topic": {"name": {"value": "blocks"}, "message_rate": {"value": "120/s"}, "message_size": {"value": "2 KiB"}}, "score": {"time_in_mesh": {"weight": {"value": 0.0027}, "quantum": {"value": "1s"}, "cap": {"value": 3600.0}}, "first_message_deliveries": {"weight": {"value": 0.664}, "decay": {"value": 0.9916}, "cap": {"value": 1500.0}}, "mesh_message_deliveries": {"weight": {"value": -0.25}, "decay": {"value": 0.997}, "cap": {"value": 400.0}, "threshold": {"value": 10.0}, "activation": {"value": "1m"}, "window": {"value": "5ms"}}, "mesh_failure_penalty": {"weight": {"value": -0.25}, "decay": {"value": 0.997}}, "invalid_message_deliveries": {"weight": {"value": -99.0}, "decay": {"value": 0.9994}}}}}

8
pubsub/scripts/go.mod Normal file
View File

@ -0,0 +1,8 @@
// This module only exists so that our script can invoke tracestat using `go run` instead of having to
// install the tracestat binary.
module github.com/libp2p/test-plans/pubsub/scripts
go 1.14
require github.com/libp2p/go-libp2p-pubsub-tracer v0.0.0-20200120141315-151ce254cf29 // indirect

334
pubsub/scripts/go.sum Normal file
View File

@ -0,0 +1,334 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8=
github.com/btcsuite/btcd v0.0.0-20190523000118-16327141da8c/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/davidlazar/go-crypto v0.0.0-20170701192655-dcfb0a7ac018/go.mod h1:rQYf4tfk5sSwFsnDg3qYaBxSjsD9S8+59vW0dKUgme4=
github.com/dgraph-io/badger v1.5.5-0.20190226225317-8115aed38f8f/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ=
github.com/dgraph-io/badger v1.6.0-rc1/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/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/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU=
github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.4 h1:UlfXKrZx1DjZoBhQHmNHLC1fK1dUJDN20Y28A7s+gJ8=
github.com/ipfs/go-cid v0.0.4/go.mod h1:4LLaPOQwmk5z9LBgQnpkivrx8BJjUyGwTXCd5Xfj6+M=
github.com/ipfs/go-datastore v0.0.1/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE=
github.com/ipfs/go-datastore v0.1.0/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE=
github.com/ipfs/go-datastore v0.1.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw=
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ds-badger v0.0.2/go.mod h1:Y3QpeSFWQf6MopLTiZD+VT6IC1yZqaGmjvRcKeSGij8=
github.com/ipfs/go-ds-badger v0.0.5/go.mod h1:g5AuuCGmr7efyzQhLL8MzwqcauPojGPUaHzfGTzuE3s=
github.com/ipfs/go-ds-badger v0.0.7/go.mod h1:qt0/fWzZDoPW6jpQeqUjR5kBfhDNB65jd9YlmAvpQBk=
github.com/ipfs/go-ds-leveldb v0.0.1/go.mod h1:feO8V3kubwsEF22n0YRQCffeb79OOYIykR4L04tMOYc=
github.com/ipfs/go-ds-leveldb v0.1.0/go.mod h1:hqAW8y4bwX5LWcCtku2rFNX3vjDZCy5LZCg+cSZvYb8=
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc=
github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM=
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jbenet/go-cienv v0.0.0-20150120210510-1bb1476777ec/go.mod h1:rGaEvXB4uRSZMmzKNLoXvTu1sfx+1kv/DojUlPrSZGs=
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
github.com/jbenet/go-temp-err-catcher v0.0.0-20150120210811-aac704a3f4f2/go.mod h1:8GXXJV31xl8whumTzdZsTt3RnUIiPqzkyf7mxToRCMs=
github.com/jbenet/goprocess v0.0.0-20160826012719-b497e2f366b8/go.mod h1:Ly/wlsjFq/qrU3Rar62tu1gASgGw6chQbSh/XgIIXCY=
github.com/jbenet/goprocess v0.1.3/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/libp2p/go-addr-util v0.0.1/go.mod h1:4ac6O7n9rIAKB1dnd+s8IbbMXkt+oBpzX4/+RACcnlQ=
github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ=
github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
github.com/libp2p/go-conn-security-multistream v0.1.0/go.mod h1:aw6eD7LOsHEX7+2hJkDxw1MteijaVcI+/eP2/x3J1xc=
github.com/libp2p/go-eventbus v0.1.0/go.mod h1:vROgu5cs5T7cv7POWlWxBaVLxfSegC5UGQf8A2eEmx4=
github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZxBdp967ls1g+k8=
github.com/libp2p/go-flow-metrics v0.0.2/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
github.com/libp2p/go-libp2p v0.5.1/go.mod h1:Os7a5Z3B+ErF4v7zgIJ7nBHNu2LYt8ZMLkTQUB3G/wA=
github.com/libp2p/go-libp2p-autonat v0.1.1/go.mod h1:OXqkeGOY2xJVWKAGV2inNF5aKN/djNA3fdpCWloIudE=
github.com/libp2p/go-libp2p-blankhost v0.1.1/go.mod h1:pf2fvdLJPsC1FsVrNP3DUUvMzUts2dsLLBEpo1vW1ro=
github.com/libp2p/go-libp2p-blankhost v0.1.4/go.mod h1:oJF0saYsAXQCSfDq254GMNmLNz6ZTHTOvtF4ZydUvwU=
github.com/libp2p/go-libp2p-circuit v0.1.4/go.mod h1:CY67BrEjKNDhdTk8UgBX1Y/H5c3xkAcs3gnksxY7osU=
github.com/libp2p/go-libp2p-core v0.0.1/go.mod h1:g/VxnTZ/1ygHxH3dKok7Vno1VfpvGcGip57wjTU4fco=
github.com/libp2p/go-libp2p-core v0.0.4/go.mod h1:jyuCQP356gzfCFtRKyvAbNkyeuxb7OlyhWZ3nls5d2I=
github.com/libp2p/go-libp2p-core v0.2.0/go.mod h1:X0eyB0Gy93v0DZtSYbEM7RnMChm9Uv3j7yRXjO77xSI=
github.com/libp2p/go-libp2p-core v0.2.2/go.mod h1:8fcwTbsG2B+lTgRJ1ICZtiM5GWCWZVoVrLaDRvIRng0=
github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g=
github.com/libp2p/go-libp2p-core v0.2.5/go.mod h1:6+5zJmKhsf7yHn1RbmYDu08qDUpIUxGdqHuEZckmZOA=
github.com/libp2p/go-libp2p-core v0.3.0 h1:F7PqduvrztDtFsAa/bcheQ3azmNo+Nq7m8hQY5GiUW8=
github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw=
github.com/libp2p/go-libp2p-crypto v0.1.0/go.mod h1:sPUokVISZiy+nNuTTH/TY+leRSxnFj/2GLjtOTW90hI=
github.com/libp2p/go-libp2p-discovery v0.2.0/go.mod h1:s4VGaxYMbw4+4+tsoQTqh7wfxg97AEdo4GYBt6BadWg=
github.com/libp2p/go-libp2p-loggables v0.1.0/go.mod h1:EyumB2Y6PrYjr55Q3/tiJ/o3xoDasoRYM7nOzEpoa90=
github.com/libp2p/go-libp2p-mplex v0.2.0/go.mod h1:Ejl9IyjvXJ0T9iqUTE1jpYATQ9NM3g+OtR+EMMODbKo=
github.com/libp2p/go-libp2p-mplex v0.2.1/go.mod h1:SC99Rxs8Vuzrf/6WhmH41kNn13TiYdAWNYHrwImKLnE=
github.com/libp2p/go-libp2p-nat v0.0.5/go.mod h1:1qubaE5bTZMJE+E/uu2URroMbzdubFz1ChgiN79yKPE=
github.com/libp2p/go-libp2p-netutil v0.1.0/go.mod h1:3Qv/aDqtMLTUyQeundkKsA+YCThNdbQD54k3TqjpbFU=
github.com/libp2p/go-libp2p-peer v0.2.0/go.mod h1:RCffaCvUyW2CJmG2gAWVqwePwW7JMgxjsHm7+J5kjWY=
github.com/libp2p/go-libp2p-peerstore v0.1.0/go.mod h1:2CeHkQsr8svp4fZ+Oi9ykN1HBb6u0MOvdJ7YIsmcwtY=
github.com/libp2p/go-libp2p-peerstore v0.1.3/go.mod h1:BJ9sHlm59/80oSkpWgr1MyY1ciXAXV397W6h1GH/uKI=
github.com/libp2p/go-libp2p-peerstore v0.1.4/go.mod h1:+4BDbDiiKf4PzpANZDAT+knVdLxvqh7hXOujessqdzs=
github.com/libp2p/go-libp2p-pnet v0.1.0/go.mod h1:ZkyZw3d0ZFOex71halXRihWf9WH/j3OevcJdTmD0lyE=
github.com/libp2p/go-libp2p-pubsub v0.2.5 h1:tPKbkjAUI0xLGN3KKTKKy9TQEviVfrP++zJgH5Muke4=
github.com/libp2p/go-libp2p-pubsub v0.2.5/go.mod h1:9Q2RRq8ofXkoewORcyVlgUFDKLKw7BuYSlJVWRcVk3Y=
github.com/libp2p/go-libp2p-pubsub-tracer v0.0.0-20200120141315-151ce254cf29 h1:Fj1cGt6KgExlji/QYQBUi6O7bryGzUATDoPx6qXTJ38=
github.com/libp2p/go-libp2p-pubsub-tracer v0.0.0-20200120141315-151ce254cf29/go.mod h1:gSp/Ht64JWnnWb4X10w9IsBbgAlg203PAOyKi1m94AQ=
github.com/libp2p/go-libp2p-secio v0.1.0/go.mod h1:tMJo2w7h3+wN4pgU2LSYeiKPrfqBgkOsdiKK77hE7c8=
github.com/libp2p/go-libp2p-secio v0.2.0/go.mod h1:2JdZepB8J5V9mBp79BmwsaPQhRPNN2NrnB2lKQcdy6g=
github.com/libp2p/go-libp2p-secio v0.2.1/go.mod h1:cWtZpILJqkqrSkiYcDBh5lA3wbT2Q+hz3rJQq3iftD8=
github.com/libp2p/go-libp2p-swarm v0.1.0/go.mod h1:wQVsCdjsuZoc730CgOvh5ox6K8evllckjebkdiY5ta4=
github.com/libp2p/go-libp2p-swarm v0.2.2/go.mod h1:fvmtQ0T1nErXym1/aa1uJEyN7JzaTNyBcHImCxRpPKU=
github.com/libp2p/go-libp2p-testing v0.0.2/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
github.com/libp2p/go-libp2p-testing v0.0.3/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
github.com/libp2p/go-libp2p-testing v0.0.4/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
github.com/libp2p/go-libp2p-testing v0.1.0/go.mod h1:xaZWMJrPUM5GlDBxCeGUi7kI4eqnjVyavGroI2nxEM0=
github.com/libp2p/go-libp2p-testing v0.1.1/go.mod h1:xaZWMJrPUM5GlDBxCeGUi7kI4eqnjVyavGroI2nxEM0=
github.com/libp2p/go-libp2p-transport-upgrader v0.1.1/go.mod h1:IEtA6or8JUbsV07qPW4r01GnTenLW4oi3lOPbUMGJJA=
github.com/libp2p/go-libp2p-yamux v0.2.0/go.mod h1:Db2gU+XfLpm6E4rG5uGCFX6uXA8MEXOxFcRoXUODaK8=
github.com/libp2p/go-libp2p-yamux v0.2.1/go.mod h1:1FBXiHDk1VyRM1C0aez2bCfHQ4vMZKkAQzZbkSQt5fI=
github.com/libp2p/go-maddr-filter v0.0.4/go.mod h1:6eT12kSQMA9x2pvFQa+xesMKUBlj9VImZbj3B9FBH/Q=
github.com/libp2p/go-maddr-filter v0.0.5/go.mod h1:Jk+36PMfIqCJhAnaASRH83bdAvfDRp/w6ENFaC9bG+M=
github.com/libp2p/go-mplex v0.0.3/go.mod h1:pK5yMLmOoBR1pNCqDlA2GQrdAVTMkqFalaTWe7l4Yd0=
github.com/libp2p/go-mplex v0.1.0/go.mod h1:SXgmdki2kwCUlCCbfGLEgHjC4pFqhTp0ZoV6aiKgxDU=
github.com/libp2p/go-msgio v0.0.2/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ=
github.com/libp2p/go-msgio v0.0.4/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ=
github.com/libp2p/go-nat v0.0.4/go.mod h1:Nmw50VAvKuk38jUBcmNh6p9lUJLoODbJRvYAa/+KSDo=
github.com/libp2p/go-openssl v0.0.2/go.mod h1:v8Zw2ijCSWBQi8Pq5GAixw6DbFfa9u6VIYDXnvOXkc0=
github.com/libp2p/go-openssl v0.0.3/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc=
github.com/libp2p/go-openssl v0.0.4 h1:d27YZvLoTyMhIN4njrkr8zMDOM4lfpHIp6A+TK9fovg=
github.com/libp2p/go-openssl v0.0.4/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc=
github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA=
github.com/libp2p/go-reuseport-transport v0.0.2/go.mod h1:YkbSDrvjUVDL6b8XqriyA20obEtsW9BLkuOUyQAOCbs=
github.com/libp2p/go-stream-muxer v0.0.1/go.mod h1:bAo8x7YkSpadMTbtTaxGVHWUQsR/l5MEaHbKaliuT14=
github.com/libp2p/go-stream-muxer-multistream v0.2.0/go.mod h1:j9eyPol/LLRqT+GPLSxvimPhNph4sfYfMoDPd7HkzIc=
github.com/libp2p/go-tcp-transport v0.1.0/go.mod h1:oJ8I5VXryj493DEJ7OsBieu8fcg2nHGctwtInJVpipc=
github.com/libp2p/go-tcp-transport v0.1.1/go.mod h1:3HzGvLbx6etZjnFlERyakbaYPdfjg2pWP97dFZworkY=
github.com/libp2p/go-ws-transport v0.2.0/go.mod h1:9BHJz/4Q5A9ludYWKoGCFC5gUElzlHoKzu0yY9p/klM=
github.com/libp2p/go-yamux v1.2.2/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow=
github.com/libp2p/go-yamux v1.2.3/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
github.com/minio/sha256-simd v0.0.0-20190328051042-05b4dd3047e5/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
github.com/minio/sha256-simd v0.1.0/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
github.com/mr-tron/base58 v1.1.1/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
github.com/multiformats/go-multiaddr v0.0.1/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.0.2/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.0.4/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.1.0/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.2.0 h1:lR52sFwcTCuQb6bTfnXF6zA2XfyYvyd+5a9qECv/J90=
github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4=
github.com/multiformats/go-multiaddr-dns v0.0.1/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
github.com/multiformats/go-multiaddr-dns v0.0.2/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
github.com/multiformats/go-multiaddr-dns v0.2.0/go.mod h1:TJ5pr5bBO7Y1B18djPuRsVkduhQH2YqYSbxWJzYGdK0=
github.com/multiformats/go-multiaddr-fmt v0.0.1/go.mod h1:aBYjqL4T/7j4Qx+R73XSv/8JsgnRFlf0w2KGLCmXl3Q=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multiaddr-net v0.0.1/go.mod h1:nw6HSxNmCIQH27XPGBuX+d1tnvM7ihcFwHMSstNAVUU=
github.com/multiformats/go-multiaddr-net v0.1.0/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
github.com/multiformats/go-multiaddr-net v0.1.1/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
github.com/multiformats/go-multibase v0.0.1 h1:PN9/v21eLywrFWdFNsFKaU04kLJzuYzmrJR+ubhT9qA=
github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs=
github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U=
github.com/multiformats/go-multihash v0.0.5/go.mod h1:lt/HCbqlQwlPBz7lv0sQCdtfcMtlJvakRUn/0Ual8po=
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
github.com/multiformats/go-multihash v0.0.10 h1:lMoNbh2Ssd9PUF74Nz008KGzGPlfeV6wH3rit5IIGCM=
github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
github.com/multiformats/go-multistream v0.1.0/go.mod h1:fJTiDfXJVmItycydCnNx4+wSzZ5NwG2FEVAI30fiovg=
github.com/multiformats/go-varint v0.0.1 h1:TR/0rdQtnNxuN2IhiB639xC3tWM4IUi7DkTBVTdGW/M=
github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smola/gocompat v0.2.0/go.mod h1:1B0MlxbmoZNo3h8guHp8HztB3BSYR5itql9qtVc0ypY=
github.com/spacemonkeygo/openssl v0.0.0-20181017203307-c2dcc5cca94a/go.mod h1:7AyxJNCJ7SBZ1MfVQCWD6Uqo2oubI2Eq2y2eqf+A5r0=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM=
github.com/whyrusleeping/go-logging v0.0.1/go.mod h1:lDPYj54zutzG1XYfHAhcc7oNXEburHQBn+Iqd4yS4vE=
github.com/whyrusleeping/mafmt v1.2.8/go.mod h1:faQJFPbLSxzD9xpA02ttW/tS9vZykNvXwGvqIpk20FA=
github.com/whyrusleeping/mdns v0.0.0-20190826153040-b9b60ed33aa9/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4=
github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7/go.mod h1:X2c0RVCI1eSUFI8eLcY3c0423ykwiUdxLJtkDvruhjI=
github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee/go.mod h1:m2aV4LZI4Aez7dP5PMyVKEHhUyEJ/RjmPEDOpDvudHg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190225124518-7f87c0fbb88b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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/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-20181011144130-49bb7cea24b1/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-20190227160552-c95aed5357e7/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181205085412-a5c9d58dba9a/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-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/tools v0.0.0-20180221164845-07fd8470d635/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-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181130052023-1c3d964395ce/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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8=
gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -0,0 +1,389 @@
import os
import json
import pathlib
import multiprocessing as mp
from glob import glob
import pandas as pd
import toml
import ipywidgets as widgets
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
import matplotlib.patches as mpatches
import seaborn as sns
import numpy as np
import zipfile
def mkdirp(dirpath):
pathlib.Path(dirpath).mkdir(parents=True, exist_ok=True)
# sugar around recursive glob search
def find_files(dirname, filename_glob):
path = '{}/**/{}'.format(dirname, filename_glob)
return glob(path, recursive=True)
def empty_scores_dataframe():
return pd.DataFrame([], columns=['observer', 'peer', 'timestamp', 'score']).astype(
{'score': 'float64', 'observer': 'int64', 'peer': 'int64', 'timestamp': 'datetime64[ns]'})
def aggregate_peer_scores_single(scores_filepath, peers_table):
df = empty_scores_dataframe()
# select the cols from peers table we want to join on
p = peers_table[['peer_id', 'seq', 'honest']]
with open(scores_filepath, 'rt') as f:
for line in iter(f.readline, ''):
try:
data = json.loads(line)
except BaseException as err:
print('error parsing score json: ', err)
continue
scores = pd.json_normalize(data['Scores'])
scores = scores.T \
.rename(columns={0: 'score'}) \
.reset_index() \
.rename(columns={'index': 'peer_id'})
scores['timestamp'] = pd.to_datetime(data['Timestamp'])
scores['observer_id'] = data['PeerID']
# join with peers table to convert peer ids to seq numbers
s = scores.merge(p, on='peer_id').drop(columns=['peer_id'])
s = s.merge(p.drop(columns=['honest']), left_on='observer_id', right_on='peer_id', suffixes=['_peer', '_observer'])
s = s.drop(columns=['peer_id', 'observer_id'])
s = s.rename(columns={'seq_peer': 'peer', 'seq_observer': 'observer'})
df = df.append(s, ignore_index=True)
df.set_index('timestamp', inplace=True)
return df
def aggregate_peer_scores(score_filepaths, peers_table):
if len(score_filepaths) == 0:
return empty_scores_dataframe()
pool = mp.Pool(mp.cpu_count())
args = [(f, peers_table) for f in score_filepaths]
results = pool.starmap(aggregate_peer_scores_single, args)
# concat all data frames into one
return pd.concat(results)
def empty_metrics_dataframe():
return pd.DataFrame([], columns=['published', 'rejected', 'delivered', 'duplicates', 'droppedrpc',
'peersadded', 'peersremoved', 'topicsjoined', 'topicsleft', 'peer',
'sent_rpcs', 'sent_messages', 'sent_grafts', 'sent_prunes',
'sent_iwants', 'sent_ihaves', 'recv_rpcs', 'recv_messages',
'recv_grafts', 'recv_prunes', 'recv_iwants', 'recv_ihaves'])
def aggregate_metrics_to_pandas_single(metrics_filepath, peers_table):
def munge_keys(d, prefix=''):
out = dict()
for k, v in d.items():
outkey = prefix + k.lower()
out[outkey] = v
return out
rows = list()
with open(metrics_filepath, 'rb') as f:
try:
e = json.load(f)
except BaseException as err:
print('error loading metrics entry: ', err)
else:
pid = e['LocalPeer']
sent = munge_keys(e['SentRPC'], 'sent_')
recv = munge_keys(e['ReceivedRPC'], 'recv_')
del(e['LocalPeer'], e['SentRPC'], e['ReceivedRPC'])
row = munge_keys(e)
row.update(sent)
row.update(recv)
rows.append(row)
row['peer_id'] = pid
df = pd.DataFrame(rows)
p = peers_table[['peer_id', 'seq']]
df = df.merge(p, on='peer_id').drop(columns=['peer_id']).rename(columns={'seq': 'peer'})
return df.astype('int64')
def aggregate_metrics_to_pandas(metrics_filepaths, peers_table):
if len(metrics_filepaths) == 0:
return empty_metrics_dataframe()
pool = mp.Pool(mp.cpu_count())
args = [(f, peers_table) for f in metrics_filepaths]
results = pool.starmap(aggregate_metrics_to_pandas_single, args)
# concat all data frames into one
return pd.concat(results)
def cdf_to_pandas(cdf_filepath):
if os.path.exists(cdf_filepath):
return pd.read_csv(cdf_filepath, delim_whitespace=True, names=['delay_ms', 'count'], dtype='int64')
else:
return pd.DataFrame([], columns=['delay_ms', 'count'], dtype='int64')
def peer_info_to_pandas(peer_info_filename):
with open(peer_info_filename, 'rt') as f:
data = json.load(f)
peers = pd.json_normalize(data)
peers['honest'] = peers['type'] == 'honest'
return peers.astype({'type': 'category',
't_warm': 'datetime64[ns]',
't_connect': 'datetime64[ns]',
't_run': 'datetime64[ns]',
't_cool': 'datetime64[ns]',
't_complete': 'datetime64[ns]'})
def to_pandas(aggregate_output_dir, pandas_output_dir):
mkdirp(pandas_output_dir)
print('converting peer ids and info to pandas...')
peer_info_filename = os.path.join(aggregate_output_dir, 'peer-info.json')
peers = peer_info_to_pandas(peer_info_filename)
outfile = os.path.join(pandas_output_dir, 'peers.gz')
peers.to_pickle(outfile)
print('converting peer scores to pandas...')
scores_files = find_files(aggregate_output_dir, 'peer-scores*')
df = aggregate_peer_scores(scores_files, peers)
outfile = os.path.join(pandas_output_dir, 'scores.gz')
print('writing pandas peer scores to {}'.format(outfile))
df.to_pickle(outfile)
print('converting aggregate metrics to pandas...')
outfile = os.path.join(pandas_output_dir, 'metrics.gz')
metrics_files = find_files(aggregate_output_dir, '*aggregate.json')
df = aggregate_metrics_to_pandas(metrics_files, peers)
print('writing aggregate metrics pandas data to {}'.format(outfile))
df.to_pickle(outfile)
print('converting latency cdf to pandas...')
outfile = os.path.join(pandas_output_dir, 'cdf.gz')
cdf_file = os.path.join(aggregate_output_dir, 'tracestat-cdf.txt')
df = cdf_to_pandas(cdf_file)
print('writing cdf pandas data to {}'.format(outfile))
df.to_pickle(outfile)
def write_pandas(tables, output_dir):
pandas_dir = os.path.join(output_dir, 'pandas')
mkdirp(pandas_dir)
for name, df in tables.items():
fname = os.path.join(pandas_dir, '{}.gz'.format(name))
df.to_pickle(fname)
def load_pandas(analysis_dir):
analysis_dir = os.path.abspath(analysis_dir)
pandas_dir = os.path.join(analysis_dir, 'pandas')
if not os.path.exists(pandas_dir):
print('Cached pandas data not found. Converting analysis data from {} to pandas'.format(analysis_dir))
to_pandas(analysis_dir, pandas_dir)
tables = {}
for f in os.listdir(pandas_dir):
if not f.endswith('.gz'):
continue
name = os.path.splitext(f)[0]
tables[name] = pd.read_pickle(os.path.join(pandas_dir, f))
if 'cdf' in tables:
tables['pdf'] = cdf_to_pdf(tables['cdf'])
return tables
def test_params_panel(analysis_dir):
param_filename = os.path.join(analysis_dir, '..', 'template-params.toml')
with open(param_filename, 'rt') as f:
contents = f.read()
test_params = toml.loads(contents)
params_out = widgets.Output()
with params_out:
print(contents)
params_panel = widgets.Accordion([params_out])
params_panel.set_title(0, 'Test Parameters')
params_panel.selected_index = None
return (params_panel, test_params)
def save_fig_fn(dest, formats=['png', 'pdf']):
mkdirp(dest)
def save_fig(fig, filename, **kwargs):
try:
for fmt in formats:
base = os.path.splitext(filename)[0]
name = os.path.join(dest, '{}.{}'.format(base, fmt))
fig.savefig(name, format=fmt, **kwargs)
except BaseException as err:
print('Error saving figure to {}: {}'.format(filename, err))
return save_fig
def zipdir(path, ziph, extensions=['.png', '.pdf', '.eps', '.svg']):
# ziph is zipfile handle
for root, dirs, files in os.walk(path):
for file in files:
strs = os.path.splitext(file)
if len(strs) < 2:
continue
ext = strs[1]
if ext not in extensions:
continue
ziph.write(os.path.join(root, file))
def archive_figures(figure_dir, out_filename):
zipf = zipfile.ZipFile(out_filename, 'w', zipfile.ZIP_DEFLATED)
zipdir(figure_dir, zipf)
zipf.close()
def no_scores_message():
from IPython.display import display, Markdown
display(Markdown("""##### No peer score data, chart omitted"""))
def tracestat_summary(analysis_dir):
summary_file = os.path.join(analysis_dir, 'tracestat-summary.txt')
if os.path.exists(summary_file):
with open(summary_file, 'rt') as f:
return f.read()
else:
return('no tracestat summary file found')
def make_line(label, ax, x, color, alpha=0.5, linestyle='dashed'):
ax.axvline(x=x, linestyle=linestyle, color=color, alpha=alpha)
return mlines.Line2D([], [], color=color, linestyle=linestyle, label=label, alpha=alpha)
def make_span(label, ax, start, end, color, alpha=0.3):
ax.axvspan(start, end, facecolor=color, alpha=alpha)
return mpatches.Patch(color=color, alpha=alpha, label=label)
def annotate_times(ax, time_annotations, legend_anchor=None):
colors = sns.color_palette('Set2')
def next_color():
c = colors.pop(0)
colors.append(c)
return c
legends = []
for a in time_annotations:
t1 = a['time']
if pd.isnull(t1):
continue
label = a['label']
if 'end_time' in a:
# if we have an end_time, draw a span between start and end
t2 = a['end_time']
if pd.isnull(t2):
continue
legends.append(make_span(label, ax, t1, t2, next_color()))
else:
# otherwise, draw a dashed line at t1
legends.append(make_line(label, ax, t1, next_color()))
if len(legends) != 0 and legend_anchor is not None:
# add the original legend to the plot
ax.add_artist(ax.legend(loc='upper left'))
# add second legend for marker lines
ax.legend(handles=legends, bbox_to_anchor=legend_anchor, loc='upper left')
def annotate_score_plot(plot, title, legend_anchor=None, time_annotations=[]):
plot.set_title(title)
plot.set_ylabel('score')
plot.set_xlabel('')
if len(time_annotations) != 0:
annotate_times(plot, time_annotations, legend_anchor=legend_anchor)
def draw_latency_threshold_lines(max_val, eth_threshold=3000, fil_threshold=6000):
legends = []
if max_val > eth_threshold * 0.75:
plt.axvline(eth_threshold, linestyle='--', color='orange')
l = mlines.Line2D([], [], color='orange', linestyle='--', label='Eth2 threshold')
legends.append(l)
if max_val > fil_threshold * 0.75:
plt.axvline(fil_threshold, linestyle='--', color='blue')
l = mlines.Line2D([], [], color='blue', linestyle='--', label='Fil threshold')
legends.append(l)
if len(legends) > 0:
plt.legend(handles=legends)
def plot_latency_cdf(cdf):
fig = plt.figure(figsize=(11,6))
fig.suptitle("Latency CDF")
plt.plot('delay_ms', 'count', data=cdf)
plt.ylabel('messages')
plt.xlabel('ms to fully propagate')
draw_latency_threshold_lines(cdf['delay_ms'].max())
plt.show()
return fig
def plot_latency_pdf(pdf):
fig = plt.figure(figsize=(11,6))
fig.suptitle('Latency Distribution (PDF)')
plt.hist(pdf['delay_ms'], weights=pdf['count'], bins=50)
plt.ylabel('messages')
plt.xlabel('ms to fully propagate')
draw_latency_threshold_lines(pdf['delay_ms'].max())
plt.show()
return fig
def plot_latency_pdf_above_quantile(pdf, quantile=0.99):
delays = pdf.reindex(pdf.index.repeat(pdf['count']))
q = delays['delay_ms'].quantile(quantile)
fig = plt.figure(figsize=(11,6))
qname = 'p{}'.format(int(round(quantile, 2) * 100))
fig.suptitle('Latency PDF above {} ({:.2f}ms)'.format(qname, round(q, 2)))
delays['delay_ms'].where(delays['delay_ms'] > q).dropna().plot.hist(bins=50)
plt.ylabel('messages')
plt.xlabel('ms to fully propagate')
plt.show()
return fig
def cdf_to_pdf(cdf):
delta = [0] * len(cdf['count'])
delta[0] = cdf['count'][0]
for x in range(1, len(cdf['count'])):
delta[x] = cdf['count'][x] - cdf['count'][x-1]
return pd.DataFrame({'delay_ms': cdf['delay_ms'], 'count': delta})
def p25(x):
return np.percentile(x, q=25)
def p50(x):
return np.percentile(x, q=50)
def p75(x):
return np.percentile(x, q=75)
def p95(x):
return np.percentile(x, q=95)
def p99(x):
return np.percentile(x, q=99)

View File

@ -0,0 +1,16 @@
toml
jinja2
ndjson
pandas
numpy
matplotlib
jupyter
ipywidgets
bunch
stringcase
papermill
jupyter-ui-poll
jupyter_contrib_nbextensions
durations
seaborn
python-rclone

256
pubsub/scripts/run.py Executable file
View File

@ -0,0 +1,256 @@
#!/usr/bin/env python
import toml
import jinja2
import json
import argparse
import os
import pathlib
import subprocess
import time
import analyze
import re
TESTGROUND_BIN = 'testground'
DEFAULT_GS_VERSION = 'latest'
# setting this build tag lets us compile test code that targets the new API from the hardening branch
HARDENED_API_BUILD_TAG = 'hardened_api'
# Testground build/run config settings to use when running on kubernetes
K8S_BUILD_CONFIG = {'bypass_cache': True, 'push_registry': True, 'registry_type': 'aws'}
K8S_RUN_CONFIG = {}
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('param_files', nargs='*',
help='name of one or more parameter files to use when generating composition from template. ' +
'if a param is defined in multiple files, last one wins.')
parser.add_argument('--name',
help='name of composition. will be used to create output directory.')
parser.add_argument('--template_dir',
default='./templates/baseline',
help='path to directory containing composition template and param files')
parser.add_argument('-o', '--output',
help='directory to write composition file and test outputs to',
default='./output')
parser.add_argument('--branch',
default='master',
help='configures the test to use the API from the given gossipsub branch')
parser.add_argument('--commit',
help='configures the test to use the API from the given gossipsub commit')
parser.add_argument('--k8s', action='store_true', default=False,
help='runs the test on kubernetes')
parser.add_argument('--dry-run', dest='dry_run', action='store_true', default=False,
help='skip running tests, just write composition files and exit')
parser.add_argument('--instances', type=int,
help='override the total number of test instances. equivalent to -D N_NODES=x')
parser.add_argument('-D', '--define', dest='definitions', action='append',
metavar='<key=value>',
help='set template variable `key` to `value`, e.g. -D T_RUN=10m')
parser.add_argument('--skip-analysis', dest='skip_analysis', action='store_true', default=False,
help='skip analysis phase after test run (can run manually later with analyze.py)')
return parser.parse_args()
def get_param_filepath(template_dir, param_filename):
p = param_filename
if os.path.exists(p):
return p
p = os.path.join(template_dir, 'params', p)
if os.path.exists(p):
return p
p = param_filename + '.toml'
if os.path.exists(p):
return p
p = os.path.join(template_dir, 'params', p)
if os.path.exists(p):
return p
raise ValueError("can't find param file " + param_filename)
def load_params(template_dir, param_files):
base_params_path = os.path.join(template_dir, 'params', '_base.toml')
paths = [get_param_filepath(template_dir, p) for p in param_files]
params = dict()
for path in [base_params_path] + paths:
p = toml.load(path)
for k, v in p.items():
params[k] = v
return params
# The TOPOLOGY parameter can be either
# - a JSON representation of a topology
# - a path to a file in that format
def parse_topology(params):
if 'TOPOLOGY' not in params:
params['TOPOLOGY'] = None
return
# If it's already JSON, we're all set
top = params['TOPOLOGY']
if len(top) > 0 and top[0] == '{':
return
# It's not JSON so assume it's a file path
if not os.path.exists(top):
raise ValueError("can't find topology file " + top)
# Make sure the file contents is JSON
with open(top, 'r') as infile:
contents = infile.read()
jsonstr = json.loads(contents)
params['TOPOLOGY'] = jsonstr
# N_CONTAINER_NODES_TOTAL is the total number of nodes including multiple nodes
# per container
def parse_n_container_nodes_total(params):
n_nodes = params.get('N_NODES', 20)
n_nodes_cont_honest = params.get('N_HONEST_PEERS_PER_NODE', 1)
total = n_nodes * n_nodes_cont_honest
params['N_CONTAINER_NODES_TOTAL'] = total
def render_template(template_dir, params):
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir)
)
template = env.get_template('template.toml.j2')
return template.render(**params)
def composition_name():
ts = time.strftime("%Y%m%d-%H%M%S")
return 'pubsub-test-{}'.format(ts)
def mkdirp(dirpath):
pathlib.Path(dirpath).mkdir(parents=True, exist_ok=True)
def run_composition(comp_filepath, output_dir, k8s=False):
archive_type = 'tgz' if k8s else 'zip'
outfilename = 'test-output.{}'.format(archive_type)
outpath = os.path.join(output_dir, outfilename)
print('running testground composition {}'.format(comp_filepath))
print("writing test outputs to {}".format(output_dir))
cmd = [TESTGROUND_BIN, 'run', 'composition', '-f', comp_filepath, '--collect', '-o', outpath]
subprocess.run(cmd, check=True)
print('test completed successfully!')
return outpath
def pubsub_commit(ref_str):
# if the input looks like a git commit already, just return it as-is
if re.match(r'\b([a-f0-9]{40})\b', ref_str):
return ref_str
out = subprocess.run(['git', 'ls-remote', 'git://github.com/libp2p/go-libp2p-pubsub'],
check=True, capture_output=True, text=True)
# look for matching branch or tag in output
pattern = r'^\b([a-f0-9]{40})\b.*refs/(heads|tags)/' + ref_str + '$'
m = re.search(pattern, out.stdout, re.MULTILINE)
if not m:
raise ValueError('no branch or tag found matching {}'.format(ref_str))
return m.group(1)
def run():
args = parse_args()
template_dir = args.template_dir
params = load_params(template_dir, args.param_files)
branch = None
if args.branch:
branch = args.branch
params['GS_VERSION'] = pubsub_commit(args.branch)
if args.commit:
params['GS_VERSION'] = args.commit
if branch is None and args.commit is None:
params['GS_VERSION'] = 'latest'
gs_version_msg = 'Using go-libp2p-pubsub commit ' + params['GS_VERSION']
if branch:
gs_version_msg += ' (' + branch + ')'
print(gs_version_msg)
if args.k8s:
params['TEST_RUNNER'] = 'cluster:k8s'
params['BUILD_CONFIG'] = toml.dumps(K8S_BUILD_CONFIG)
params['RUN_CONFIG'] = toml.dumps(K8S_RUN_CONFIG)
if args.instances is not None:
params['N_NODES'] = args.instances
if args.definitions is not None:
for defstr in args.definitions:
[k, v] = defstr.split('=')
try:
v = float(v)
if v.is_integer():
v = int(v)
params[k] = v
except:
params[k] = v
parse_topology(params)
parse_n_container_nodes_total(params)
comp = composition_name()
if args.name:
comp += '-' + args.name
workdir = os.path.join(args.output, comp)
pathlib.Path(workdir).mkdir(parents=True, exist_ok=True)
comp_filepath = os.path.join(workdir, 'composition.toml')
with open(comp_filepath, 'w', encoding='utf8') as f:
f.write(render_template(template_dir, params))
param_filepath = os.path.join(workdir, 'template-params.toml')
with open(param_filepath, 'wt') as f:
toml.dump(params, f)
print('wrote composition file to {}'.format(comp_filepath))
if args.dry_run:
print('dry run. skipping test execution')
return
test_output_archive = run_composition(comp_filepath, workdir, args.k8s)
if not args.skip_analysis:
print('extracting test output data')
analyze.extract_test_outputs(test_output_archive)
if __name__ == "__main__":
run()

155
pubsub/scripts/sync_outputs.py Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
import rclone
import os
import sys
import argparse
DEFAULT_S3_BUCKET = 'gossipsub-test-outputs'
DEFAULT_REGION = 'eu-central-1'
RCLONE_CONFIG_TEMPLATE = """
[s3]
type = s3
provider = AWS
env_auth = true
region = {region}
location_constraint = "{region}"
acl = public-read
"""
def rclone_config(region):
return RCLONE_CONFIG_TEMPLATE.format(region=region)
class OutputSyncer(object):
def __init__(self, region=DEFAULT_REGION, bucket=DEFAULT_S3_BUCKET):
self.config = rclone_config(region)
self.bucket = bucket
self._ensure_rclone_exists()
def _ensure_rclone_exists(self):
result = rclone.with_config(self.config).listremotes()
if result['code'] == -20:
raise EnvironmentError("the 'rclone' command must be present on the $PATH")
def list_outputs(self):
path = 's3:/{}/'.format(self.bucket)
result = rclone.with_config(self.config).run_cmd('lsd', [path])
if result['code'] != 0:
raise ValueError('failed to list output bucket: {}'.format(result))
out = result['out'].decode('utf8')
dirs = []
for line in out.splitlines():
name = line.split()[-1]
dirs.append(name)
return dirs
def fetch(self, name, dest_dir):
src = 's3:/{}/{}'.format(self.bucket, name)
dest = os.path.join(dest_dir, name)
result = rclone.with_config(self.config).sync(src, dest)
if result['code'] != 0:
print('error fetching {}: {}'.format(name, result['error']), file=sys.stderr)
def fetch_all(self, dest_dir):
src = 's3:/{}/'.format(self.bucket)
result = rclone.with_config(self.config).sync(src, dest_dir)
if result['code'] != 0:
print('error fetching all test outputs: {}'.format(result['error']), file=sys.stderr)
def store_single(self, test_run_dir):
"""
:param test_run_dir: path to local dir containing a single test run output, e.g. ./output/pubsub-test-20200409-152658
"""
name = os.path.basename(test_run_dir)
dest = 's3:/{}/{}'.format(self.bucket, name)
result = rclone.with_config(self.config).sync(test_run_dir, dest)
if result['code'] != 0:
print('error storing {}: {}'.format(name, result['error']), file=sys.stderr)
def store_all(self, src_dir, ignore=[]):
"""
:param src_dir: path to local dir containing multiple test run dirs, e.g. ./output
:param ignore: list of subdirectories to ignore
"""
for f in os.listdir(src_dir):
if f in ignore:
continue
src = os.path.join(src_dir, f)
dest = 's3:/{}/{}'.format(self.bucket, f)
print('syncing {} to {}'.format(src, dest))
result = rclone.with_config(self.config).sync(src, dest)
if result['code'] != 0:
print('error storing {}: {}'.format(f, result['error']), file=sys.stderr)
def parse_args():
parser = argparse.ArgumentParser(description="sync test outputs to/from an s3 bucket")
parser.add_argument('--region', default=DEFAULT_REGION, help='AWS region containing test output bucket')
parser.add_argument('--bucket', default=DEFAULT_S3_BUCKET, help='name of s3 bucket to store and fetch test outputs')
commands = parser.add_subparsers()
ls_cmd = commands.add_parser('list', aliases=['ls'], help='list test outputs in the s3 bucket')
ls_cmd.set_defaults(subcommand='list')
fetch_cmd = commands.add_parser('fetch', help='fetch one or more named test outputs from the s3 bucket')
fetch_cmd.set_defaults(subcommand='fetch')
fetch_cmd.add_argument('names', nargs='+', help='name of a test output directory to fetch')
fetch_cmd.add_argument('--dest', default='./output', help='directory to store fetched test output')
fetch_all_cmd = commands.add_parser('fetch-all', help='fetch all test outputs from the s3 bucket to a local dir')
fetch_all_cmd.set_defaults(subcommand='fetch-all')
fetch_all_cmd.add_argument('dest', help='directory to store fetched test output')
store_cmd = commands.add_parser('store', help='store one or more test outputs in s3')
store_cmd.set_defaults(subcommand='store')
store_cmd.add_argument('paths', nargs='+', help='path to a test output directory to store')
store_all_cmd = commands.add_parser('store-all', help='send all test outputs in a directory to s3')
store_all_cmd.set_defaults(subcommand='store-all')
store_all_cmd.set_defaults(ignore=['failed'])
store_all_cmd.add_argument('dir', help='local dir containing test output directories')
store_all_cmd.add_argument('--ignore', help='subdirectory to ignore (e.g. failed outputs)',
action='append')
return parser.parse_args()
def run():
args = parse_args()
syncer = OutputSyncer(region=args.region, bucket=args.bucket)
if args.subcommand == 'list':
outputs = syncer.list_outputs()
print('\n'.join(outputs))
return
if args.subcommand == 'fetch':
dest_dir = args.dest
for name in args.names:
print('fetching {} from s3://{} to {}'.format(name, args.bucket, dest_dir))
syncer.fetch(name, dest_dir)
return
if args.subcommand == 'fetch-all':
dest_dir = args.dest
print('fetching all test outputs from s3://{}'.format(args.bucket))
syncer.fetch_all(dest_dir)
return
if args.subcommand == 'store':
for p in args.paths:
print('syncing {} to s3://{}'.format(p, args.bucket))
syncer.store_single(p)
return
if args.subcommand == 'store-all':
print('syncing all subdirs of {} to s3://{} - excluding {}'.format(args.dir, args.bucket, args.ignore))
syncer.store_all(args.dir, ignore=args.ignore)
if __name__ == '__main__':
run()

View File

@ -0,0 +1,3 @@
COMPOSITION_NAME = "baseline-1k-nodes"
N_NODES = 1000
N_PUBLISHER = 100

View File

@ -0,0 +1,90 @@
COMPOSITION_NAME = "baseline"
# the set of go build tags to apply when building.
# set this to an empty array if you want to target a commit before gossipsub v1.1 was merged.
BUILD_SELECTORS = ['hardened_api']
# version of go-libp2p-pubsub to use when building the test plan
GS_VERSION = "latest"
# time to run simulation (after warmup)
T_RUN = "3m"
# time to wait after subscribing to topics before publishing
T_WARM = "30s"
# cooldown time after publishing stops
T_COOL = "30s"
# total number of nodes in simulation
N_NODES = 20
# number of nodes that are attackers
N_ATTACK_NODES = 0
# number of honest (non-attack) nodes that are publishers
# the remaining honest nodes will lurk in their topics without
# publishing
N_PUBLISHER = 10
# how often to dump peer score values
T_SCORE_INSPECT_PERIOD = '5s'
# TOPIC_CONFIG controls which topics will be joined.
# The n_messages and message_size params are only relevant to publisher
# nodes.
#
# The message delivery rate is derived from the runtime, with each
# publisher attempting to deliver n_messages at a uniform rate througout
# t_run.
#
# Right now we use one TOPIC_CONFIG for all groups in the composition,
# but there's no reason you coudn't give each group its own config,
# for example to have peers publishing at different rates to the
# same topic.
TOPIC_CONFIG = [
{ id = 'blocks', message_rate = "120/s", message_size = '2 KiB' },
]
[PEER_SCORE_PARAMS]
IPColocationFactorWeight=0
IPColocationFactorThreshold=1
DecayInterval="5s"
DecayToZero=0.01
RetainScore="10s"
[PEER_SCORE_PARAMS.Thresholds]
GossipThreshold = -4000
PublishThreshold = -5000
GraylistThreshold = -10000
AcceptPXThreshold = 0
[PEER_SCORE_PARAMS.Topics.blocks]
TopicWeight = 0.25
# P1
TimeInMeshWeight = 0.0027
TimeInMeshQuantum = "1s"
TimeInMeshCap = 3600
# P2
FirstMessageDeliveriesWeight = 0.664
FirstMessageDeliveriesDecay = 0.9916
FirstMessageDeliveriesCap = 1500
# P3
MeshMessageDeliveriesWeight = -0.25
MeshMessageDeliveriesDecay = 0.97
MeshMessageDeliveriesCap = 400
MeshMessageDeliveriesThreshold = 100
MeshMessageDeliveriesActivation = '30s'
MeshMessageDeliveryWindow = '5ms'
# P3b
MeshFailurePenaltyWeight = -0.25
MeshFailurePenaltyDecay = 0.997
# P4
InvalidMessageDeliveriesWeight = -99
InvalidMessageDeliveriesDecay = 0.9994

View File

@ -0,0 +1,116 @@
## This composition runs a pubsub simulation with no adversarial nodes, to
## establish baseline metrics.
##
[metadata]
name = "pubsub-{{ COMPOSITION_NAME }}"
author = "yusefnapora"
[global]
plan = "{{ TEST_PLAN | default('test-plans/pubsub/test') }}"
case = "evaluate"
builder = "{{ TEST_BUILDER | default('docker:go') }}"
runner = "{{ TEST_RUNNER | default('local:docker') }}"
total_instances = {{ N_NODES }}
[global.build_config]
{{ BUILD_CONFIG | default('') }}
[global.run_config]
{{ RUN_CONFIG | default('') }}
[[groups]]
id = "publishers"
instances = { count = {{ N_PUBLISHER }} }
[groups.build]
selectors = {{ BUILD_SELECTORS }}
dependencies = [
{ module = "github.com/libp2p/go-libp2p-pubsub", version = "{{ GS_VERSION }}" }
]
[groups.run.test_params]
t_heartbeat = "{{ T_HEARTBEAT | default('1s') }}"
t_heartbeat_initial_delay = "{{ T_HEARTBEAT_INITIAL_DELAY | default('100ms') }}"
t_run = "{{ T_RUN }}"
t_warm = "{{ T_WARM }}"
t_cool = "{{ T_COOL | default('10s') }}"
t_setup = "{{ T_SETUP | default('1m') }}"
full_traces = "{{ FULL_TRACES | default('false') }}"
publisher = "true"
flood_publishing = '{{ FLOOD_PUBLISHING }}'
topics = '{{ TOPIC_CONFIG | tojson() }}'
score_params = '{{ PEER_SCORE_PARAMS | tojson() }}'
t_score_inspect_period = '{{ T_SCORE_INSPECT_PERIOD | default('0s') }}'
validate_queue_size = '{{ VALIDATE_QUEUE_SIZE | default(0) }}'
outbound_queue_size = '{{ OUTBOUND_QUEUE_SIZE | default(0) }}'
t_latency = '{{ T_LATENCY | default('5ms') }}'
t_latency_max = '{{ T_LATENCY_MAX | default('50ms') }}'
jitter_pct = '{{ JITTER_PCT | default(10) | int }}'
bandwidth_mb = '{{ BANDWIDTH_MB | default(10240) | int }}'
topology = '{{ TOPOLOGY | tojson() }}'
degree = '{{ N_DEGREE | default(20) | int }}'
overlay_d = '{{ OVERLAY_D | default(-1) | int }}'
overlay_dlo = '{{ OVERLAY_DLO | default(-1) | int }}'
overlay_dhi = '{{ OVERLAY_DHI | default(-1) | int }}'
overlay_dscore = '{{ OVERLAY_DSCORE | default(-1) | int }}'
overlay_dlazy = '{{ OVERLAY_DLAZY | default(-1) | int }}'
gossip_factor = '{{ GOSSIP_FACTOR | default(0.25) | float }}'
opportunistic_graft_ticks = '{{ OPPORTUNISTIC_GRAFT_TICKS | default(60) | int }}'
n_container_nodes_total = '{{ N_CONTAINER_NODES_TOTAL | default(N_NODES) }}'
n_nodes_per_container = '{{ N_HONEST_PEERS_PER_NODE | default(1) | int }}'
connect_delays = '{{ HONEST_CONNECT_DELAYS | default('') }}'
connect_delay_jitter_pct = '{{ HONEST_CONNECT_DELAY_JITTER_PCT | default('5') | int }}'
[[groups]]
id = "lurkers"
instances = { count = {{ N_NODES - N_PUBLISHER | int }} }
[groups.build]
selectors = {{ BUILD_SELECTORS }}
dependencies = [
{ module = "github.com/libp2p/go-libp2p-pubsub", version = "{{ GS_VERSION }}" }
]
[groups.run.test_params]
t_heartbeat = "{{ T_HEARTBEAT | default('1s') }}"
t_heartbeat_initial_delay = "{{ T_HEARTBEAT_INITIAL_DELAY | default('100ms') }}"
t_run = "{{ T_RUN }}"
t_warm = "{{ T_WARM }}"
t_cool = "{{ T_COOL | default('10s') }}"
t_setup = "{{ T_SETUP | default('1m') }}"
full_traces = "{{ FULL_TRACES | default('false') }}"
topics = '{{ TOPIC_CONFIG | tojson() }}'
score_params = '{{ PEER_SCORE_PARAMS | tojson() }}'
t_score_inspect_period = '{{ T_SCORE_INSPECT_PERIOD | default('0s') }}'
validate_queue_size = '{{ VALIDATE_QUEUE_SIZE | default(0) }}'
outbound_queue_size = '{{ OUTBOUND_QUEUE_SIZE | default(0) }}'
t_latency = '{{ T_LATENCY | default('5ms') }}'
t_latency_max = '{{ T_LATENCY_MAX | default('50ms') }}'
jitter_pct = '{{ JITTER_PCT | default(10) | int }}'
bandwidth_mb = '{{ BANDWIDTH_MB | default(10240) | int }}'
topology = '{{ TOPOLOGY | tojson() }}'
degree = '{{ N_DEGREE | default(20) | int }}'
overlay_d = '{{ OVERLAY_D | default(-1) | int }}'
overlay_dlo = '{{ OVERLAY_DLO | default(-1) | int }}'
overlay_dhi = '{{ OVERLAY_DHI | default(-1) | int }}'
overlay_dscore = '{{ OVERLAY_DSCORE | default(-1) | int }}'
overlay_dlazy = '{{ OVERLAY_DLAZY | default(-1) | int }}'
gossip_factor = '{{ GOSSIP_FACTOR | default(0.25) | float }}'
opportunistic_graft_ticks = '{{ OPPORTUNISTIC_GRAFT_TICKS | default(60) | int }}'
n_container_nodes_total = '{{ N_CONTAINER_NODES_TOTAL }}'
n_nodes_per_container = '{{ N_HONEST_PEERS_PER_NODE | default(1) | int }}'
connect_delays = '{{ HONEST_CONNECT_DELAYS | default('') }}'
connect_delay_jitter_pct = '{{ HONEST_CONNECT_DELAY_JITTER_PCT | default('5') | int }}'

545
pubsub/scripts/ui.py Normal file
View File

@ -0,0 +1,545 @@
import ipywidgets as widgets
import run as run_helpers
from bunch import Bunch
import functools
import operator
import stringcase
import subprocess
import os
import toml
import json
import time
from jupyter_ui_poll import ui_events
from IPython.display import display
import analyze
import shutil
# TODO: move constants
TEMPLATE = 'templates/baseline'
# set to path of testground bin if not in PATH
TESTGROUND = 'testground'
class RunButton(object):
def __init__(self, config):
self.config = config
self.pressed = False
self.button = widgets.Button(description='Run Test', button_style='primary')
self.button.on_click(self._clicked)
def _clicked(self, evt):
self.pressed = True
self.button.description = 'Running'
self.button.button_style = 'info'
self.button.disabled = True
def wait(self):
display(self.button)
with ui_events() as poll:
while self.pressed is False:
poll(10) # React to UI events (upto 10 at a time)
time.sleep(0.1)
self._run()
def _run(self):
endpoint = self.config.widgets.testground.daemon_endpoint.value
workdir = self.config.widgets.test_execution.output_dir.value
failed_dir = self.config.widgets.test_execution.failed_dir.value
run_helpers.mkdirp(workdir)
run_helpers.mkdirp(failed_dir)
params = self.config.template_params()
comp = self.config.composition()
comp_filename = os.path.join(workdir, 'composition.toml')
params_filename = os.path.join(workdir, 'template-params.toml')
config_snapshot_filename = os.path.join(workdir, 'config-snapshot.json')
if 'k8s' in params['TEST_RUNNER']:
archive_filename = os.path.join(workdir, 'test-output.tgz')
else:
archive_filename = os.path.join(workdir, 'test-output.zip')
with open(comp_filename, 'wt') as f:
f.write(comp)
with open(params_filename, 'w') as f:
toml.dump(params, f)
with open(config_snapshot_filename, 'w') as f:
json.dump(self.config.snapshot(), f)
cmd = [TESTGROUND, '--vv',
'--endpoint', endpoint,
'run', 'composition',
'-f', comp_filename,
'--collect', '-o', archive_filename]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
for line in iter(p.stdout.readline, ''):
print(line, end='')
if p.poll():
break
self.button.description = 'Done'
self.button.button_style = 'danger'
return_code = p.wait()
if return_code:
try:
shutil.move(workdir, failed_dir)
except BaseException as err:
print('tried to move output from failed test to {}, but failed with error: {}'.format(failed_dir, err))
raise ValueError('test execution failed, skipping analysis. moved outputs to {}'.format(failed_dir))
print('test outputs saved to {}'.format(workdir))
print('extracting test data for analysis...')
analysis_dir = os.path.join(workdir, 'analysis')
analyze.extract_test_outputs(archive_filename, analysis_dir, convert_to_pandas=False, prep_notebook=True)
print('saved analysis outputs to {}'.format(analysis_dir))
# a collapsible panel for a single topic's params
class TopicConfigPanel(object):
def __init__(self):
self.topic_widgets = Bunch(
name=widgets.Text(description="Topic Name", value="blocks"),
message_rate=widgets.Text(description="Message Rate (msg/sec)", value='120/s'),
message_size=widgets.Text(description="Message Size", value="2 KiB"),
)
self.topic_weight = widgets.FloatText(description="Topic Weight", value=0.25)
# NOTE: don't change the description values! they're used to derive the JSON keys when
# collecting the param values later
self.score_widgets = Bunch(
time_in_mesh=Bunch(
weight=widgets.FloatText(description="Time in Mesh Weight", value=0.0027),
quantum=widgets.Text(description="Time in Mesh Quantum", value='1s'),
cap=widgets.FloatText(description="Time in Mesh Cap", value=3600)
),
first_message_deliveries=Bunch(
weight=widgets.FloatText(description="First Message Deliveries Weight", value=0.664),
decay=widgets.FloatText(description="First Message Deliveries Decay", value=0.9916),
cap=widgets.FloatText(description="First Message Deliveries Cap", value=1500),
),
mesh_message_deliveries=Bunch(
weight=widgets.FloatText(description="Mesh Message Deliveries Weight", value=-0.25),
decay=widgets.FloatText(description="Mesh Message Deliveries Decay", value=0.97),
cap=widgets.FloatText(description="Mesh Message Deliveries Cap", value=400),
threshold=widgets.FloatText(description="Mesh Message Deliveries Threshold", value=100),
activation=widgets.Text(description="Mesh Message Deliveries Activation", value="30s"),
window=widgets.Text(description="Mesh Message Delivery Window", value="5ms"),
),
mesh_failure_penalty=Bunch(
weight=widgets.FloatText(description="Mesh Failure Penalty Weight", value=-0.25),
decay=widgets.FloatText(description="Mesh Failure Penalty Decay", value=0.997),
),
invalid_message_deliveries=Bunch(
weight=widgets.FloatText(description="Invalid Message Deliveries Weight", value=-99),
decay=widgets.FloatText(description="Invalid Message Deliveries Decay", value=0.9994),
)
)
topic_panel = widgets.VBox([
labeled(self.topic_widgets.name),
labeled(self.topic_widgets.message_rate),
labeled(self.topic_widgets.message_size),
])
score_panel = widgets.VBox([
widgets.HTML('<h3>Peer Score Params</h3>'),
labeled(self.topic_weight),
to_collapsible_sections(self.score_widgets)],
layout={'width': '900px'})
self.panel = widgets.VBox([topic_panel, score_panel], layout={'width': '900px'})
def ui(self):
return self.panel
def snapshot(self):
return {
'topic_weight': {'value': self.topic_weight.value},
'topic': widget_snapshot(self.topic_widgets),
'score': widget_snapshot(self.score_widgets),
}
def apply_snapshot(self, snapshot):
if 'topic_weight' in snapshot and 'value' in snapshot['topic_weight']:
self.topic_weight.value = snapshot['topic_weight']['value']
if 'topic' in snapshot:
apply_snapshot(self.topic_widgets, snapshot['topic'])
if 'score' in snapshot:
apply_snapshot(self.score_widgets, snapshot['score'])
def topic_id(self):
return self.topic_widgets.name.value
def topic_params(self):
return {
'id': self.topic_widgets.name.value,
'message_rate': self.topic_widgets.message_rate.value,
'message_size': self.topic_widgets.message_size.value,
}
def score_params(self):
p = {
'TopicWeight': self.topic_weight.value,
}
for group in self.score_widgets.values():
for param in group.values():
key = param.description.replace(' ', '')
p[key] = param.value
return p
# ConfigPanel is a collection of widgets to set the test parameters.
class ConfigPanel(object):
def __init__(self):
# all the widgets used to configure the test
default_out_dir = os.path.join('.', 'output', 'pubsub-test-{}'.format(time.strftime("%Y%m%d-%H%M%S")))
default_failed_dir = os.path.join('.', 'output', 'failed')
w = Bunch(
test_execution=Bunch(
output_dir=widgets.Text(description="Local directory to collect test outputs", value=default_out_dir),
failed_dir=widgets.Text(description="Local dir to store output from failed runs", value=default_failed_dir)
),
testground=Bunch(
daemon_endpoint=widgets.Text(description="Daemon Endpoint", value='localhost:8080'),
builder=widgets.Dropdown(description="Builder", options=['docker:go', 'exec:go']),
runner=widgets.Dropdown(description="Runner", options=['cluster:k8s', 'local:docker', 'local:exec']),
plan_dir=widgets.Text(description="Subdir of $TESTGROUND_HOME/plans containing pubsub plan", value="test-plans/pubsub/test"),
keep_service=widgets.Checkbox(description="Keep pods after execution? (k8s only)", value=False),
log_level=widgets.Dropdown(description="Log level to set on test instances", options=["info", "debug", "warn", "error"]),
),
time=Bunch(
setup=widgets.Text(description="Test Setup time", value='1m'),
run=widgets.Text(description="Test Runtime", value='2m'),
warm=widgets.Text(description="Warmup time", value='5s'),
cool=widgets.Text(description="Cooldown time", value='10s'),
),
node_counts=Bunch(
total=widgets.IntText(description="Total number of test instances", disabled=True),
total_peers=widgets.IntText(description="Total number of peers in all containers", disabled=True),
publisher=widgets.IntText(description="Number of publisher nodes", value=100),
lurker=widgets.IntText(description="Number of lurker nodes", value=50),
honest_per_container=widgets.IntText(description="# of honest peers per container", value=1),
),
pubsub=Bunch(
branch=widgets.Text(description="go-libp2p-pubsub branch/tag/commit to target", value="master"),
use_hardened_api=widgets.Checkbox(description="target hardening branch API", value=True),
heartbeat=widgets.Text(description='Heartbeat interval', value='1s'),
hearbeat_delay=widgets.Text(description='Initial heartbeat delay', value='100ms'),
validate_queue_size=widgets.IntText(description='Size of validation queue', value=32),
outbound_queue_size=widgets.IntText(description='Size of outbound RPC queue', value=32),
score_inspect_period=widgets.Text(description='Interval to dump peer scores', value='5s'),
full_traces=widgets.Checkbox(description='Capture full event traces)', value=False),
degree=widgets.IntText(description='D: target mesh degree', value=10),
degree_lo=widgets.IntText(description='D_lo: mesh degree low bound', value=8),
degree_hi=widgets.IntText(description='D_hi: mesh degree upper bound', value=16),
degree_score=widgets.IntText(description='D_score: peers to select by score', value=5),
degree_lazy=widgets.IntText(description='D_lazy: lazy propagation degree', value=12),
gossip_factor=widgets.FloatText(description='Gossip Factor', value=0.25),
opportunistic_graft_ticks=widgets.IntText(description='Opportunistic Graft heartbeat ticks', value=60),
),
network=Bunch(
latency = widgets.Text(description="Min latency", value='5ms'),
max_latency = widgets.Text(description="Max latency. If zero, latency will = min latency.", value='50ms'),
jitter_pct = widgets.IntSlider(description="Latency jitter %", value=10, min=1, max=100),
bandwidth_mb = widgets.IntText(description="Bandwidth (mb)", value=10240),
degree=widgets.IntText(description="Degree (# of initial connections) for honest peers", value=20),
# TODO: support upload of topology file
# topology_file = widgets.FileUpload(description="Upload fixed topology file", accept='.json', multiple=False),
),
honest_behavior=Bunch(
flood_publishing=widgets.Checkbox(value=True, description='Flood Publishing', indent=False),
connect_delay = widgets.Text(description='Honest peer connection delay. e.g. "30s" or "50@30s,30@1m"', value='0s'),
connect_jitter_pct = widgets.BoundedIntText(description='Jitter % for honest connect delay', value=5, min=0, max=100),
),
peer_score=Bunch(
gossip_threshold=widgets.FloatText(description='Gossip Threshold', value=-4000),
publish_threshold=widgets.FloatText(description='Publish Threshold', value=-5000),
graylist_threshold=widgets.FloatText(description='Graylist Threshold', value=-10000),
acceptpx_threshold=widgets.FloatText(description='Accept PX Threshold', value=0),
opportunistic_graft_threshold=widgets.FloatText(description='Opportunistic Graft Threshold', value=0),
ip_colocation_weight=widgets.FloatText(description='IP Colocation Factor Weight', value=0),
ip_colocation_threshold=widgets.IntText(description='IP Colocation Factor Threshold', value=1),
decay_interval=widgets.Text(description='Score Decay Interval', value='1s'),
decay_to_zero=widgets.FloatText(description='Decay Zero Threshold', value=0.01),
retain_score=widgets.Text(description="Time to Retain Score", value='30s'),
)
)
# wire up node count widgets to calculate and show the total number of containers and peers
# and update when the params they're derived from change
sum_values(w.node_counts.total, w.node_counts.publisher, w.node_counts.lurker)
mul_values(w.node_counts.total_peers, w.node_counts.total, w.node_counts.honest_per_container)
self.topic_config = TopicConfigPanel()
self.save_widgets = Bunch(
save_button = widgets.Button(description='Save Config', button_style='primary'),
load_button = widgets.Button(description='Load Saved Config', button_style='warning'),
snapshot_filename = widgets.Text(description='Path:', value='configs/snapshot.json')
)
self.save_widgets.save_button.on_click(self.save_clicked)
self.save_widgets.load_button.on_click(self.load_clicked)
save_panel = widgets.HBox(list(self.save_widgets.values()))
self.panel = widgets.VBox([
to_collapsible_sections(w),
collapsible("Topic Config", [self.topic_config.ui()]),
save_panel,
])
self.widgets = w
def ui(self):
return self.panel
def save_clicked(self, evt):
filename = self.save_widgets.snapshot_filename.value
with open(filename, 'wt') as f:
json.dump(self.snapshot(), f)
print('saved config snapshot to {}'.format(filename))
def load_clicked(self, evt):
filename = self.save_widgets.snapshot_filename.value
with open(filename, 'rt') as f:
snap = json.load(f)
# HACK: ignore the test_execution.output_dir param from the snapshot, to
# avoid overwriting the output of a prior run
if 'test_execution' in snap.get('main', {}):
del(snap['main']['test_execution']['output_dir'])
self.apply_snapshot(snap)
print('loaded config snapshot from {}'.format(filename))
def snapshot(self):
return {
'main': widget_snapshot(self.widgets),
'topic': self.topic_config.snapshot(),
}
def apply_snapshot(self, snapshot):
if 'main' in snapshot:
apply_snapshot(self.widgets, snapshot['main'])
if 'topic' in snapshot:
self.topic_config.apply_snapshot(snapshot['topic'])
def template_params(self):
w = self.widgets
n_nodes = w.node_counts.total.value
n_publisher = w.node_counts.publisher.value
n_nodes_cont_honest = w.node_counts.honest_per_container.value
n_honest_nodes = n_nodes
n_honest_peers_total = n_honest_nodes * n_nodes_cont_honest
n_container_nodes_total = n_honest_peers_total
p = {
# testground
'TEST_BUILDER': w.testground.builder.value,
'TEST_RUNNER': w.testground.runner.value,
'TEST_PLAN': w.testground.plan_dir.value,
# time
'T_SETUP': w.time.setup.value,
'T_RUN': w.time.run.value,
'T_WARM': w.time.warm.value,
'T_COOL': w.time.cool.value,
# node counts
'N_NODES': n_nodes,
'N_CONTAINER_NODES_TOTAL': n_container_nodes_total,
'N_PUBLISHER': n_publisher,
'N_HONEST_PEERS_PER_NODE': n_nodes_cont_honest,
# pubsub
'T_HEARTBEAT': w.pubsub.heartbeat.value,
'T_HEARTBEAT_INITIAL_DELAY': w.pubsub.hearbeat_delay.value,
'T_SCORE_INSPECT_PERIOD': w.pubsub.score_inspect_period.value,
'VALIDATE_QUEUE_SIZE': w.pubsub.validate_queue_size.value,
'OUTBOUND_QUEUE_SIZE': w.pubsub.outbound_queue_size.value,
'FULL_TRACES': w.pubsub.full_traces.value,
'OVERLAY_D': w.pubsub.degree.value,
'OVERLAY_DLO': w.pubsub.degree_lo.value,
'OVERLAY_DHI': w.pubsub.degree_hi.value,
'OVERLAY_DSCORE': w.pubsub.degree_score.value,
'OVERLAY_DLAZY': w.pubsub.degree_lazy.value,
'GOSSIP_FACTOR': w.pubsub.gossip_factor.value,
'OPPORTUNISTIC_GRAFT_TICKS': w.pubsub.opportunistic_graft_ticks.value,
# network
'T_LATENCY': w.network.latency.value,
'T_LATENCY_MAX': w.network.max_latency.value,
'JITTER_PCT': w.network.jitter_pct.value,
'BANDWIDTH_MB': w.network.bandwidth_mb.value,
'N_DEGREE': w.network.degree.value,
# TODO: load topology file
'TOPOLOGY': {},
# honest behavior
'FLOOD_PUBLISHING': w.honest_behavior.flood_publishing.value,
'HONEST_CONNECT_DELAY_JITTER_PCT': w.honest_behavior.connect_jitter_pct.value,
# topic & peer score configs
'TOPIC_CONFIG': self._topic_config(),
'PEER_SCORE_PARAMS': self._peer_score_params(),
}
if w.pubsub.use_hardened_api.value:
p['BUILD_SELECTORS'] = [run_helpers.HARDENED_API_BUILD_TAG]
else:
p['BUILD_SELECTORS'] = []
p['GS_VERSION'] = run_helpers.pubsub_commit(w.pubsub.branch.value)
run_config = ['log_level="{}"'.format(w.testground.log_level.value)]
if w.testground.runner.value == 'cluster:k8s':
buildopts = ['push_registry=true', 'registry_type="aws"']
p['BUILD_CONFIG'] = '\n'.join(buildopts)
if w.testground.keep_service.value:
run_config.append('keep_service=true')
p['RUN_CONFIG'] = '\n'.join(run_config)
# if the connect_delay param doesn't specify a count,
# make it apply to all honest nodes
delay = w.honest_behavior.connect_delay.value
if '@' not in delay:
delay = '{}@{}'.format(n_honest_peers_total, delay)
p['HONEST_CONNECT_DELAYS'] = delay
return p
def composition(self):
return run_helpers.render_template(TEMPLATE, self.template_params())
def _topic_config(self):
# TODO: support multiple topics
topics = [self.topic_config.topic_params()]
return topics
def _peer_score_params(self):
p = {
'Thresholds': {
'GossipThreshold': self.widgets.peer_score.gossip_threshold.value,
'PublishThreshold': self.widgets.peer_score.publish_threshold.value,
'GraylistThreshold': self.widgets.peer_score.graylist_threshold.value,
'AcceptPXThreshold': self.widgets.peer_score.acceptpx_threshold.value,
'OpportunisticGraftThreshold': self.widgets.peer_score.opportunistic_graft_threshold.value,
},
'IPColocationFactorWeight': self.widgets.peer_score.ip_colocation_weight.value,
'IPColocationFactorThreshold': self.widgets.peer_score.ip_colocation_threshold.value,
'DecayInterval': self.widgets.peer_score.decay_interval.value,
'DecayToZero': self.widgets.peer_score.decay_to_zero.value,
'RetainScore': self.widgets.peer_score.retain_score.value,
# TODO: support multiple topics
'Topics': {self.topic_config.topic_id(): self.topic_config.score_params()}
}
return p
#### widget helpers ####
def labeled(widget):
if widget.description is None or widget.description == '':
return widget
label = widget.description
widget.style.description_width = '0'
return widgets.VBox([widgets.Label(value=label), widget])
def collapsible(title, params, expanded=False):
grid = widgets.Layout(width='900px', grid_template_columns="repeat(2, 400px)")
inner = widgets.GridBox(params, layout=grid)
a = widgets.Accordion(children=[inner])
a.set_title(0, title)
a.selected_index = 0 if expanded else None
return a
def to_collapsible_sections(w, expanded=False):
# build up vbox of collapsible sections
sections = []
for name, params in w.items():
title = stringcase.sentencecase(name)
children = []
for p in params.values():
children.append(labeled(p))
sections.append(collapsible(title, children, expanded=expanded))
return widgets.VBox(sections, layout={'width': '900px'})
# sets the value of target widget to the sum of all arg widgets and updates when values change
def sum_values(target, *args):
def callback(change):
if change['name'] != 'value':
return
target.value = functools.reduce(operator.add, [a.value for a in args])
for widget in args:
widget.observe(callback)
# trigger callback to set initial value
callback({'name': 'value'})
# sets the value of target widget to the product of all arg widgets and updates when values change
def mul_values(target, *args):
def callback(change):
if change['name'] != 'value':
return
target.value = functools.reduce(operator.mul, [a.value for a in args])
for widget in args:
widget.observe(callback)
# trigger callback to set initial value
callback({'name': 'value'})
# takes a nested dict (or Bunch) whose leaves are widgets,
# and returns a dict with the same structure, but with widgets replaced with
# a snapshot of their current values
def widget_snapshot(widgets):
out = dict()
for name, val in widgets.items():
if isinstance(val, Bunch) or isinstance(val, dict):
out[name] = widget_snapshot(val)
else:
w = {'value': val.value}
out[name] = w
return out
# takes a nested dict or Bunch of widgets and the output of widget_snapshot,
# and sets the current widget values to the values from the snapshot
def apply_snapshot(widgets, snapshot):
for name, val in widgets.items():
if name not in snapshot:
continue
if isinstance(val, Bunch) or isinstance(val, dict):
apply_snapshot(val, snapshot[name])
else:
s = snapshot[name]
if 'value' in s:
val.value = s['value']

382
pubsub/test/discovery.go Normal file
View File

@ -0,0 +1,382 @@
package main
import (
"context"
"fmt"
"math/rand"
"strconv"
"strings"
"sync"
"time"
"github.com/avast/retry-go"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/peer"
swarm "github.com/libp2p/go-libp2p-swarm"
"golang.org/x/sync/errgroup"
"github.com/testground/sdk-go/runtime"
tgsync "github.com/testground/sdk-go/sync"
)
const (
PeerConnectTimeout = time.Second * 10
MaxConnectRetries = 5
)
// PeerRegistration contains the addresses, sequence numbers and node type (honest / sybil / etc)
// for each peer in the test. It is shared with every other peer using the sync service.
type PeerRegistration struct {
Info peer.AddrInfo
NType NodeType
NodeTypeSeq int64
NodeIdx int
IsPublisher bool
}
// SyncDiscovery uses the testground sync API to share PeerRegistrations for the
// local test peers and collect the info from all the other peers. It then allows
// you to connect the local peers to a subset of the test peers, using a Topology
// to control the peer selection.
type SyncDiscovery struct {
h host.Host
runenv *runtime.RunEnv
peerSubscriber *PeerSubscriber
topology Topology
nodeType NodeType
nodeTypeSeq int64
nodeIdx int
isPublisher bool
// All peers in the test
allPeers []PeerRegistration
// The peers that this node connects to
connectedLk sync.RWMutex
connected map[peer.ID]PeerRegistration
}
// A Topology filters the set of all nodes
type Topology interface {
SelectPeers(local peer.ID, remote []PeerRegistration) []PeerRegistration
}
// RandomTopology selects a subset of the total nodes at random
type RandomTopology struct {
// Count is the number of total peers to return
Count int
}
func (t RandomTopology) SelectPeers(local peer.ID, remote []PeerRegistration) []PeerRegistration {
if len(remote) == 0 || t.Count == 0 {
return []PeerRegistration{}
}
n := t.Count
if n > len(remote) {
n = len(remote)
}
indices := rand.Perm(len(remote))
out := make([]PeerRegistration, n)
for i := 0; i < n; i++ {
out[i] = remote[indices[i]]
}
return out
}
// RandomHonestTopology is a Topology that returns a subset of all non-attack nodes
type RandomHonestTopology struct {
// Count is the number of total peers to return
Count int
// PublishersOnly indicates whether to connect to publishers only or to
// both publishers and lurkers
PublishersOnly bool
}
func (t RandomHonestTopology) SelectPeers(local peer.ID, remote []PeerRegistration) []PeerRegistration {
if len(remote) == 0 {
return []PeerRegistration{}
}
filtered := make([]PeerRegistration, 0, len(remote))
for _, peer := range remote {
// Only connect to honest nodes.
// If PublishersOnly is true, only connect to Publishers
if peer.NType == NodeTypeHonest && (!t.PublishersOnly || peer.IsPublisher) {
filtered = append(filtered, peer)
}
}
return RandomTopology{t.Count}.SelectPeers(local, filtered)
}
// SinglePublisherTopology is a Topology that returns the first publisher node
type SinglePublisherTopology struct {
}
func (t SinglePublisherTopology) SelectPeers(local peer.ID, remote []PeerRegistration) []PeerRegistration {
publisher := selectSinglePublisher(remote)
if publisher != nil {
return []PeerRegistration{*publisher}
}
return []PeerRegistration{}
}
// Select the publisher with the lowest sequence number and index
func selectSinglePublisher(peers []PeerRegistration) *PeerRegistration {
lowest := int64(-1)
var lowestp PeerRegistration
for _, p := range peers {
if p.IsPublisher {
current := int64(p.NodeTypeSeq*1000000 + int64(p.NodeIdx))
if lowest < 0 || current < lowest {
lowest = current
lowestp = p
}
}
}
if lowest == -1 {
return nil
}
return &lowestp
}
// FixedTopology is defined by a topology file
type FixedTopology struct {
// def contains the definition of the topology
def *ConnectionsDef
}
func (t FixedTopology) SelectPeers(local peer.ID, remote []PeerRegistration) []PeerRegistration {
if len(remote) == 0 {
return []PeerRegistration{}
}
out := make([]PeerRegistration, 0, len(t.def.Connections))
for _, conn := range t.def.Connections {
parts := strings.Split(conn, "-")
if len(parts) != 3 {
panic(fmt.Sprintf("Badly formatted topology file"))
}
nodeType := parts[0]
nodeTypeSeq := parts[1]
nodeIdx := parts[2]
for _, p := range remote {
if nodeType == string(p.NType) && nodeTypeSeq == strconv.Itoa(int(p.NodeTypeSeq)) && nodeIdx == strconv.Itoa(int(p.NodeIdx)) {
out = append(out, p)
}
}
}
return out
}
// PeerSubscriber subscribes to peer information from all nodes in all containers.
// There is one PeerSubscriber per container (but there may be several nodes per container)
type PeerSubscriber struct {
lk sync.Mutex
peers []PeerRegistration
runenv *runtime.RunEnv
client *tgsync.Client
containerCount int
containerNodesTotal int
}
func NewPeerSubscriber(ctx context.Context, runenv *runtime.RunEnv, client *tgsync.Client, containerCount int, containerNodesTotal int) *PeerSubscriber {
return &PeerSubscriber{
runenv: runenv,
client: client,
containerCount: containerCount,
containerNodesTotal: containerNodesTotal,
}
}
var PeerRegistrationTopic = tgsync.NewTopic("pubsub-test-peers", &PeerRegistration{})
// Register node information for the local node
func (ps *PeerSubscriber) register(ctx context.Context, entry PeerRegistration) error {
if _, err := ps.client.Publish(ctx, PeerRegistrationTopic, &entry); err != nil {
return fmt.Errorf("failed to write to pubsub subtree in sync service: %w", err)
}
return nil
}
// Wait for node information from all nodes in all containers
func (ps *PeerSubscriber) waitForPeers(ctx context.Context) ([]PeerRegistration, error) {
ps.lk.Lock()
defer ps.lk.Unlock()
if ps.peers != nil {
return ps.peers, nil
}
// wait for all other peers to send their peer registration
peerCh := make(chan *PeerRegistration, 16)
ps.peers = make([]PeerRegistration, 0, ps.containerNodesTotal)
// add a random delay before subscribing, to avoid overloading the subscriber system
delay := time.Duration(rand.Intn(ps.containerCount)) * time.Millisecond
if delay > time.Second {
ps.runenv.RecordMessage("waiting for %s before subscribing", delay)
}
time.Sleep(delay)
sctx, cancelSub := context.WithCancel(ctx)
if _, err := ps.client.Subscribe(sctx, PeerRegistrationTopic, peerCh); err != nil {
cancelSub()
return nil, err
}
defer cancelSub()
start := time.Now()
ps.runenv.RecordMessage("waiting for peer information from %d peers", ps.containerNodesTotal)
for i := 0; i < ps.containerNodesTotal; i++ {
select {
case ai, ok := <-peerCh:
if !ok {
return nil, fmt.Errorf("not enough peer infos. expected %d, got %d", ps.containerNodesTotal, len(ps.peers))
}
ps.peers = append(ps.peers, *ai)
if len(ps.peers)%500 == 0 {
ps.runenv.RecordMessage("received peer information from %d of %d peers in %s", len(ps.peers), ps.containerNodesTotal, time.Since(start))
}
case <-ctx.Done():
ps.runenv.RecordMessage("context cancelled before receiving peer information from %d peers: %s", ps.containerNodesTotal, ctx.Err())
return nil, ctx.Err()
}
}
ps.runenv.RecordMessage("received peer information from %d peers in %s", len(ps.peers), time.Since(start))
return ps.peers, nil
}
func NewSyncDiscovery(h host.Host, runenv *runtime.RunEnv, peerSubscriber *PeerSubscriber,
topology Topology, nodeType NodeType, nodeTypeSeq int64, nodeIdx int, isPublisher bool) (*SyncDiscovery, error) {
return &SyncDiscovery{
h: h,
runenv: runenv,
peerSubscriber: peerSubscriber,
topology: topology,
nodeType: nodeType,
nodeTypeSeq: nodeTypeSeq,
nodeIdx: nodeIdx,
isPublisher: isPublisher,
connected: make(map[peer.ID]PeerRegistration),
}, nil
}
// Registers node and waits to collect all other nodes' registrations.
func (s *SyncDiscovery) registerAndWait(ctx context.Context) error {
// Register this node's information
localPeer := *host.InfoFromHost(s.h)
entry := PeerRegistration{
Info: localPeer,
NType: s.nodeType,
NodeTypeSeq: s.nodeTypeSeq,
NodeIdx: s.nodeIdx,
IsPublisher: s.isPublisher,
}
err := s.peerSubscriber.register(ctx, entry)
if err != nil {
return err
}
// Wait for all peers' node information
peers, err := s.peerSubscriber.waitForPeers(ctx)
if err != nil {
return err
}
// Filter out this node's information from all peers
s.allPeers = make([]PeerRegistration, 0, len(peers)-1)
for _, p := range peers {
if p.Info.ID != localPeer.ID {
s.allPeers = append(s.allPeers, p)
}
}
return nil
}
// Connect to all peers in the topology
func (s *SyncDiscovery) ConnectTopology(ctx context.Context, delay time.Duration) error {
s.runenv.RecordMessage("delay connect to peers by %s", delay)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
s.runenv.RecordMessage("connecting to peers after %s", delay)
}
selected := s.topology.SelectPeers(s.h.ID(), s.allPeers)
if len(selected) == 0 {
panic("topology selected zero peers. so lonely!!!")
}
s.connectedLk.Lock()
errgrp, ctx := errgroup.WithContext(ctx)
for _, p := range selected {
p := p
if _, ok := s.connected[p.Info.ID]; !ok {
s.connected[p.Info.ID] = p
s.runenv.RecordMessage("%s-%d-%d connecting to %s-%d-%d\n", s.nodeType, s.nodeTypeSeq, s.nodeIdx, p.NType, p.NodeTypeSeq, p.NodeIdx)
errgrp.Go(func() error {
err := s.connectWithRetry(ctx, p.Info)
if err != nil {
s.runenv.RecordMessage("error connecting libp2p host: %s", err)
}
conns := s.h.Network().ConnsToPeer(p.Info.ID)
for _, conn := range conns {
s.runenv.RecordMessage("%s-%d-%d connected to %s-%d-%d. local addr: %s remote addr: %s\n",
s.nodeType, s.nodeTypeSeq, s.nodeIdx, p.NType, p.NodeTypeSeq, p.NodeIdx,
conn.LocalMultiaddr(), conn.RemoteMultiaddr())
}
return err
})
}
}
s.connectedLk.Unlock()
return errgrp.Wait()
}
func (s *SyncDiscovery) connectWithRetry(ctx context.Context, p peer.AddrInfo) error {
return retry.Do(
func() error {
// add a random delay to each connection attempt to spread the network load
connectDelay := time.Duration(rand.Intn(1000)) * time.Millisecond
<-time.After(connectDelay)
boundedCtx, cancel := context.WithTimeout(ctx, PeerConnectTimeout)
defer cancel()
return s.h.Connect(boundedCtx, p)
},
retry.Attempts(MaxConnectRetries),
retry.OnRetry(func(n uint, err error) {
s.runenv.RecordMessage("connection attempt #%d to %s failed: %s", n, p.ID.Pretty(), err)
// clear the libp2p dial backoff for this peer, otherwise the swarm will ignore our
// dial attempt and immediately return a "dial backoff" error
if sw, ok := s.h.Network().(*swarm.Swarm); ok {
s.runenv.RecordMessage("clearing swarm dial backoff for peer %s", p.ID.Pretty())
sw.Backoff().Clear(p.ID)
}
}),
)
}
func (s *SyncDiscovery) Connected() []PeerRegistration {
s.connectedLk.RLock()
defer s.connectedLk.RUnlock()
d := make([]PeerRegistration, 0, len(s.connected))
for _, p := range s.connected {
d = append(d, p)
}
return d
}

17
pubsub/test/go.mod Normal file
View File

@ -0,0 +1,17 @@
module github.com/libp2p/test-plans/pubsub/test
go 1.13
require (
github.com/avast/retry-go v2.6.0+incompatible
github.com/gogo/protobuf v1.3.1
github.com/libp2p/go-libp2p v0.6.1
github.com/libp2p/go-libp2p-core v0.5.0
github.com/libp2p/go-libp2p-pubsub v0.2.6
github.com/libp2p/go-libp2p-swarm v0.2.2
github.com/multiformats/go-multiaddr v0.2.1
github.com/multiformats/go-multiaddr-net v0.1.3
github.com/testground/sdk-go v0.2.1
github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
)

541
pubsub/test/go.sum Normal file
View File

@ -0,0 +1,541 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/avast/retry-go v2.6.0+incompatible h1:FelcMrm7Bxacr1/RM8+/eqkDkmVN7tjlsy51dOzB3LI=
github.com/avast/retry-go v2.6.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
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/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8=
github.com/btcsuite/btcd v0.0.0-20190523000118-16327141da8c/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/davidlazar/go-crypto v0.0.0-20170701192655-dcfb0a7ac018 h1:6xT9KW8zLC5IlbaIF5Q7JNieBoACT7iW0YTxQHR0in0=
github.com/davidlazar/go-crypto v0.0.0-20170701192655-dcfb0a7ac018/go.mod h1:rQYf4tfk5sSwFsnDg3qYaBxSjsD9S8+59vW0dKUgme4=
github.com/dgraph-io/badger v1.5.5-0.20190226225317-8115aed38f8f/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ=
github.com/dgraph-io/badger v1.6.0-rc1/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs=
github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/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 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU=
github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo=
github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.4/go.mod h1:4LLaPOQwmk5z9LBgQnpkivrx8BJjUyGwTXCd5Xfj6+M=
github.com/ipfs/go-cid v0.0.5 h1:o0Ix8e/ql7Zb5UVUJEUfjsWCIY8t48++9lR8qi6oiJU=
github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog=
github.com/ipfs/go-datastore v0.0.1/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE=
github.com/ipfs/go-datastore v0.4.0/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA=
github.com/ipfs/go-datastore v0.4.1/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA=
github.com/ipfs/go-datastore v0.4.4/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA=
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ds-badger v0.0.2/go.mod h1:Y3QpeSFWQf6MopLTiZD+VT6IC1yZqaGmjvRcKeSGij8=
github.com/ipfs/go-ds-badger v0.0.5/go.mod h1:g5AuuCGmr7efyzQhLL8MzwqcauPojGPUaHzfGTzuE3s=
github.com/ipfs/go-ds-badger v0.2.1/go.mod h1:Tx7l3aTph3FMFrRS838dcSJh+jjA7cX9DrGVwx/NOwE=
github.com/ipfs/go-ds-leveldb v0.0.1/go.mod h1:feO8V3kubwsEF22n0YRQCffeb79OOYIykR4L04tMOYc=
github.com/ipfs/go-ds-leveldb v0.4.1/go.mod h1:jpbku/YqBSsBc1qgME8BkWS4AxzF2cEu1Ii2r79Hh9s=
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
github.com/ipfs/go-ipfs-util v0.0.1 h1:Wz9bL2wB2YBJqggkA4dD7oSmqB4cAnpNbGrlHJulv50=
github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc=
github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM=
github.com/ipfs/go-log v1.0.2 h1:s19ZwJxH8rPWzypjcDpqPLIyV7BnbLqvpli3iZoqYK0=
github.com/ipfs/go-log v1.0.2/go.mod h1:1MNjMxe0u6xvJZgeqbJ8vdo2TKaGwZ1a0Bpza+sr2Sk=
github.com/ipfs/go-log/v2 v2.0.2 h1:xguurydRdfKMJjKyxNXNU8lYP0VZH1NUwJRwUorjuEw=
github.com/ipfs/go-log/v2 v2.0.2/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0=
github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc=
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v1.0.1 h1:i0LektDkO1QlrTm/cSuP+PyBCDnYvjPLGl4LdWEMiaA=
github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jbenet/go-cienv v0.0.0-20150120210510-1bb1476777ec/go.mod h1:rGaEvXB4uRSZMmzKNLoXvTu1sfx+1kv/DojUlPrSZGs=
github.com/jbenet/go-cienv v0.1.0 h1:Vc/s0QbQtoxX8MwwSLWWh+xNNZvM3Lw7NsTcHrvvhMc=
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
github.com/jbenet/go-temp-err-catcher v0.0.0-20150120210811-aac704a3f4f2 h1:vhC1OXXiT9R2pczegwz6moDvuRpggaroAXhPIseh57A=
github.com/jbenet/go-temp-err-catcher v0.0.0-20150120210811-aac704a3f4f2/go.mod h1:8GXXJV31xl8whumTzdZsTt3RnUIiPqzkyf7mxToRCMs=
github.com/jbenet/goprocess v0.0.0-20160826012719-b497e2f366b8/go.mod h1:Ly/wlsjFq/qrU3Rar62tu1gASgGw6chQbSh/XgIIXCY=
github.com/jbenet/goprocess v0.1.3 h1:YKyIEECS/XvcfHtBzxtjBBbWK+MbvA6dG8ASiqwvr10=
github.com/jbenet/goprocess v0.1.3/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d h1:68u9r4wEvL3gYg2jvAOgROwZ3H+Y3hIDk4tbbmIjcYQ=
github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/libp2p/go-addr-util v0.0.1 h1:TpTQm9cXVRVSKsYbgQ7GKc3KbbHVTnbostgGaDEP+88=
github.com/libp2p/go-addr-util v0.0.1/go.mod h1:4ac6O7n9rIAKB1dnd+s8IbbMXkt+oBpzX4/+RACcnlQ=
github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ=
github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs=
github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
github.com/libp2p/go-conn-security-multistream v0.1.0 h1:aqGmto+ttL/uJgX0JtQI0tD21CIEy5eYd1Hlp0juHY0=
github.com/libp2p/go-conn-security-multistream v0.1.0/go.mod h1:aw6eD7LOsHEX7+2hJkDxw1MteijaVcI+/eP2/x3J1xc=
github.com/libp2p/go-eventbus v0.1.0 h1:mlawomSAjjkk97QnYiEmHsLu7E136+2oCWSHRUvMfzQ=
github.com/libp2p/go-eventbus v0.1.0/go.mod h1:vROgu5cs5T7cv7POWlWxBaVLxfSegC5UGQf8A2eEmx4=
github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZxBdp967ls1g+k8=
github.com/libp2p/go-flow-metrics v0.0.3 h1:8tAs/hSdNvUiLgtlSy3mxwxWP4I9y/jlkPFT7epKdeM=
github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs=
github.com/libp2p/go-libp2p v0.6.1 h1:mxabyJf4l6AmotDOKObwSfBNBWjL5VYXysVFLUMAuB8=
github.com/libp2p/go-libp2p v0.6.1/go.mod h1:CTFnWXogryAHjXAKEbOf1OWY+VeAP3lDMZkfEI5sT54=
github.com/libp2p/go-libp2p-autonat v0.1.1 h1:WLBZcIRsjZlWdAZj9CiBSvU2wQXoUOiS1Zk1tM7DTJI=
github.com/libp2p/go-libp2p-autonat v0.1.1/go.mod h1:OXqkeGOY2xJVWKAGV2inNF5aKN/djNA3fdpCWloIudE=
github.com/libp2p/go-libp2p-blankhost v0.1.1/go.mod h1:pf2fvdLJPsC1FsVrNP3DUUvMzUts2dsLLBEpo1vW1ro=
github.com/libp2p/go-libp2p-blankhost v0.1.4 h1:I96SWjR4rK9irDHcHq3XHN6hawCRTPUADzkJacgZLvk=
github.com/libp2p/go-libp2p-blankhost v0.1.4/go.mod h1:oJF0saYsAXQCSfDq254GMNmLNz6ZTHTOvtF4ZydUvwU=
github.com/libp2p/go-libp2p-circuit v0.1.4 h1:Phzbmrg3BkVzbqd4ZZ149JxCuUWu2wZcXf/Kr6hZJj8=
github.com/libp2p/go-libp2p-circuit v0.1.4/go.mod h1:CY67BrEjKNDhdTk8UgBX1Y/H5c3xkAcs3gnksxY7osU=
github.com/libp2p/go-libp2p-core v0.0.1/go.mod h1:g/VxnTZ/1ygHxH3dKok7Vno1VfpvGcGip57wjTU4fco=
github.com/libp2p/go-libp2p-core v0.0.4/go.mod h1:jyuCQP356gzfCFtRKyvAbNkyeuxb7OlyhWZ3nls5d2I=
github.com/libp2p/go-libp2p-core v0.2.0/go.mod h1:X0eyB0Gy93v0DZtSYbEM7RnMChm9Uv3j7yRXjO77xSI=
github.com/libp2p/go-libp2p-core v0.2.2/go.mod h1:8fcwTbsG2B+lTgRJ1ICZtiM5GWCWZVoVrLaDRvIRng0=
github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g=
github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw=
github.com/libp2p/go-libp2p-core v0.3.1/go.mod h1:thvWy0hvaSBhnVBaW37BvzgVV68OUhgJJLAa6almrII=
github.com/libp2p/go-libp2p-core v0.4.0/go.mod h1:49XGI+kc38oGVwqSBhDEwytaAxgZasHhFfQKibzTls0=
github.com/libp2p/go-libp2p-core v0.5.0 h1:FBQ1fpq2Fo/ClyjojVJ5AKXlKhvNc/B6U0O+7AN1ffE=
github.com/libp2p/go-libp2p-core v0.5.0/go.mod h1:49XGI+kc38oGVwqSBhDEwytaAxgZasHhFfQKibzTls0=
github.com/libp2p/go-libp2p-crypto v0.1.0/go.mod h1:sPUokVISZiy+nNuTTH/TY+leRSxnFj/2GLjtOTW90hI=
github.com/libp2p/go-libp2p-discovery v0.2.0 h1:1p3YSOq7VsgaL+xVHPi8XAmtGyas6D2J6rWBEfz/aiY=
github.com/libp2p/go-libp2p-discovery v0.2.0/go.mod h1:s4VGaxYMbw4+4+tsoQTqh7wfxg97AEdo4GYBt6BadWg=
github.com/libp2p/go-libp2p-loggables v0.1.0 h1:h3w8QFfCt2UJl/0/NW4K829HX/0S4KD31PQ7m8UXXO8=
github.com/libp2p/go-libp2p-loggables v0.1.0/go.mod h1:EyumB2Y6PrYjr55Q3/tiJ/o3xoDasoRYM7nOzEpoa90=
github.com/libp2p/go-libp2p-mplex v0.2.0/go.mod h1:Ejl9IyjvXJ0T9iqUTE1jpYATQ9NM3g+OtR+EMMODbKo=
github.com/libp2p/go-libp2p-mplex v0.2.1/go.mod h1:SC99Rxs8Vuzrf/6WhmH41kNn13TiYdAWNYHrwImKLnE=
github.com/libp2p/go-libp2p-mplex v0.2.2 h1:+Ld7YDAfVERQ0E+qqjE7o6fHwKuM0SqTzYiwN1lVVSA=
github.com/libp2p/go-libp2p-mplex v0.2.2/go.mod h1:74S9eum0tVQdAfFiKxAyKzNdSuLqw5oadDq7+L/FELo=
github.com/libp2p/go-libp2p-nat v0.0.5 h1:/mH8pXFVKleflDL1YwqMg27W9GD8kjEx7NY0P6eGc98=
github.com/libp2p/go-libp2p-nat v0.0.5/go.mod h1:1qubaE5bTZMJE+E/uu2URroMbzdubFz1ChgiN79yKPE=
github.com/libp2p/go-libp2p-netutil v0.1.0 h1:zscYDNVEcGxyUpMd0JReUZTrpMfia8PmLKcKF72EAMQ=
github.com/libp2p/go-libp2p-netutil v0.1.0/go.mod h1:3Qv/aDqtMLTUyQeundkKsA+YCThNdbQD54k3TqjpbFU=
github.com/libp2p/go-libp2p-peer v0.2.0/go.mod h1:RCffaCvUyW2CJmG2gAWVqwePwW7JMgxjsHm7+J5kjWY=
github.com/libp2p/go-libp2p-peerstore v0.1.0/go.mod h1:2CeHkQsr8svp4fZ+Oi9ykN1HBb6u0MOvdJ7YIsmcwtY=
github.com/libp2p/go-libp2p-peerstore v0.1.3/go.mod h1:BJ9sHlm59/80oSkpWgr1MyY1ciXAXV397W6h1GH/uKI=
github.com/libp2p/go-libp2p-peerstore v0.2.0 h1:XcgJhI8WyUOCbHyRLNEX5542YNj8hnLSJ2G1InRjDhk=
github.com/libp2p/go-libp2p-peerstore v0.2.0/go.mod h1:N2l3eVIeAitSg3Pi2ipSrJYnqhVnMNQZo9nkSCuAbnQ=
github.com/libp2p/go-libp2p-pnet v0.2.0 h1:J6htxttBipJujEjz1y0a5+eYoiPcFHhSYHH6na5f0/k=
github.com/libp2p/go-libp2p-pnet v0.2.0/go.mod h1:Qqvq6JH/oMZGwqs3N1Fqhv8NVhrdYcO0BW4wssv21LA=
github.com/libp2p/go-libp2p-pubsub v0.2.6 h1:ypZaukCFrtD8cNeeb9nnWG4MD2Y1T0p22aQ+f7FKJig=
github.com/libp2p/go-libp2p-pubsub v0.2.6/go.mod h1:5jEp7R3ItQ0pgcEMrPZYE9DQTg/H3CTc7Mu1j2G4Y5o=
github.com/libp2p/go-libp2p-secio v0.1.0/go.mod h1:tMJo2w7h3+wN4pgU2LSYeiKPrfqBgkOsdiKK77hE7c8=
github.com/libp2p/go-libp2p-secio v0.2.0/go.mod h1:2JdZepB8J5V9mBp79BmwsaPQhRPNN2NrnB2lKQcdy6g=
github.com/libp2p/go-libp2p-secio v0.2.1 h1:eNWbJTdyPA7NxhP7J3c5lT97DC5d+u+IldkgCYFTPVA=
github.com/libp2p/go-libp2p-secio v0.2.1/go.mod h1:cWtZpILJqkqrSkiYcDBh5lA3wbT2Q+hz3rJQq3iftD8=
github.com/libp2p/go-libp2p-swarm v0.1.0/go.mod h1:wQVsCdjsuZoc730CgOvh5ox6K8evllckjebkdiY5ta4=
github.com/libp2p/go-libp2p-swarm v0.2.2 h1:T4hUpgEs2r371PweU3DuH7EOmBIdTBCwWs+FLcgx3bQ=
github.com/libp2p/go-libp2p-swarm v0.2.2/go.mod h1:fvmtQ0T1nErXym1/aa1uJEyN7JzaTNyBcHImCxRpPKU=
github.com/libp2p/go-libp2p-testing v0.0.2/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
github.com/libp2p/go-libp2p-testing v0.0.3/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
github.com/libp2p/go-libp2p-testing v0.0.4/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E=
github.com/libp2p/go-libp2p-testing v0.1.0/go.mod h1:xaZWMJrPUM5GlDBxCeGUi7kI4eqnjVyavGroI2nxEM0=
github.com/libp2p/go-libp2p-testing v0.1.1 h1:U03z3HnGI7Ni8Xx6ONVZvUFOAzWYmolWf5W5jAOPNmU=
github.com/libp2p/go-libp2p-testing v0.1.1/go.mod h1:xaZWMJrPUM5GlDBxCeGUi7kI4eqnjVyavGroI2nxEM0=
github.com/libp2p/go-libp2p-transport-upgrader v0.1.1/go.mod h1:IEtA6or8JUbsV07qPW4r01GnTenLW4oi3lOPbUMGJJA=
github.com/libp2p/go-libp2p-transport-upgrader v0.2.0 h1:5EhPgQhXZNyfL22ERZTUoVp9UVVbNowWNVtELQaKCHk=
github.com/libp2p/go-libp2p-transport-upgrader v0.2.0/go.mod h1:mQcrHj4asu6ArfSoMuyojOdjx73Q47cYD7s5+gZOlns=
github.com/libp2p/go-libp2p-yamux v0.2.0/go.mod h1:Db2gU+XfLpm6E4rG5uGCFX6uXA8MEXOxFcRoXUODaK8=
github.com/libp2p/go-libp2p-yamux v0.2.5 h1:MuyItOqz03oi8npvjgMJxgnhllJLZnO/dKVOpTZ9+XI=
github.com/libp2p/go-libp2p-yamux v0.2.5/go.mod h1:Zpgj6arbyQrmZ3wxSZxfBmbdnWtbZ48OpsfmQVTErwA=
github.com/libp2p/go-maddr-filter v0.0.4/go.mod h1:6eT12kSQMA9x2pvFQa+xesMKUBlj9VImZbj3B9FBH/Q=
github.com/libp2p/go-maddr-filter v0.0.5 h1:CW3AgbMO6vUvT4kf87y4N+0P8KUl2aqLYhrGyDUbLSg=
github.com/libp2p/go-maddr-filter v0.0.5/go.mod h1:Jk+36PMfIqCJhAnaASRH83bdAvfDRp/w6ENFaC9bG+M=
github.com/libp2p/go-mplex v0.0.3/go.mod h1:pK5yMLmOoBR1pNCqDlA2GQrdAVTMkqFalaTWe7l4Yd0=
github.com/libp2p/go-mplex v0.1.0/go.mod h1:SXgmdki2kwCUlCCbfGLEgHjC4pFqhTp0ZoV6aiKgxDU=
github.com/libp2p/go-mplex v0.1.1 h1:huPH/GGRJzmsHR9IZJJsrSwIM5YE2gL4ssgl1YWb/ps=
github.com/libp2p/go-mplex v0.1.1/go.mod h1:Xgz2RDCi3co0LeZfgjm4OgUF15+sVR8SRcu3SFXI1lk=
github.com/libp2p/go-msgio v0.0.2/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ=
github.com/libp2p/go-msgio v0.0.4 h1:agEFehY3zWJFUHK6SEMR7UYmk2z6kC3oeCM7ybLhguA=
github.com/libp2p/go-msgio v0.0.4/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ=
github.com/libp2p/go-nat v0.0.4 h1:KbizNnq8YIf7+Hn7+VFL/xE0eDrkPru2zIO9NMwL8UQ=
github.com/libp2p/go-nat v0.0.4/go.mod h1:Nmw50VAvKuk38jUBcmNh6p9lUJLoODbJRvYAa/+KSDo=
github.com/libp2p/go-openssl v0.0.2/go.mod h1:v8Zw2ijCSWBQi8Pq5GAixw6DbFfa9u6VIYDXnvOXkc0=
github.com/libp2p/go-openssl v0.0.3/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc=
github.com/libp2p/go-openssl v0.0.4 h1:d27YZvLoTyMhIN4njrkr8zMDOM4lfpHIp6A+TK9fovg=
github.com/libp2p/go-openssl v0.0.4/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc=
github.com/libp2p/go-reuseport v0.0.1 h1:7PhkfH73VXfPJYKQ6JwS5I/eVcoyYi9IMNGc6FWpFLw=
github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA=
github.com/libp2p/go-reuseport-transport v0.0.2 h1:WglMwyXyBu61CMkjCCtnmqNqnjib0GIEjMiHTwR/KN4=
github.com/libp2p/go-reuseport-transport v0.0.2/go.mod h1:YkbSDrvjUVDL6b8XqriyA20obEtsW9BLkuOUyQAOCbs=
github.com/libp2p/go-stream-muxer v0.0.1/go.mod h1:bAo8x7YkSpadMTbtTaxGVHWUQsR/l5MEaHbKaliuT14=
github.com/libp2p/go-stream-muxer-multistream v0.2.0 h1:714bRJ4Zy9mdhyTLJ+ZKiROmAFwUHpeRidG+q7LTQOg=
github.com/libp2p/go-stream-muxer-multistream v0.2.0/go.mod h1:j9eyPol/LLRqT+GPLSxvimPhNph4sfYfMoDPd7HkzIc=
github.com/libp2p/go-tcp-transport v0.1.0/go.mod h1:oJ8I5VXryj493DEJ7OsBieu8fcg2nHGctwtInJVpipc=
github.com/libp2p/go-tcp-transport v0.1.1 h1:yGlqURmqgNA2fvzjSgZNlHcsd/IulAnKM8Ncu+vlqnw=
github.com/libp2p/go-tcp-transport v0.1.1/go.mod h1:3HzGvLbx6etZjnFlERyakbaYPdfjg2pWP97dFZworkY=
github.com/libp2p/go-ws-transport v0.2.0 h1:MJCw2OrPA9+76YNRvdo1wMnSOxb9Bivj6sVFY1Xrj6w=
github.com/libp2p/go-ws-transport v0.2.0/go.mod h1:9BHJz/4Q5A9ludYWKoGCFC5gUElzlHoKzu0yY9p/klM=
github.com/libp2p/go-yamux v1.2.2/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow=
github.com/libp2p/go-yamux v1.3.3 h1:mWuzZRCAeTBFdynLlsYgA/EIeMOLr8XY04wa52NRhsE=
github.com/libp2p/go-yamux v1.3.3/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
github.com/minio/sha256-simd v0.0.0-20190328051042-05b4dd3047e5/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
github.com/minio/sha256-simd v0.1.0/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
github.com/mr-tron/base58 v1.1.1/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
github.com/multiformats/go-multiaddr v0.0.1/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.0.2/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.0.4/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.1.0/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4=
github.com/multiformats/go-multiaddr v0.2.1 h1:SgG/cw5vqyB5QQe5FPe2TqggU9WtrA9X4nZw7LlVqOI=
github.com/multiformats/go-multiaddr v0.2.1/go.mod h1:s/Apk6IyxfvMjDafnhJgJ3/46z7tZ04iMk5wP4QMGGE=
github.com/multiformats/go-multiaddr-dns v0.0.1/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
github.com/multiformats/go-multiaddr-dns v0.0.2/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
github.com/multiformats/go-multiaddr-dns v0.2.0 h1:YWJoIDwLePniH7OU5hBnDZV6SWuvJqJ0YtN6pLeH9zA=
github.com/multiformats/go-multiaddr-dns v0.2.0/go.mod h1:TJ5pr5bBO7Y1B18djPuRsVkduhQH2YqYSbxWJzYGdK0=
github.com/multiformats/go-multiaddr-fmt v0.0.1/go.mod h1:aBYjqL4T/7j4Qx+R73XSv/8JsgnRFlf0w2KGLCmXl3Q=
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multiaddr-net v0.0.1/go.mod h1:nw6HSxNmCIQH27XPGBuX+d1tnvM7ihcFwHMSstNAVUU=
github.com/multiformats/go-multiaddr-net v0.1.0/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
github.com/multiformats/go-multiaddr-net v0.1.1/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
github.com/multiformats/go-multiaddr-net v0.1.2/go.mod h1:QsWt3XK/3hwvNxZJp92iMQKME1qHfpYmyIjFVsSOY6Y=
github.com/multiformats/go-multiaddr-net v0.1.3 h1:q/IYAvoPKuRzGeERn3uacWgm0LIWkLZBAvO5DxSzq3g=
github.com/multiformats/go-multiaddr-net v0.1.3/go.mod h1:ilNnaM9HbmVFqsb/qcNysjCu4PVONlrBZpHIrw/qQuA=
github.com/multiformats/go-multibase v0.0.1 h1:PN9/v21eLywrFWdFNsFKaU04kLJzuYzmrJR+ubhT9qA=
github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs=
github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U=
github.com/multiformats/go-multihash v0.0.5/go.mod h1:lt/HCbqlQwlPBz7lv0sQCdtfcMtlJvakRUn/0Ual8po=
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/BhLEy87zidlc=
github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
github.com/multiformats/go-multistream v0.1.0/go.mod h1:fJTiDfXJVmItycydCnNx4+wSzZ5NwG2FEVAI30fiovg=
github.com/multiformats/go-multistream v0.1.1 h1:JlAdpIFhBhGRLxe9W6Om0w++Gd6KMWoFPZL/dEnm9nI=
github.com/multiformats/go-multistream v0.1.1/go.mod h1:KmHZ40hzVxiaiwlj3MEbYgK9JFk2/9UktWZAF54Du38=
github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/multiformats/go-varint v0.0.2/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg=
github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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 v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smola/gocompat v0.2.0/go.mod h1:1B0MlxbmoZNo3h8guHp8HztB3BSYR5itql9qtVc0ypY=
github.com/spacemonkeygo/openssl v0.0.0-20181017203307-c2dcc5cca94a/go.mod h1:7AyxJNCJ7SBZ1MfVQCWD6Uqo2oubI2Eq2y2eqf+A5r0=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/testground/sdk-go v0.2.1 h1:/YLO5ZY084tMyUckkapOcKVZEdPrbJM2b010ficJq7M=
github.com/testground/sdk-go v0.2.1/go.mod h1:3auzMDXaoK7NQ+CLQS3pqp4hmREECWO9V+TJi/IWmms=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM=
github.com/whyrusleeping/go-logging v0.0.1/go.mod h1:lDPYj54zutzG1XYfHAhcc7oNXEburHQBn+Iqd4yS4vE=
github.com/whyrusleeping/mafmt v1.2.8 h1:TCghSl5kkwEE0j+sU/gudyhVMRlpBin8fMBBHg59EbA=
github.com/whyrusleeping/mafmt v1.2.8/go.mod h1:faQJFPbLSxzD9xpA02ttW/tS9vZykNvXwGvqIpk20FA=
github.com/whyrusleeping/mdns v0.0.0-20190826153040-b9b60ed33aa9/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4=
github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7 h1:E9S12nwJwEOXe2d6gT6qxdvqMnNq+VnSsKPgm2ZZNds=
github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7/go.mod h1:X2c0RVCI1eSUFI8eLcY3c0423ykwiUdxLJtkDvruhjI=
github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee h1:lYbXeSvJi5zk5GLKVuid9TVjS9a0OmLIDKTfoZBL6Ow=
github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee/go.mod h1:m2aV4LZI4Aez7dP5PMyVKEHhUyEJ/RjmPEDOpDvudHg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190225124518-7f87c0fbb88b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
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-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/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-20190227160552-c95aed5357e7/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191112182307-2180aed22343 h1:00ohfJ4K98s3m6BGUoBd8nyfp4Yl0GoIKvw5abItTjI=
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
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-20190227155943-e225da77a7e6/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-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181130052023-1c3d964395ce/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-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f h1:kDxGY2VmgABOe55qheT/TFqUMtcTHnomIPS1iv3G4Ms=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8=
gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE=
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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

24
pubsub/test/main.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"errors"
"fmt"
"github.com/testground/sdk-go/runtime"
)
func main() {
runtime.Invoke(run)
}
// Pick a different example function to run
// depending on the name of the test case.
func run(runenv *runtime.RunEnv) error {
switch c := runenv.TestCase; c {
case "evaluate":
return RunSimulation(runenv)
default:
msg := fmt.Sprintf("Unknown Testcase %s", c)
return errors.New(msg)
}
}

66
pubsub/test/manifest.toml Normal file
View File

@ -0,0 +1,66 @@
name = "pubsub"
[defaults]
builder = "exec:go"
runner = "local:exec"
[builders."docker:go"]
enabled = true
go_version = "1.14"
module_path = "github.com/libp2p/test-plans/pubsub/test"
exec_pkg = "."
go_ipfs_version = "0.4.22"
[builders."exec:go"]
enabled = true
module_path = "github.com/libp2p/test-plans/pubsub/test"
exec_pkg = "."
[runners."local:docker"]
enabled = true
[runners."local:exec"]
enabled = true
[runners."cluster:k8s"]
enabled = true
[[testcases]]
name = "evaluate"
instances = { min = 1, max = 2000, default = 10 }
[testcases.params]
# params with type "duration" must be parseable by time.ParseDuration, e.g. 2m or 30s
# params with type "size" must be parseable by https://godoc.org/github.com/dustin/go-humanize#ParseBytes, e.g. "1kb"
## global params
t_heartbeat = { type = "duration", desc = "Interval between emiting maintenance messages", default="1s" }
t_heartbeat_initial_delay = { type = "duration", desc = "Delay before starting hearbeat", default="100ms" }
t_setup = { type = "duration", desc = "Upper bound on expected time period for waiting for all peers to register etc", default="1m" }
t_run = { type = "duration", desc = "Time to run the simulation", default="2m" }
t_warm = { type = "duration", desc = "Time to wait for nodes to establish connections before beginning publishing", default="5s" }
t_cool = { type = "duration", desc = "Time to wait after test execution for straggling publishers, etc.", default="10s" }
topics = { type = "json", desc = "json array of TopicConfig objects." }
score_params = { type = "json", desc = "a json ScoreParams object (see params.go). ignored unless hardened_api build flag is set."}
full_traces = { type = "bool", desc = "if true, collect full pubsub protobuf trace events, in addition to aggregate metrics", default="false" }
validate_queue_size = { type = "int", desc = "Size of pubsub validation queue", default=0 }
outbound_queue_size = { type = "int", desc = "Size of pubsub outbound queue", default=0 }
t_latency = { type = "duration", desc = "Network latency between nodes", default="5ms" }
t_latency_max = { type = "duration", desc = "If supplied, latency is between t_latency and t_latency_max", default="50ms" }
jitter_pct = { type = "int", desc = "Jitter in latency", default=10 }
bandwidth_mb = { type = "int", desc = "Bandwidth in MiB", default=10240 }
topology = { type = "string", desc = "topology in json format" }
degree = { type = "int", desc = "the number of nodes to connect to", default=20 }
n_container_nodes_total = { type = "int", desc = "the number of total nodes including multiple nodes per container", default=1 }
n_nodes_per_container = { type = "int", desc = "the number of nodes to start up in each container", default=1 }
## pubsub node config
publisher = { type = "bool", desc = "if true, this instance should publish to subscribed topics instead of lurking", default=false }
flood_publishing = { type = "bool", desc = "if true, nodes will flood when publishing their own messages. only applies to hardening branch", default=false }
t_score_inspect_period = { type = "duration", desc = "Interval between printing peer scores", default="0" }
overlay_d = { type = "int", desc = "the number of nodes gossipsub tries to stay connected to", default=-1 }
overlay_dlo = { type = "int", desc = "the low watermark of overlay_d", default=-1 }
overlay_dhi = { type = "int", desc = "the high watermark of overlay_d", default=-1 }
overlay_dscore = { type = "int", desc = "the number of peers to keep by score", default=-1 }
overlay_dlazy = { type = "int", desc = "degree for gossip nodes", default=-1 }
gossip_factor = { type = "float", desc = "gossip factor", default=0.25 }
opportunistic_graft_ticks = { type = "int", desc = "Number of heartbeat ticks for attempting opportunistic grafting", default=60 }

55
pubsub/test/net.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"context"
"math/rand"
"time"
"github.com/testground/sdk-go/network"
"github.com/testground/sdk-go/runtime"
)
// setupNetwork instructs the sidecar (if enabled) to setup the network for this
// test case.
func setupNetwork(ctx context.Context, runenv *runtime.RunEnv, netParams NetworkParams, netclient *network.Client) error {
if !runenv.TestSidecar {
return nil
}
// Wait for the network to be initialized.
runenv.RecordMessage("Waiting for network initialization")
err := netclient.WaitNetworkInitialized(ctx)
if err != nil {
return err
}
runenv.RecordMessage("Network init complete")
latency := netParams.latency
if netParams.latencyMax > 0 {
// If a maximum latency is supplied, choose a random latency between
// latency and max latency
latency += time.Duration(rand.Float64() * float64(netParams.latencyMax-latency))
}
config := &network.Config{
Network: "default",
Enable: true,
Default: network.LinkShape{
Latency: latency,
Bandwidth: uint64(netParams.bandwidthMB) * 1024 * 1024,
Jitter: (time.Duration(netParams.jitterPct) * netParams.latency) / 100,
},
CallbackState: "network-configured",
}
// random delay to avoid overloading weave (we hope)
delay := time.Duration(rand.Intn(1000)) * time.Millisecond
<-time.After(delay)
err = netclient.ConfigureNetwork(ctx, config)
if err != nil {
return err
}
runenv.RecordMessage("egress: %s latency (%d%% jitter) and %dMB bandwidth", netParams.latency, netParams.jitterPct, netParams.bandwidthMB)
return nil
}

324
pubsub/test/node.go Normal file
View File

@ -0,0 +1,324 @@
package main
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"sync"
"time"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/peer"
"github.com/testground/sdk-go/runtime"
pubsub "github.com/libp2p/go-libp2p-pubsub"
)
type PubsubNodeConfig struct {
// topics to join when node starts
Topics []TopicConfig
// whether we're a publisher or a lurker
Publisher bool
// pubsub event tracer
Tracer pubsub.EventTracer
// Test instance identifier
Seq int64
// How long to wait after connecting to bootstrap peers before publishing
Warmup time.Duration
// How long to wait for cooldown
Cooldown time.Duration
// Gossipsub heartbeat params
Heartbeat HeartbeatParams
// whether to flood the network when publishing our own messages.
// Ignored unless hardening_api build tag is present.
FloodPublishing bool
// Params for peer scoring function. Ignored unless hardening_api build tag is present.
PeerScoreParams ScoreParams
OverlayParams OverlayParams
// Params for inspecting the scoring values.
PeerScoreInspect InspectParams
// Size of the pubsub validation queue.
ValidateQueueSize int
// Size of the pubsub outbound queue.
OutboundQueueSize int
// Heartbeat tics for opportunistic grafting
OpportunisticGraftTicks int
}
type InspectParams struct {
// The callback function that is called with the peer scores
Inspect func(map[peer.ID]float64)
// The interval between calling Inspect (defaults to zero: dont inspect).
Period time.Duration
}
type topicState struct {
cfg TopicConfig
nMessages int64
topic *pubsub.Topic
sub *pubsub.Subscription
pubTicker *time.Ticker
done chan struct{}
}
type PubsubNode struct {
cfg PubsubNodeConfig
ctx context.Context
shutdown func()
runenv *runtime.RunEnv
h host.Host
ps *pubsub.PubSub
lk sync.RWMutex
topics map[string]*topicState
pubwg sync.WaitGroup
}
// NewPubsubNode prepares the given Host to act as an honest pubsub node using the provided PubsubNodeConfig.
// The returned PubsubNode will not start immediately; call Run to begin the test behavior.
func NewPubsubNode(runenv *runtime.RunEnv, ctx context.Context, h host.Host, cfg PubsubNodeConfig) (*PubsubNode, error) {
opts, err := pubsubOptions(cfg)
if err != nil {
return nil, err
}
// Set the heartbeat initial delay and interval
pubsub.GossipSubHeartbeatInitialDelay = cfg.Heartbeat.InitialDelay
pubsub.GossipSubHeartbeatInterval = cfg.Heartbeat.Interval
ps, err := pubsub.NewGossipSub(ctx, h, opts...)
if err != nil {
return nil, fmt.Errorf("error making new gossipsub: %s", err)
}
ctx, cancel := context.WithCancel(ctx)
p := PubsubNode{
cfg: cfg,
ctx: ctx,
shutdown: cancel,
runenv: runenv,
h: h,
ps: ps,
topics: make(map[string]*topicState),
}
return &p, nil
}
func (p *PubsubNode) log(msg string, args ...interface{}) {
id := p.h.ID().Pretty()
idSuffix := id[len(id)-8:]
prefix := fmt.Sprintf("[honest %d %s] ", p.cfg.Seq, idSuffix)
p.runenv.RecordMessage(prefix+msg, args...)
}
func (p *PubsubNode) Run(runtime time.Duration, waitForReadyStateThenConnectAsync func(context.Context) error) error {
defer func() {
// end subscription goroutines before exit
for _, ts := range p.topics {
ts.done <- struct{}{}
}
p.shutdown()
}()
// Wait for all nodes to be in the ready state (including attack nodes, if any)
// then start connecting (asynchronously)
if err := waitForReadyStateThenConnectAsync(p.ctx); err != nil {
return err
}
// join initial topics
p.runenv.RecordMessage("Joining initial topics")
for _, t := range p.cfg.Topics {
go p.joinTopic(t, runtime)
}
// wait for warmup time to expire
p.runenv.RecordMessage("Wait for %s warmup time", p.cfg.Warmup)
select {
case <-time.After(p.cfg.Warmup):
case <-p.ctx.Done():
return p.ctx.Err()
}
// ensure we have at least enough peers to fill a mesh after warmup period
npeers := len(p.h.Network().Peers())
if npeers < pubsub.GossipSubD {
panic(fmt.Errorf("not enough peers after warmup period. Need at least D=%d, have %d", pubsub.GossipSubD, npeers))
}
// block until complete
p.runenv.RecordMessage("Wait for %s run time", runtime)
select {
case <-time.After(runtime):
case <-p.ctx.Done():
return p.ctx.Err()
}
// if we're publishing, wait until we've sent all our messages or the context expires
if p.cfg.Publisher {
donech := make(chan struct{}, 1)
go func() {
p.pubwg.Wait()
donech <- struct{}{}
}()
select {
case <-donech:
case <-p.ctx.Done():
return p.ctx.Err()
}
}
p.runenv.RecordMessage("Run time complete, cooling down for %s", p.cfg.Cooldown)
select {
case <-time.After(p.cfg.Cooldown):
case <-p.ctx.Done():
return p.ctx.Err()
}
p.runenv.RecordMessage("Cool down complete")
return nil
}
func (p *PubsubNode) joinTopic(t TopicConfig, runtime time.Duration) {
p.lk.Lock()
defer p.lk.Unlock()
publishInterval := time.Duration(float64(t.MessageRate.Interval) / t.MessageRate.Quantity)
totalMessages := int64(runtime / publishInterval)
if p.cfg.Publisher {
p.log("publishing to topic %s. message_rate: %.2f/%ds, publishInterval %dms, msg size %d bytes. total expected messages: %d",
t.Id, t.MessageRate.Quantity, t.MessageRate.Interval/time.Second, publishInterval/time.Millisecond, t.MessageSize, totalMessages)
} else {
p.log("joining topic %s as a lurker", t.Id)
}
if _, ok := p.topics[t.Id]; ok {
// already joined, ignore
return
}
topic, err := p.ps.Join(t.Id)
if err != nil {
p.log("error joining topic %s: %s", t.Id, err)
return
}
sub, err := topic.Subscribe()
if err != nil {
p.log("error subscribing to topic %s: %s", t.Id, err)
return
}
ts := topicState{
cfg: t,
topic: topic,
sub: sub,
nMessages: totalMessages,
done: make(chan struct{}, 1),
}
p.topics[t.Id] = &ts
go p.consumeTopic(&ts)
if !p.cfg.Publisher {
return
}
go func() {
p.runenv.RecordMessage("Wait for %s warmup time before starting publisher", p.cfg.Warmup)
select {
case <-time.After(p.cfg.Warmup):
case <-p.ctx.Done():
p.runenv.RecordMessage("Context done before warm up time in publisher: %s", p.ctx.Err())
return
}
p.runenv.RecordMessage("Starting publisher with %s publish interval", publishInterval)
ts.pubTicker = time.NewTicker(publishInterval)
p.publishLoop(&ts)
}()
}
func (p *PubsubNode) makeMessage(seq int64, size uint64) ([]byte, error) {
type msg struct {
sender string
seq int64
data []byte
}
data := make([]byte, size)
rand.Read(data)
m := msg{sender: p.h.ID().Pretty(), seq: seq, data: data}
return json.Marshal(m)
}
func (p *PubsubNode) sendMsg(seq int64, ts *topicState) {
msg, err := p.makeMessage(seq, uint64(ts.cfg.MessageSize))
if err != nil {
p.log("error making message for topic %s: %s", ts.cfg.Id, err)
return
}
err = ts.topic.Publish(p.ctx, msg)
if err != nil && err != context.Canceled {
p.log("error publishing to %s: %s", ts.cfg.Id, err)
return
}
}
func (p *PubsubNode) publishLoop(ts *topicState) {
var counter int64
p.pubwg.Add(1)
defer p.pubwg.Done()
for {
select {
case <-ts.done:
return
case <-p.ctx.Done():
return
case <-ts.pubTicker.C:
go p.sendMsg(counter, ts)
counter++
if counter > ts.nMessages {
ts.pubTicker.Stop()
return
}
}
}
}
func (p *PubsubNode) consumeTopic(ts *topicState) {
for {
_, err := ts.sub.Next(p.ctx)
if err != nil && err != context.Canceled {
p.log("error reading from %s: %s", ts.cfg.Id, err)
return
}
//p.log("got message on topic %s from %s\n", ts.cfg.Id, msg.ReceivedFrom.Pretty())
select {
case <-ts.done:
return
case <-p.ctx.Done():
return
default:
continue
}
}
}

38
pubsub/test/node_v10.go Normal file
View File

@ -0,0 +1,38 @@
// +build !hardened_api
// This file is used when the hardened_api build tag is not present.
// It targets the go-libp2p-pubsub API before GossipSub v1.1.0 was introduced.
// When using this file, peer scores will not be present in the test output.
package main
import (
pubsub "github.com/libp2p/go-libp2p-pubsub"
)
func pubsubOptions(cfg PubsubNodeConfig) ([]pubsub.Option, error) {
opts := []pubsub.Option{
pubsub.WithEventTracer(cfg.Tracer),
}
if cfg.ValidateQueueSize > 0 {
opts = append(opts, pubsub.WithValidateQueueSize(cfg.ValidateQueueSize))
}
if cfg.OutboundQueueSize > 0 {
opts = append(opts, pubsub.WithPeerOutboundQueueSize(cfg.OutboundQueueSize))
}
// Set the overlay parameters
if cfg.OverlayParams.d >= 0 {
pubsub.GossipSubD = cfg.OverlayParams.d
}
if cfg.OverlayParams.dlo >= 0 {
pubsub.GossipSubDlo = cfg.OverlayParams.dlo
}
if cfg.OverlayParams.dhi >= 0 {
pubsub.GossipSubDhi = cfg.OverlayParams.dhi
}
return opts, nil
}

116
pubsub/test/node_v11.go Normal file
View File

@ -0,0 +1,116 @@
// +build hardened_api
// The hardened_api build tag should be used when targeting a version of go-libp2p-pubsub after
// GossipSub v1.1.0 was introduced.
package main
import (
"fmt"
"github.com/libp2p/go-libp2p-core/peer"
pubsub "github.com/libp2p/go-libp2p-pubsub"
)
func pubsubOptions(cfg PubsubNodeConfig) ([]pubsub.Option, error) {
opts := []pubsub.Option{
pubsub.WithEventTracer(cfg.Tracer),
pubsub.WithFloodPublish(cfg.FloodPublishing),
scoreParamsOption(cfg.PeerScoreParams),
}
if cfg.PeerScoreInspect.Inspect != nil && cfg.PeerScoreInspect.Period != 0 {
opts = append(opts, pubsub.WithPeerScoreInspect(cfg.PeerScoreInspect.Inspect, cfg.PeerScoreInspect.Period))
}
if cfg.ValidateQueueSize > 0 {
opts = append(opts, pubsub.WithValidateQueueSize(cfg.ValidateQueueSize))
}
if cfg.OutboundQueueSize > 0 {
opts = append(opts, pubsub.WithPeerOutboundQueueSize(cfg.OutboundQueueSize))
}
// Set the overlay parameters
if cfg.OverlayParams.d >= 0 {
pubsub.GossipSubD = cfg.OverlayParams.d
}
if cfg.OverlayParams.dlo >= 0 {
pubsub.GossipSubDlo = cfg.OverlayParams.dlo
}
if cfg.OverlayParams.dhi >= 0 {
pubsub.GossipSubDhi = cfg.OverlayParams.dhi
}
if cfg.OverlayParams.dscore >= 0 {
pubsub.GossipSubDscore = cfg.OverlayParams.dscore
}
if cfg.OverlayParams.dlazy >= 0 {
pubsub.GossipSubDlazy = cfg.OverlayParams.dlazy
}
if cfg.OverlayParams.gossipFactor > 0 {
pubsub.GossipSubGossipFactor = cfg.OverlayParams.gossipFactor
}
// set opportunistic graft params
if cfg.OpportunisticGraftTicks > 0 {
pubsub.GossipSubOpportunisticGraftTicks = uint64(cfg.OpportunisticGraftTicks)
}
return opts, nil
}
// TODO: implement app-specific scoring
func applicationScore(id peer.ID) float64 {
return 1.0
}
func scoreParamsOption(params ScoreParams) pubsub.Option {
topicParams := make(map[string]*pubsub.TopicScoreParams, len(params.Topics))
for name, t := range params.Topics {
topicParams[name] = convertTopicParams(t)
}
psp := pubsub.PeerScoreParams{
Topics: topicParams,
AppSpecificScore: applicationScore,
AppSpecificWeight: 0,
IPColocationFactorWeight: params.IPColocationFactorWeight,
IPColocationFactorThreshold: params.IPColocationFactorThreshold,
DecayInterval: params.DecayInterval.Duration,
DecayToZero: params.DecayToZero,
RetainScore: params.RetainScore.Duration,
}
pst := pubsub.PeerScoreThresholds{
GossipThreshold: params.Thresholds.GossipThreshold,
PublishThreshold: params.Thresholds.PublishThreshold,
GraylistThreshold: params.Thresholds.GraylistThreshold,
AcceptPXThreshold: params.Thresholds.AcceptPXThreshold,
OpportunisticGraftThreshold: params.Thresholds.OpportunisticGraftThreshold,
}
fmt.Printf("peer score params: %v\nthresholds: %v\n", psp, pst)
return pubsub.WithPeerScore(&psp, &pst)
}
func convertTopicParams(p *TopicScoreParams) *pubsub.TopicScoreParams {
return &pubsub.TopicScoreParams{
TopicWeight: p.TopicWeight,
TimeInMeshWeight: p.TimeInMeshWeight,
TimeInMeshQuantum: p.TimeInMeshQuantum.Duration,
TimeInMeshCap: p.TimeInMeshCap,
FirstMessageDeliveriesWeight: p.FirstMessageDeliveriesWeight,
FirstMessageDeliveriesDecay: p.FirstMessageDeliveriesDecay,
FirstMessageDeliveriesCap: p.FirstMessageDeliveriesCap,
MeshMessageDeliveriesWeight: p.MeshMessageDeliveriesWeight,
MeshMessageDeliveriesDecay: p.MeshMessageDeliveriesDecay,
MeshMessageDeliveriesCap: p.MeshMessageDeliveriesCap,
MeshMessageDeliveriesThreshold: p.MeshMessageDeliveriesThreshold,
MeshMessageDeliveriesWindow: p.MeshMessageDeliveriesWindow.Duration,
MeshMessageDeliveriesActivation: p.MeshMessageDeliveriesActivation.Duration,
MeshFailurePenaltyWeight: p.MeshFailurePenaltyWeight,
MeshFailurePenaltyDecay: p.MeshFailurePenaltyDecay,
InvalidMessageDeliveriesWeight: p.InvalidMessageDeliveriesWeight,
InvalidMessageDeliveriesDecay: p.InvalidMessageDeliveriesDecay,
}
}

249
pubsub/test/params.go Normal file
View File

@ -0,0 +1,249 @@
package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/testground/sdk-go/ptypes"
"github.com/testground/sdk-go/runtime"
)
type NodeType string
const (
NodeTypeHonest NodeType = "honest"
)
type TopicConfig struct {
Id string `json:"id"`
MessageRate ptypes.Rate `json:"message_rate"`
MessageSize ptypes.Size `json:"message_size"`
}
type HeartbeatParams struct {
InitialDelay time.Duration
Interval time.Duration
}
type NetworkParams struct {
latency time.Duration
latencyMax time.Duration
jitterPct int
bandwidthMB int
}
// ScoreParams is mapped to pubsub.PeerScoreParams when targeting the hardened_api pubsub branch
type ScoreParams struct {
Topics map[string]*TopicScoreParams
Thresholds PeerScoreThresholds
// TODO: figure out how to parameterize the app score function
IPColocationFactorWeight float64
IPColocationFactorThreshold int
DecayInterval ptypes.Duration
DecayToZero float64
RetainScore ptypes.Duration
}
type OverlayParams struct {
d int
dlo int
dhi int
dscore int
dlazy int
gossipFactor float64
}
type PeerScoreThresholds struct {
GossipThreshold float64
PublishThreshold float64
GraylistThreshold float64
AcceptPXThreshold float64
OpportunisticGraftThreshold float64
}
// TopicScoreParams is mapped to pubsub.TopicScoreParams when targeting the hardened_api pubsub branch
type TopicScoreParams struct {
TopicWeight float64
TimeInMeshWeight float64
TimeInMeshQuantum ptypes.Duration
TimeInMeshCap float64
FirstMessageDeliveriesWeight float64
FirstMessageDeliveriesDecay float64
FirstMessageDeliveriesCap float64
MeshMessageDeliveriesWeight, MeshMessageDeliveriesDecay float64
MeshMessageDeliveriesCap, MeshMessageDeliveriesThreshold float64
MeshMessageDeliveriesWindow, MeshMessageDeliveriesActivation ptypes.Duration
MeshFailurePenaltyWeight, MeshFailurePenaltyDecay float64
InvalidMessageDeliveriesWeight, InvalidMessageDeliveriesDecay float64
}
type testParams struct {
heartbeat HeartbeatParams
setup time.Duration
warmup time.Duration
runtime time.Duration
cooldown time.Duration
nodeType NodeType
publisher bool
floodPublishing bool
fullTraces bool
topics []TopicConfig
degree int
containerNodesTotal int
nodesPerContainer int
connectDelays []time.Duration
connectDelayJitterPct int
connsDef map[string]*ConnectionsDef
connectToPublishersOnly bool
netParams NetworkParams
overlayParams OverlayParams
scoreParams ScoreParams
scoreInspectPeriod time.Duration
validateQueueSize int
outboundQueueSize int
opportunisticGraftTicks int
}
func durationParam(runenv *runtime.RunEnv, name string) time.Duration {
if !runenv.IsParamSet(name) {
runenv.RecordMessage("duration param %s not set, defaulting to zero", name)
return 0
}
return parseDuration(runenv.StringParam(name))
}
func parseDuration(val string) time.Duration {
d, err := time.ParseDuration(val)
if err != nil {
panic(fmt.Errorf("param %s is not a valid duration: %s", val, err))
}
return d
}
func parseParams(runenv *runtime.RunEnv) testParams {
np := NetworkParams{
latency: durationParam(runenv, "t_latency"),
latencyMax: durationParam(runenv, "t_latency_max"),
jitterPct: runenv.IntParam("jitter_pct"),
bandwidthMB: runenv.IntParam("bandwidth_mb"),
}
op := OverlayParams{
d: runenv.IntParam("overlay_d"),
dlo: runenv.IntParam("overlay_dlo"),
dhi: runenv.IntParam("overlay_dhi"),
dscore: runenv.IntParam("overlay_dscore"),
dlazy: runenv.IntParam("overlay_dlazy"),
gossipFactor: runenv.FloatParam("gossip_factor"),
}
p := testParams{
heartbeat: HeartbeatParams{
InitialDelay: durationParam(runenv, "t_heartbeat_initial_delay"),
Interval: durationParam(runenv, "t_heartbeat"),
},
setup: durationParam(runenv, "t_setup"),
warmup: durationParam(runenv, "t_warm"),
runtime: durationParam(runenv, "t_run"),
cooldown: durationParam(runenv, "t_cool"),
publisher: runenv.BooleanParam("publisher"),
floodPublishing: runenv.BooleanParam("flood_publishing"),
fullTraces: runenv.BooleanParam("full_traces"),
nodeType: NodeTypeHonest,
connectToPublishersOnly: runenv.BooleanParam("connect_to_publishers_only"),
degree: runenv.IntParam("degree"),
containerNodesTotal: runenv.IntParam("n_container_nodes_total"),
nodesPerContainer: runenv.IntParam("n_nodes_per_container"),
scoreInspectPeriod: durationParam(runenv, "t_score_inspect_period"),
netParams: np,
overlayParams: op,
validateQueueSize: runenv.IntParam("validate_queue_size"),
outboundQueueSize: runenv.IntParam("outbound_queue_size"),
opportunisticGraftTicks: runenv.IntParam("opportunistic_graft_ticks"),
}
if runenv.IsParamSet("topics") {
jsonstr := runenv.StringParam("topics")
err := json.Unmarshal([]byte(jsonstr), &p.topics)
if err != nil {
panic(err)
}
runenv.RecordMessage("topics: %v", p.topics)
}
if runenv.IsParamSet("score_params") {
jsonstr := runenv.StringParam("score_params")
err := json.Unmarshal([]byte(jsonstr), &p.scoreParams)
if err != nil {
panic(err)
}
// add warmup time to the mesh delivery activation window for each topic
for _, topic := range p.scoreParams.Topics {
topic.MeshMessageDeliveriesActivation.Duration += p.warmup
}
}
if runenv.IsParamSet("topology") {
jsonstr := runenv.StringParam("topology")
err := json.Unmarshal([]byte(jsonstr), &p.connsDef)
if err != nil {
panic(err)
}
}
if runenv.IsParamSet("connect_delays") {
// eg: "5@10s,15@1m,5@2m"
connDelays := runenv.StringParam("connect_delays")
if connDelays != "" && connDelays != "\"\"" {
cds := strings.Split(connDelays, ",")
for _, cd := range cds {
parts := strings.Split(cd, "@")
if len(parts) != 2 {
panic(fmt.Sprintf("Badly formatted connect_delays param %s", connDelays))
}
count, err := strconv.Atoi(parts[0])
if err != nil {
panic(fmt.Sprintf("Badly formatted connect_delays param %s", connDelays))
}
dur := parseDuration(parts[1])
for i := 0; i < count; i++ {
p.connectDelays = append(p.connectDelays, dur)
}
}
}
p.connectDelayJitterPct = 5
if runenv.IsParamSet("connect_delay_jitter_pct") {
p.connectDelayJitterPct = runenv.IntParam("connect_delay_jitter_pct")
}
}
return p
}
func parseNodeType(nt string) NodeType {
switch nt {
// currently only honest nodes are supported
default:
return NodeTypeHonest
}
}

446
pubsub/test/run.go Normal file
View File

@ -0,0 +1,446 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net"
"os"
rt "runtime"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-core/crypto"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/peer"
"github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr-net"
"golang.org/x/sync/errgroup"
"github.com/testground/sdk-go/network"
"github.com/testground/sdk-go/runtime"
"github.com/testground/sdk-go/sync"
)
// Listen on the address in the testground data network
func listenAddrs(netclient *network.Client) []multiaddr.Multiaddr {
ip, err := netclient.GetDataNetworkIP()
if err == network.ErrNoTrafficShaping {
ip = net.ParseIP("0.0.0.0")
} else if err != nil {
panic(fmt.Errorf("error getting data network addr: %s", err))
}
dataAddr, err := manet.FromIP(ip)
if err != nil {
panic(fmt.Errorf("could not convert IP to multiaddr; ip=%s, err=%s", ip, err))
}
// add /tcp/0 to auto select TCP listen port
listenAddr := dataAddr.Encapsulate(multiaddr.StringCast("/tcp/0"))
return []multiaddr.Multiaddr{listenAddr}
}
type testInstance struct {
*runtime.RunEnv
params testParams
h host.Host
seq int64
nodeTypeSeq int64
nodeIdx int
latency time.Duration
connsDef *ConnectionsDef
client *sync.Client
discovery *SyncDiscovery
peerSubscriber *PeerSubscriber
}
type Message struct {
Name string
Body string
Time int64
}
// Create a new libp2p host
func createHost(ctx context.Context) (host.Host, error) {
priv, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 256)
if err != nil {
return nil, err
}
// Don't listen yet, we need to set up networking first
return libp2p.New(ctx, libp2p.Identity(priv), libp2p.NoListenAddrs)
}
func RunSimulation(runenv *runtime.RunEnv) error {
params := parseParams(runenv)
totalTime := params.setup + params.runtime + params.warmup + params.cooldown
ctx, cancel := context.WithTimeout(context.Background(), totalTime)
defer cancel()
client := sync.MustBoundClient(ctx, runenv)
defer client.Close()
// Create the hosts, but don't listen yet (we need to set up the data
// network before listening)
hosts := make([]host.Host, params.nodesPerContainer)
for i := 0; i < params.nodesPerContainer; i++ {
h, err := createHost(ctx)
if err != nil {
return err
}
hosts[i] = h
}
// Get sequence number within a node type (eg honest-1, honest-2, etc)
nodeTypeSeq, err := getNodeTypeSeqNum(ctx, client, hosts[0], params.nodeType)
if err != nil {
return fmt.Errorf("failed to get node type sequence number: %w", err)
}
// Make sure each container has a distinct random seed
rand.Seed(nodeTypeSeq * time.Now().UnixNano())
runenv.RecordMessage("%s container %d num cpus: %d", params.nodeType, nodeTypeSeq, rt.NumCPU())
// Get the sequence number of each node in the container
peers := sync.NewTopic("nodes", &peer.AddrInfo{})
seqs := make([]int64, params.nodesPerContainer)
for nodeIdx := 0; nodeIdx < params.nodesPerContainer; nodeIdx++ {
seq, err := client.Publish(ctx, peers, host.InfoFromHost(hosts[nodeIdx]))
if err != nil {
return fmt.Errorf("failed to write peer subtree in sync service: %w", err)
}
seqs[nodeIdx] = seq
}
// If a topology definition was provided, read the latency from it
if len(params.connsDef) > 0 {
// Note: The latency is the same for all nodes in the same container
nodeIdx := 0
connsDef, err := loadConnections(params.connsDef, params.nodeType, nodeTypeSeq, nodeIdx)
if err != nil {
return err
}
params.netParams.latency = connsDef.Latency
params.netParams.latencyMax = time.Duration(0)
}
netclient := network.NewClient(client, runenv)
// Set up traffic shaping. Note: this is the same for all nodes in the same container.
if err := setupNetwork(ctx, runenv, params.netParams, netclient); err != nil {
return fmt.Errorf("Failed to set up network: %w", err)
}
// Set up a subscription for node information from all peers in all containers.
// Note that there is only on PeerSubscriber per container (but there may be
// several nodes per container).
peerSubscriber := NewPeerSubscriber(ctx, runenv, client, runenv.TestInstanceCount, params.containerNodesTotal)
// Create each node in the container
errgrp, ctx := errgroup.WithContext(ctx)
for nodeIdx := 0; nodeIdx < params.nodesPerContainer; nodeIdx++ {
nodeIdx := nodeIdx
errgrp.Go(func() (err error) {
t := testInstance{
RunEnv: runenv,
h: hosts[nodeIdx],
seq: seqs[nodeIdx],
nodeTypeSeq: nodeTypeSeq,
nodeIdx: nodeIdx,
params: params,
client: client,
peerSubscriber: peerSubscriber,
}
// Load the connection definition for the node
var connsDef *ConnectionsDef
if len(params.connsDef) > 0 {
connsDef, err = loadConnections(params.connsDef, params.nodeType, nodeTypeSeq, nodeIdx)
if err != nil {
return
}
t.connsDef = connsDef
}
// Listen for incoming connections
laddr := listenAddrs(netclient)
runenv.RecordMessage("listening on %s", laddr)
if err = t.h.Network().Listen(laddr...); err != nil {
return nil
}
id := host.InfoFromHost(t.h).ID.Pretty()
runenv.RecordMessage("Host peer ID: %s, seq %d, node type: %s, node type seq: %d, node index: %d / %d, addrs: %v",
id, t.seq, params.nodeType, nodeTypeSeq, nodeIdx, params.nodesPerContainer, t.h.Addrs())
switch params.nodeType {
case NodeTypeHonest:
err = t.startPubsubNode(ctx)
default:
runenv.RecordMessage("unsupported node type %d", params.nodeType)
}
return
})
}
return errgrp.Wait()
}
func getNodeTypeSeqNum(ctx context.Context, client *sync.Client, h host.Host, nodeType NodeType) (int64, error) {
topic := sync.NewTopic("node-type-"+string(nodeType), &peer.AddrInfo{})
return client.Publish(ctx, topic, host.InfoFromHost(h))
}
func (t *testInstance) startPubsubNode(ctx context.Context) error {
tracerOut := fmt.Sprintf("%s%ctracer-output-honest-%d", t.TestOutputsPath, os.PathSeparator, t.seq)
t.RecordMessage("writing honest node tracer output to %s", tracerOut)
// if we're a publisher, our message publish rate should be a fraction of
// the total message rate for each topic. For now, we distribute the
// publish rates uniformly across the number of instances in our
// testground composition
topics := make([]TopicConfig, len(t.params.topics))
if t.params.publisher {
// FIXME: this assumes all publishers are in the same group, might not always hold up.
nPublishers := t.TestGroupInstanceCount
for i, topic := range t.params.topics {
topics[i] = topic
topics[i].MessageRate.Quantity /= float64(nPublishers)
}
} else {
topics = t.params.topics
}
tracer, err := NewTestTracer(tracerOut, t.h.ID(), t.params.fullTraces)
if err != nil {
return fmt.Errorf("error making test tracer: %s", err)
}
scoreInspectParams := InspectParams{}
if t.params.scoreInspectPeriod != 0 {
scoreInspectParams.Period = t.params.scoreInspectPeriod
outpath := fmt.Sprintf("%s%cpeer-scores-honest-%d.json", t.TestOutputsPath, os.PathSeparator, t.seq)
file, err := os.OpenFile(outpath, os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
return fmt.Errorf("error opening peer score output file at %s: %s", outpath, err)
}
defer file.Close()
t.RecordMessage("recording peer scores to %s", outpath)
enc := json.NewEncoder(file)
type entry struct {
Timestamp int64
PeerID string
Scores map[string]float64
}
scoreInspectParams.Inspect = func(scores map[peer.ID]float64) {
ts := time.Now().UnixNano()
pretty := make(map[string]float64, len(scores))
for p, s := range scores {
pretty[p.Pretty()] = s
}
e := entry{
Timestamp: ts,
PeerID: t.h.ID().Pretty(),
Scores: pretty,
}
err := enc.Encode(e)
if err != nil {
t.RecordMessage("error encoding peer scores: %s", err)
}
}
}
cfg := PubsubNodeConfig{
Publisher: t.params.publisher,
FloodPublishing: t.params.floodPublishing,
PeerScoreParams: t.params.scoreParams,
OverlayParams: t.params.overlayParams,
PeerScoreInspect: scoreInspectParams,
Topics: topics,
Tracer: tracer,
Seq: t.seq,
Warmup: t.params.warmup,
Cooldown: t.params.cooldown,
Heartbeat: t.params.heartbeat,
ValidateQueueSize: t.params.validateQueueSize,
OutboundQueueSize: t.params.outboundQueueSize,
OpportunisticGraftTicks: t.params.opportunisticGraftTicks,
}
n, err := NewPubsubNode(t.RunEnv, ctx, t.h, cfg)
if err != nil {
return err
}
discovery, err := t.setupDiscovery(ctx)
if err != nil {
return err
}
err = n.Run(t.params.runtime, func(ctx context.Context) error {
// wait for all other nodes to be ready
if err := t.waitForReadyState(ctx); err != nil {
return err
}
// connect topology async
go t.connectTopology(ctx)
return nil
})
if err2 := tracer.Stop(); err2 != nil {
t.RecordMessage("error stopping test tracer: %s", err2)
}
return t.outputConns(discovery)
}
func (t *testInstance) setupDiscovery(ctx context.Context) (*SyncDiscovery, error) {
t.RecordMessage("Setup discovery")
// By default connect to a randomly-chosen subset of all honest nodes
var topology Topology
topology = RandomHonestTopology{
Count: t.params.degree,
PublishersOnly: t.params.connectToPublishersOnly,
}
// If a topology file was supplied, use the topology defined there
if t.connsDef != nil {
topology = FixedTopology{t.connsDef}
}
// Register this node and get node information for all peers
discovery, err := NewSyncDiscovery(t.h, t.RunEnv, t.peerSubscriber, topology,
t.params.nodeType, t.nodeTypeSeq, t.nodeIdx, t.params.publisher)
if err != nil {
return nil, fmt.Errorf("error creating discovery service: %s", err)
}
t.discovery = discovery
err = discovery.registerAndWait(ctx)
if err != nil {
return nil, fmt.Errorf("error waiting for discovery service: %s", err)
}
return discovery, nil
}
// Called when nodes are ready to start the run, and are waiting for all other nodes to be ready
func (t *testInstance) waitForReadyStateThenConnect(ctx context.Context) error {
// wait for all other nodes to be ready
if err := t.waitForReadyState(ctx); err != nil {
return err
}
// connect topology
return t.connectTopology(ctx)
}
// Called when nodes are ready to start the run, and are waiting for all other nodes to be ready
func (t *testInstance) waitForReadyState(ctx context.Context) error {
// Set a state barrier.
state := sync.State("ready")
doneCh := t.client.MustBarrier(ctx, state, t.params.containerNodesTotal).C
// Signal we've entered the state.
t.RecordMessage("Signalling ready state")
_, err := t.client.SignalEntry(ctx, state)
if err != nil {
return err
}
// Wait until all others have signalled.
select {
case <-ctx.Done():
return ctx.Err()
case err := <-doneCh:
if err != nil {
return err
}
t.RecordMessage("All instances in ready state, continuing")
}
return nil
}
func (t *testInstance) connectTopology(ctx context.Context) error {
// Default to a connect delay in the range of 0s - 1s
delay := time.Duration(float64(time.Second) * rand.Float64())
// If an explicit delay was specified, calculate the delay
nodeTypeIdx := int(t.nodeTypeSeq - 1)
if nodeTypeIdx < len(t.params.connectDelays) {
expdelay := t.params.connectDelays[nodeTypeIdx]
// Add +/- jitter percent
delay = expdelay + time.Duration(t.params.connectDelayJitterPct)*expdelay/100
delay += time.Duration(rand.Float64() * float64(time.Duration(t.params.connectDelayJitterPct*2)*expdelay/100))
}
// Connect to other peers in the topology
err := t.discovery.ConnectTopology(ctx, delay)
if err != nil {
t.RecordMessage("Error connecting to topology peer: %s", err)
}
return nil
}
// Wait for all nodes to signal that they have completed the run
// (or there's a timeout)
func (t *testInstance) waitForCompleteState(ctx context.Context) error {
// Set a state barrier.
state := sync.State("complete")
// Signal we've entered the state, and wait until all others have signalled.
t.RecordMessage("Signalling complete state")
_, err := t.client.SignalAndWait(ctx, state, t.params.containerNodesTotal)
if err != nil {
return err
}
t.RecordMessage("All instances in complete state, done")
return nil
}
type ConnectionsDef struct {
Latency time.Duration
Connections []string
}
func loadConnections(connsDef map[string]*ConnectionsDef, nodeType NodeType, nodeTypeSeq int64, nodeIdx int) (*ConnectionsDef, error) {
nodeKey := fmt.Sprintf("%s-%d-%d", nodeType, nodeTypeSeq, nodeIdx)
def, ok := connsDef[nodeKey]
if !ok {
return nil, fmt.Errorf("Topology file '%s' has no entry for '%s'")
}
return def, nil
}
func (t *testInstance) outputConns(discovery *SyncDiscovery) error {
connsOut := fmt.Sprintf("%s%cconnections-%s-%d-%d.json", t.TestOutputsPath, os.PathSeparator, t.params.nodeType, t.nodeTypeSeq, t.nodeIdx)
var conns []string
for _, p := range discovery.Connected() {
conns = append(conns, fmt.Sprintf("%s-%d-%d", p.NType, p.NodeTypeSeq, p.NodeIdx))
}
jsonstr, err := json.MarshalIndent(ConnectionsDef{
Latency: t.params.netParams.latency,
Connections: conns,
}, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(connsOut, jsonstr, os.ModePerm)
}

223
pubsub/test/tracer.go Normal file
View File

@ -0,0 +1,223 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"github.com/libp2p/go-libp2p-core/peer"
pubsub "github.com/libp2p/go-libp2p-pubsub"
pb "github.com/libp2p/go-libp2p-pubsub/pb"
)
type RPCMetrics struct {
RPCs uint64
Messages uint64
Grafts uint64
Prunes uint64
IWants uint64
IHaves uint64
}
type TestMetrics struct {
LocalPeer string
Published uint64
Rejected uint64
Delivered uint64
Duplicates uint64
DroppedRPC uint64
PeersAdded uint64
PeersRemoved uint64
TopicsJoined uint64
TopicsLeft uint64
SentRPC RPCMetrics
ReceivedRPC RPCMetrics
}
type TestTracer struct {
full pubsub.EventTracer
filtered pubsub.EventTracer
aggregateOutputPath string
eventCh chan *pb.TraceEvent
doneCh chan struct{}
metrics TestMetrics
}
func NewTestTracer(outputPathPrefix string, localPeerID peer.ID, full bool) (*TestTracer, error) {
var fullTracer pubsub.EventTracer
var err error
if full {
fullTracer, err = pubsub.NewPBTracer(outputPathPrefix + "-full.bin")
if err != nil {
return nil, fmt.Errorf("error making protobuf event tracer: %s", err)
}
}
filteredTracer, err := newFilteringTracer(outputPathPrefix+"-filtered.bin",
pb.TraceEvent_PUBLISH_MESSAGE, pb.TraceEvent_DELIVER_MESSAGE,
pb.TraceEvent_GRAFT, pb.TraceEvent_PRUNE)
if err != nil {
return nil, fmt.Errorf("error making filtered event tracer: %s", err)
}
t := &TestTracer{
full: fullTracer,
filtered: filteredTracer,
aggregateOutputPath: outputPathPrefix + "-aggregate.json",
eventCh: make(chan *pb.TraceEvent, 1024),
doneCh: make(chan struct{}, 1),
}
t.metrics.LocalPeer = localPeerID.Pretty()
go t.eventLoop()
return t, nil
}
func (t *TestTracer) Stop() error {
t.doneCh <- struct{}{}
jsonstr, err := json.MarshalIndent(t.metrics, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(t.aggregateOutputPath, jsonstr, os.ModePerm)
}
func (t *TestTracer) eventLoop() {
for {
select {
case <-t.doneCh:
return
case evt := <-t.eventCh:
switch evt.GetType() {
case pb.TraceEvent_PUBLISH_MESSAGE:
t.publishMessage(evt)
case pb.TraceEvent_REJECT_MESSAGE:
t.rejectMessage(evt)
case pb.TraceEvent_DUPLICATE_MESSAGE:
t.duplicateMessage(evt)
case pb.TraceEvent_DELIVER_MESSAGE:
t.deliverMessage(evt)
case pb.TraceEvent_ADD_PEER:
t.addPeer(evt)
case pb.TraceEvent_REMOVE_PEER:
t.removePeer(evt)
case pb.TraceEvent_RECV_RPC:
t.recvRPC(evt)
case pb.TraceEvent_SEND_RPC:
t.sendRPC(evt)
case pb.TraceEvent_DROP_RPC:
t.dropRPC(evt)
case pb.TraceEvent_JOIN:
t.join(evt)
case pb.TraceEvent_LEAVE:
t.leave(evt)
case pb.TraceEvent_GRAFT:
t.graft(evt)
case pb.TraceEvent_PRUNE:
t.prune(evt)
}
}
}
}
func (t *TestTracer) Trace(evt *pb.TraceEvent) {
t.filtered.Trace(evt)
if t.full != nil {
t.full.Trace(evt)
}
t.eventCh <- evt
}
func (t *TestTracer) publishMessage(evt *pb.TraceEvent) {
t.metrics.Published++
}
func (t *TestTracer) rejectMessage(evt *pb.TraceEvent) {
t.metrics.Rejected++
}
func (t *TestTracer) deliverMessage(evt *pb.TraceEvent) {
t.metrics.Delivered++
}
func (t *TestTracer) duplicateMessage(evt *pb.TraceEvent) {
t.metrics.Duplicates++
}
func (t *TestTracer) sendRPC(evt *pb.TraceEvent) {
meta := evt.GetSendRPC().GetMeta()
updateRPCStats(&t.metrics.SentRPC, meta)
}
func (t *TestTracer) recvRPC(evt *pb.TraceEvent) {
meta := evt.GetRecvRPC().GetMeta()
updateRPCStats(&t.metrics.ReceivedRPC, meta)
}
func updateRPCStats(stats *RPCMetrics, meta *pb.TraceEvent_RPCMeta) {
ctrl := meta.GetControl()
stats.RPCs += 1
stats.Messages += uint64(len(meta.GetMessages()))
stats.IHaves += uint64(len(ctrl.GetIhave()))
stats.IWants += uint64(len(ctrl.GetIwant()))
stats.Grafts += uint64(len(ctrl.GetGraft()))
stats.Prunes += uint64(len(ctrl.GetPrune()))
}
func (t *TestTracer) dropRPC(evt *pb.TraceEvent) {
t.metrics.DroppedRPC++
}
func (t *TestTracer) addPeer(evt *pb.TraceEvent) {
t.metrics.PeersAdded++
}
func (t *TestTracer) removePeer(evt *pb.TraceEvent) {
t.metrics.PeersRemoved++
}
func (t *TestTracer) join(evt *pb.TraceEvent) {
t.metrics.TopicsJoined++
}
func (t *TestTracer) leave(evt *pb.TraceEvent) {
t.metrics.TopicsLeft++
}
func (t *TestTracer) graft(evt *pb.TraceEvent) {
// already accounted for in sendRPC
}
func (t *TestTracer) prune(evt *pb.TraceEvent) {
// already accounted for in sendRPC
}
var _ pubsub.EventTracer = (*TestTracer)(nil)
type filteringTracer struct {
pubsub.EventTracer
whitelist []pb.TraceEvent_Type
}
func newFilteringTracer(outputPath string, typeWhitelist ...pb.TraceEvent_Type) (*filteringTracer, error) {
tracer, err := pubsub.NewPBTracer(outputPath)
if err != nil {
return nil, fmt.Errorf("error making protobuf event tracer: %s", err)
}
return &filteringTracer{EventTracer: tracer, whitelist: typeWhitelist}, nil
}
func (t *filteringTracer) Trace(evt *pb.TraceEvent) {
for _, typ := range t.whitelist {
if evt.GetType() == typ {
t.EventTracer.Trace(evt)
return
}
}
}