From f0762b1814d232eca2ba93f98d2222ebf571571a Mon Sep 17 00:00:00 2001 From: Yusef Napora Date: Fri, 15 May 2020 14:09:41 -0400 Subject: [PATCH] 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 --- .gitignore | 41 ++ pubsub/README.md | 319 ++++++++++ pubsub/scripts/Analysis-Template.ipynb | 508 ++++++++++++++++ pubsub/scripts/Runner.ipynb | 179 ++++++ pubsub/scripts/analyze.py | 292 ++++++++++ .../scripts/configs/cluster-k8s/1k-peers.json | 1 + .../configs/local-docker/100-peers.json | 1 + .../configs/local-docker/25-peers.json | 1 + .../configs/local-docker/50-peers.json | 1 + .../scripts/configs/local-exec/25-peers.json | 1 + .../scripts/configs/local-exec/50-peers.json | 1 + pubsub/scripts/go.mod | 8 + pubsub/scripts/go.sum | 334 +++++++++++ pubsub/scripts/notebook_helper.py | 389 +++++++++++++ pubsub/scripts/requirements.txt | 16 + pubsub/scripts/run.py | 256 ++++++++ pubsub/scripts/sync_outputs.py | 155 +++++ .../templates/baseline/params/1k-nodes.toml | 3 + .../templates/baseline/params/_base.toml | 90 +++ .../templates/baseline/template.toml.j2 | 116 ++++ pubsub/scripts/ui.py | 545 ++++++++++++++++++ pubsub/test/discovery.go | 382 ++++++++++++ pubsub/test/go.mod | 17 + pubsub/test/go.sum | 541 +++++++++++++++++ pubsub/test/main.go | 24 + pubsub/test/manifest.toml | 66 +++ pubsub/test/net.go | 55 ++ pubsub/test/node.go | 324 +++++++++++ pubsub/test/node_v10.go | 38 ++ pubsub/test/node_v11.go | 116 ++++ pubsub/test/params.go | 249 ++++++++ pubsub/test/run.go | 446 ++++++++++++++ pubsub/test/tracer.go | 223 +++++++ 33 files changed, 5738 insertions(+) create mode 100644 .gitignore create mode 100644 pubsub/README.md create mode 100644 pubsub/scripts/Analysis-Template.ipynb create mode 100644 pubsub/scripts/Runner.ipynb create mode 100755 pubsub/scripts/analyze.py create mode 100644 pubsub/scripts/configs/cluster-k8s/1k-peers.json create mode 100644 pubsub/scripts/configs/local-docker/100-peers.json create mode 100644 pubsub/scripts/configs/local-docker/25-peers.json create mode 100644 pubsub/scripts/configs/local-docker/50-peers.json create mode 100644 pubsub/scripts/configs/local-exec/25-peers.json create mode 100644 pubsub/scripts/configs/local-exec/50-peers.json create mode 100644 pubsub/scripts/go.mod create mode 100644 pubsub/scripts/go.sum create mode 100644 pubsub/scripts/notebook_helper.py create mode 100644 pubsub/scripts/requirements.txt create mode 100755 pubsub/scripts/run.py create mode 100755 pubsub/scripts/sync_outputs.py create mode 100644 pubsub/scripts/templates/baseline/params/1k-nodes.toml create mode 100644 pubsub/scripts/templates/baseline/params/_base.toml create mode 100644 pubsub/scripts/templates/baseline/template.toml.j2 create mode 100644 pubsub/scripts/ui.py create mode 100644 pubsub/test/discovery.go create mode 100644 pubsub/test/go.mod create mode 100644 pubsub/test/go.sum create mode 100644 pubsub/test/main.go create mode 100644 pubsub/test/manifest.toml create mode 100644 pubsub/test/net.go create mode 100644 pubsub/test/node.go create mode 100644 pubsub/test/node_v10.go create mode 100644 pubsub/test/node_v11.go create mode 100644 pubsub/test/params.go create mode 100644 pubsub/test/run.go create mode 100644 pubsub/test/tracer.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5d622d --- /dev/null +++ b/.gitignore @@ -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 diff --git a/pubsub/README.md b/pubsub/README.md new file mode 100644 index 0000000..fdb988b --- /dev/null +++ b/pubsub/README.md @@ -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. diff --git a/pubsub/scripts/Analysis-Template.ipynb b/pubsub/scripts/Analysis-Template.ipynb new file mode 100644 index 0000000..95eee4b --- /dev/null +++ b/pubsub/scripts/Analysis-Template.ipynb @@ -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", + "

\n", + "\n", + "

\n", + " Expand to show example data\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", + "
" + ] + }, + { + "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 +} \ No newline at end of file diff --git a/pubsub/scripts/Runner.ipynb b/pubsub/scripts/Runner.ipynb new file mode 100644 index 0000000..01b1f9e --- /dev/null +++ b/pubsub/scripts/Runner.ipynb @@ -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", + "
\n", + "
\n", + " \n", + " The UI Widgets aren't showing up\n", + " \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", + "
\n", + "\n", + "
\n", + "\n", + "
\n", + " \n", + " The test fails immediately\n", + " \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", + "
\n", + "
\n", + "\n", + "
\n", + " \n", + " The test completes, but it prints errors about missing programs\n", + " \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", + "
" + ] + }, + { + "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 +} diff --git a/pubsub/scripts/analyze.py b/pubsub/scripts/analyze.py new file mode 100755 index 0000000..0e939cf --- /dev/null +++ b/pubsub/scripts/analyze.py @@ -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() diff --git a/pubsub/scripts/configs/cluster-k8s/1k-peers.json b/pubsub/scripts/configs/cluster-k8s/1k-peers.json new file mode 100644 index 0000000..70fc21a --- /dev/null +++ b/pubsub/scripts/configs/cluster-k8s/1k-peers.json @@ -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}}}}} \ No newline at end of file diff --git a/pubsub/scripts/configs/local-docker/100-peers.json b/pubsub/scripts/configs/local-docker/100-peers.json new file mode 100644 index 0000000..7bced09 --- /dev/null +++ b/pubsub/scripts/configs/local-docker/100-peers.json @@ -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}}}}} \ No newline at end of file diff --git a/pubsub/scripts/configs/local-docker/25-peers.json b/pubsub/scripts/configs/local-docker/25-peers.json new file mode 100644 index 0000000..a31174a --- /dev/null +++ b/pubsub/scripts/configs/local-docker/25-peers.json @@ -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}}}}} \ No newline at end of file diff --git a/pubsub/scripts/configs/local-docker/50-peers.json b/pubsub/scripts/configs/local-docker/50-peers.json new file mode 100644 index 0000000..09e1a8e --- /dev/null +++ b/pubsub/scripts/configs/local-docker/50-peers.json @@ -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}}}}} \ No newline at end of file diff --git a/pubsub/scripts/configs/local-exec/25-peers.json b/pubsub/scripts/configs/local-exec/25-peers.json new file mode 100644 index 0000000..18d5ec1 --- /dev/null +++ b/pubsub/scripts/configs/local-exec/25-peers.json @@ -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}}}}} \ No newline at end of file diff --git a/pubsub/scripts/configs/local-exec/50-peers.json b/pubsub/scripts/configs/local-exec/50-peers.json new file mode 100644 index 0000000..d31c0dc --- /dev/null +++ b/pubsub/scripts/configs/local-exec/50-peers.json @@ -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}}}}} \ No newline at end of file diff --git a/pubsub/scripts/go.mod b/pubsub/scripts/go.mod new file mode 100644 index 0000000..0ace010 --- /dev/null +++ b/pubsub/scripts/go.mod @@ -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 diff --git a/pubsub/scripts/go.sum b/pubsub/scripts/go.sum new file mode 100644 index 0000000..c3270d5 --- /dev/null +++ b/pubsub/scripts/go.sum @@ -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= diff --git a/pubsub/scripts/notebook_helper.py b/pubsub/scripts/notebook_helper.py new file mode 100644 index 0000000..cab036f --- /dev/null +++ b/pubsub/scripts/notebook_helper.py @@ -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) \ No newline at end of file diff --git a/pubsub/scripts/requirements.txt b/pubsub/scripts/requirements.txt new file mode 100644 index 0000000..30aec8a --- /dev/null +++ b/pubsub/scripts/requirements.txt @@ -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 \ No newline at end of file diff --git a/pubsub/scripts/run.py b/pubsub/scripts/run.py new file mode 100755 index 0000000..f242f02 --- /dev/null +++ b/pubsub/scripts/run.py @@ -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='', + 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() + diff --git a/pubsub/scripts/sync_outputs.py b/pubsub/scripts/sync_outputs.py new file mode 100755 index 0000000..bd737aa --- /dev/null +++ b/pubsub/scripts/sync_outputs.py @@ -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() diff --git a/pubsub/scripts/templates/baseline/params/1k-nodes.toml b/pubsub/scripts/templates/baseline/params/1k-nodes.toml new file mode 100644 index 0000000..eb9d61e --- /dev/null +++ b/pubsub/scripts/templates/baseline/params/1k-nodes.toml @@ -0,0 +1,3 @@ +COMPOSITION_NAME = "baseline-1k-nodes" +N_NODES = 1000 +N_PUBLISHER = 100 diff --git a/pubsub/scripts/templates/baseline/params/_base.toml b/pubsub/scripts/templates/baseline/params/_base.toml new file mode 100644 index 0000000..e117bca --- /dev/null +++ b/pubsub/scripts/templates/baseline/params/_base.toml @@ -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 diff --git a/pubsub/scripts/templates/baseline/template.toml.j2 b/pubsub/scripts/templates/baseline/template.toml.j2 new file mode 100644 index 0000000..10b62b0 --- /dev/null +++ b/pubsub/scripts/templates/baseline/template.toml.j2 @@ -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 }}' diff --git a/pubsub/scripts/ui.py b/pubsub/scripts/ui.py new file mode 100644 index 0000000..0ca9bad --- /dev/null +++ b/pubsub/scripts/ui.py @@ -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('

Peer Score Params

'), + 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'] diff --git a/pubsub/test/discovery.go b/pubsub/test/discovery.go new file mode 100644 index 0000000..6d2762d --- /dev/null +++ b/pubsub/test/discovery.go @@ -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 +} diff --git a/pubsub/test/go.mod b/pubsub/test/go.mod new file mode 100644 index 0000000..d9bb5df --- /dev/null +++ b/pubsub/test/go.mod @@ -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 +) diff --git a/pubsub/test/go.sum b/pubsub/test/go.sum new file mode 100644 index 0000000..c7092ba --- /dev/null +++ b/pubsub/test/go.sum @@ -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= diff --git a/pubsub/test/main.go b/pubsub/test/main.go new file mode 100644 index 0000000..45e158e --- /dev/null +++ b/pubsub/test/main.go @@ -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) + } +} diff --git a/pubsub/test/manifest.toml b/pubsub/test/manifest.toml new file mode 100644 index 0000000..f50b255 --- /dev/null +++ b/pubsub/test/manifest.toml @@ -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 } diff --git a/pubsub/test/net.go b/pubsub/test/net.go new file mode 100644 index 0000000..c460261 --- /dev/null +++ b/pubsub/test/net.go @@ -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 +} diff --git a/pubsub/test/node.go b/pubsub/test/node.go new file mode 100644 index 0000000..936fed2 --- /dev/null +++ b/pubsub/test/node.go @@ -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 + } + } +} diff --git a/pubsub/test/node_v10.go b/pubsub/test/node_v10.go new file mode 100644 index 0000000..4a2802d --- /dev/null +++ b/pubsub/test/node_v10.go @@ -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 +} diff --git a/pubsub/test/node_v11.go b/pubsub/test/node_v11.go new file mode 100644 index 0000000..89067ca --- /dev/null +++ b/pubsub/test/node_v11.go @@ -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, + } +} diff --git a/pubsub/test/params.go b/pubsub/test/params.go new file mode 100644 index 0000000..02d6d04 --- /dev/null +++ b/pubsub/test/params.go @@ -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 + } +} diff --git a/pubsub/test/run.go b/pubsub/test/run.go new file mode 100644 index 0000000..99fc198 --- /dev/null +++ b/pubsub/test/run.go @@ -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) +} diff --git a/pubsub/test/tracer.go b/pubsub/test/tracer.go new file mode 100644 index 0000000..1db5fea --- /dev/null +++ b/pubsub/test/tracer.go @@ -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 + } + } +}