From bee97962e7d35d64a7be37e81879087ae1c192b7 Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Mon, 28 Sep 2020 07:46:00 +0100 Subject: [PATCH] Initial release --- .dockerignore | 23 + .github/workflows/golangci-lint.yml | 23 + .github/workflows/release.yml | 115 ++ Dockerfile | 19 + LICENSE | 201 ++++ README.md | 79 +- clients.go | 51 + docs/accountmanager.md | 82 ++ docs/configuration.md | 77 ++ docs/getting_started.md | 38 + docs/graffiti.md | 36 + docs/majordomo.md | 48 + docs/metrics/prometheus.md | 69 ++ go.mod | 51 +- go.sum | 709 +++++++++++- loggers/jaeger.go | 38 + logging.go | 71 ++ main.go | 624 +++++++++- media/architecture.svg | 1022 +++++++++++++++++ mock/accountmanager.go | 216 ++++ mock/eth2client.go | 125 ++ services/accountmanager/dirk/parameters.go | 209 ++++ services/accountmanager/dirk/service.go | 411 +++++++ .../accountmanager/dirk/validatingaccount.go | 185 +++ services/accountmanager/service.go | 132 +++ services/accountmanager/wallet/generate.go | 17 + services/accountmanager/wallet/parameters.go | 187 +++ services/accountmanager/wallet/service.go | 393 +++++++ .../accountmanager/wallet/signingcontainer.go | 20 + .../wallet/signingcontainer_encoding.go | 88 ++ .../wallet/validatingaccount.go | 188 +++ services/attestationaggregator/service.go | 80 ++ .../standard/parameters.go | 115 ++ .../attestationaggregator/standard/service.go | 198 ++++ services/attester/helpers.go | 95 ++ services/attester/service.go | 85 ++ services/attester/standard/parameters.go | 126 ++ services/attester/standard/service.go | 216 ++++ services/beaconblockproposer/service.go | 61 +- .../standard/parameters.go | 114 ++ .../beaconblockproposer/standard/service.go | 211 ++++ services/beaconcommitteesubscriber/service.go | 37 + .../standard/parameters.go | 115 ++ .../standard/service.go | 232 ++++ services/beaconnode/mock/service.go | 50 - services/chaintime/service.go | 34 + services/chaintime/standard/parameters.go | 90 ++ services/chaintime/standard/service.go | 114 ++ services/chaintime/standard/service_test.go | 174 +++ services/controller/standard/attester.go | 158 +++ services/controller/standard/parameters.go | 205 ++++ services/controller/standard/proposer.go | 81 ++ services/controller/standard/service.go | 273 +++++ .../graffitiprovider/dynamic/parameters.go | 80 ++ services/graffitiprovider/dynamic/service.go | 99 ++ .../graffitiprovider/dynamic/service_test.go | 310 +++++ .../service.go | 17 +- .../graffitiprovider/static/parameters.go | 68 ++ services/graffitiprovider/static/service.go | 55 + .../graffitiprovider/static/service_test.go | 41 + services/metrics/null/service.go | 74 ++ services/metrics/prometheus/accountmanager.go | 37 + services/metrics/prometheus/attestation.go | 55 + .../prometheus/attestationaggregation.go | 72 ++ .../metrics/prometheus/beaconblockproposal.go | 55 + .../prometheus/beaconcommitteesubscription.go | 85 ++ services/metrics/prometheus/client.go | 58 + services/metrics/prometheus/controller.go | 69 ++ services/metrics/prometheus/parameters.go | 68 ++ services/metrics/prometheus/scheduler.go | 72 ++ services/metrics/prometheus/service.go | 108 ++ services/metrics/service.go | 83 ++ services/scheduler/basic/parameters.go | 68 ++ services/scheduler/basic/service.go | 321 ++++++ services/scheduler/basic/service_test.go | 383 ++++++ services/scheduler/service.go | 76 ++ services/signer/service.go | 42 - services/submitter/immediate/parameters.go | 102 ++ services/submitter/immediate/service.go | 160 +++ services/submitter/immediate/service_test.go | 131 +++ services/submitter/multinode/parameters.go | 114 ++ services/submitter/multinode/service.go | 59 + .../multinode/submitaggregateattestation.go | 66 ++ .../submitter/multinode/submitattestation.go | 66 ++ .../submitter/multinode/submitbeaconblock.go | 66 ++ .../submitbeaconcommitteesubscriptions.go | 87 ++ services/submitter/null/parameters.go | 55 + services/submitter/null/service.go | 121 ++ services/submitter/null/service_test.go | 68 ++ services/submitter/service.go | 58 + .../beaconblockproposal/best/parameters.go | 104 ++ .../beaconblockproposal/best/service.go | 148 +++ .../beaconblockproposal/first/parameters.go | 93 ++ .../beaconblockproposal/first/service.go | 98 ++ 94 files changed, 12225 insertions(+), 178 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 clients.go create mode 100644 docs/accountmanager.md create mode 100644 docs/configuration.md create mode 100644 docs/getting_started.md create mode 100644 docs/graffiti.md create mode 100644 docs/majordomo.md create mode 100644 docs/metrics/prometheus.md create mode 100644 loggers/jaeger.go create mode 100644 logging.go create mode 100644 media/architecture.svg create mode 100644 mock/accountmanager.go create mode 100644 mock/eth2client.go create mode 100644 services/accountmanager/dirk/parameters.go create mode 100644 services/accountmanager/dirk/service.go create mode 100644 services/accountmanager/dirk/validatingaccount.go create mode 100644 services/accountmanager/service.go create mode 100644 services/accountmanager/wallet/generate.go create mode 100644 services/accountmanager/wallet/parameters.go create mode 100644 services/accountmanager/wallet/service.go create mode 100644 services/accountmanager/wallet/signingcontainer.go create mode 100644 services/accountmanager/wallet/signingcontainer_encoding.go create mode 100644 services/accountmanager/wallet/validatingaccount.go create mode 100644 services/attestationaggregator/service.go create mode 100644 services/attestationaggregator/standard/parameters.go create mode 100644 services/attestationaggregator/standard/service.go create mode 100644 services/attester/helpers.go create mode 100644 services/attester/service.go create mode 100644 services/attester/standard/parameters.go create mode 100644 services/attester/standard/service.go create mode 100644 services/beaconblockproposer/standard/parameters.go create mode 100644 services/beaconblockproposer/standard/service.go create mode 100644 services/beaconcommitteesubscriber/service.go create mode 100644 services/beaconcommitteesubscriber/standard/parameters.go create mode 100644 services/beaconcommitteesubscriber/standard/service.go delete mode 100644 services/beaconnode/mock/service.go create mode 100644 services/chaintime/service.go create mode 100644 services/chaintime/standard/parameters.go create mode 100644 services/chaintime/standard/service.go create mode 100644 services/chaintime/standard/service_test.go create mode 100644 services/controller/standard/attester.go create mode 100644 services/controller/standard/parameters.go create mode 100644 services/controller/standard/proposer.go create mode 100644 services/controller/standard/service.go create mode 100644 services/graffitiprovider/dynamic/parameters.go create mode 100644 services/graffitiprovider/dynamic/service.go create mode 100644 services/graffitiprovider/dynamic/service_test.go rename services/{beaconnode => graffitiprovider}/service.go (63%) create mode 100644 services/graffitiprovider/static/parameters.go create mode 100644 services/graffitiprovider/static/service.go create mode 100644 services/graffitiprovider/static/service_test.go create mode 100644 services/metrics/null/service.go create mode 100644 services/metrics/prometheus/accountmanager.go create mode 100644 services/metrics/prometheus/attestation.go create mode 100644 services/metrics/prometheus/attestationaggregation.go create mode 100644 services/metrics/prometheus/beaconblockproposal.go create mode 100644 services/metrics/prometheus/beaconcommitteesubscription.go create mode 100644 services/metrics/prometheus/client.go create mode 100644 services/metrics/prometheus/controller.go create mode 100644 services/metrics/prometheus/parameters.go create mode 100644 services/metrics/prometheus/scheduler.go create mode 100644 services/metrics/prometheus/service.go create mode 100644 services/metrics/service.go create mode 100644 services/scheduler/basic/parameters.go create mode 100644 services/scheduler/basic/service.go create mode 100644 services/scheduler/basic/service_test.go create mode 100644 services/scheduler/service.go delete mode 100644 services/signer/service.go create mode 100644 services/submitter/immediate/parameters.go create mode 100644 services/submitter/immediate/service.go create mode 100644 services/submitter/immediate/service_test.go create mode 100644 services/submitter/multinode/parameters.go create mode 100644 services/submitter/multinode/service.go create mode 100644 services/submitter/multinode/submitaggregateattestation.go create mode 100644 services/submitter/multinode/submitattestation.go create mode 100644 services/submitter/multinode/submitbeaconblock.go create mode 100644 services/submitter/multinode/submitbeaconcommitteesubscriptions.go create mode 100644 services/submitter/null/parameters.go create mode 100644 services/submitter/null/service.go create mode 100644 services/submitter/null/service_test.go create mode 100644 services/submitter/service.go create mode 100644 strategies/beaconblockproposal/best/parameters.go create mode 100644 strategies/beaconblockproposal/best/service.go create mode 100644 strategies/beaconblockproposal/first/parameters.go create mode 100644 strategies/beaconblockproposal/first/service.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b222fb5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +coverage.html + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# Vim +*.sw? + +# Local TODO +TODO.md + +Dockerfile diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..80c29ef --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,23 @@ +name: golangci-lint +on: [ push, pull_request ] +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v1 + with: + # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. + version: v1.29 + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..21d779f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,115 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ^1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: | + go get -v -t -d ./... + if [ -f Gopkg.toml ]; then + curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + dep ensure + fi + + - name: Set env + run: | + echo '::set-env name=GO111MODULE::on' + # Release tag comes from the github reference. + RELEASE_TAG=$(echo ${GITHUB_REF} | sed -e 's!.*/!!') + echo "::set-env name=RELEASE_TAG::${RELEASE_TAG}" + echo "::set-output name=RELEASE_TAG::${RELEASE_TAG}" + # Ensure the release tag has expected format. + echo ${RELEASE_TAG} | grep -q '^v' || exit 1 + # Release version is same as release tag without leading 'v'. + RELEASE_VERSION=$(echo ${GITHUB_REF} | sed -e 's!.*/v!!') + echo "::set-env name=RELEASE_VERSION::${RELEASE_VERSION}" + echo "::set-output name=RELEASE_VERSION::${RELEASE_VERSION}" + + - name: Build + run: go build -v -ldflags="-X github.com/attestantio/vouch/cmd.ReleaseVersion=${RELEASE_VERSION}" . + + - name: Test + run: go test -v . + + - name: Fetch xgo + run: | + go get github.com/suburbandad/xgo + + - name: Cross-compile + run: xgo -v -x -ldflags="-X github.com/attestantio/vouch/cmd.ReleaseVersion=${RELEASE_VERSION}" --targets="linux/amd64,linux/arm64,windows/amd64" github.com/attestantio/vouch + + - name: Create windows zip file + run: | + mv vouch-windows-4.0-amd64.exe vouch.exe + zip --junk-paths vouch-${RELEASE_VERSION}-windows-exe.zip vouch.exe + + - name: Create linux AMD64 tgz file + run: | + mv vouch-linux-amd64 vouch + tar zcf vouch-${RELEASE_VERSION}-linux-amd64.tar.gz vouch + + - name: Create linux ARM64 tgz file + run: | + mv vouch-linux-arm64 vouch + tar zcf vouch-${RELEASE_VERSION}-linux-arm64.tar.gz vouch + + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ env.RELEASE_VERSION }} + draft: false + prerelease: false + + - name: Upload windows zip file + id: upload-release-asset-windows + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./vouch-${{ env.RELEASE_VERSION }}-windows-exe.zip + asset_name: vouch-${{ env.RELEASE_VERSION }}-windows-exe.zip + asset_content_type: application/zip + + - name: Upload linux AMD64 tgz file + id: upload-release-asset-linux-amd64 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./vouch-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz + asset_name: vouch-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz + asset_content_type: application/gzip + + - name: Upload linux ARM64 tgz file + id: upload-release-asset-linux-arm64 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./vouch-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz + asset_name: vouch-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz + asset_content_type: application/gzip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..05fe60c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.14-buster as builder + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY . . + +RUN go build + +FROM debian:buster-slim + +WORKDIR /app + +COPY --from=builder /app/vouch /app + +ENTRYPOINT ["/app/vouch"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index d2687b8..13bba5c 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,63 @@ -Vouch is an Ethereum 2 validator focused on speed of execution for medium-to-large numbers of validators. +# Vouch -## Architecture +[![Tag](https://img.shields.io/github/tag/attestantio/vouch.svg)](https://github.com/attestantio/vouch/releases/) +[![License](https://img.shields.io/github/license/attestantio/vouch.svg)](LICENSE) +[![GoDoc](https://godoc.org/github.com/attestantio/vouch?status.svg)](https://godoc.org/github.com/attestantio/vouch) +![Lint](https://github.com/attestantio/vouch/workflows/golangci-lint/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/attestantio/vouch)](https://goreportcard.com/report/github.com/attestantio/vouch) -Vouch is designed with an architecture that allows each action of a validator to take place at the optimum time. +An Ethereum 2 multi-node validator client. +## Table of Contents -Note that there are a number of features that Vouch does not provide. These are, in general, decisions that have been taken to keep Vouch focused on its primary role of validating. +- [Install](#install) + - [Binaries](#binaries) + - [Docker](#docker) + - [Source](#source) +- [Usage](#usage) +- [Maintainers](#maintainers) +- [Contribute](#contribute) +- [License](#license) - - key generation and wallet functions - - user-facing metrics +## Install - - When Vouch is started - - Connections to beacon nodes are established - - Signers are initialised - - Initial state of the network is ascertained +### Binaries - - When notification of a new epoch arrives - - Beacon block proposer goroutine is spun off - - Beacon block attester goroutine is spun off +Binaries for the latest version of Vouch can be obtained from [the releases page](https://github.com/attestantio/vouch/releases/latest). - - When a beacon block proposer goroutine starts - - Fetch the expected block proposals for the current epoch - - Spin off a goroutine for each proposal for which we are responsible +### Docker - - When a beacn block proposal goroutine starts - - Wait until appropriate time - - Fetch details for the block - - Create the block - - Publish the block +You can obtain the latest version of Vouch using docker with: - - When a chain reorganisation is notified - - Beacon block proposer goroutine is notified - - Beacon block attester goroutine is notified +``` +docker pull attestant/vouch +``` - - When a beacn block proposer goroutine is notified of a chain reorganisation - - Fetch the updated block proposals for the current epoch - - Notify the relevant +### Source + +Vouch is a standard Go module which can be installed with: + +```sh +go get github.com/attestantio/vouch +``` + +## Usage +Vouch sits between the beacon node(s) and signer(s) in an Ethereum 2 validating infrastructure. It runs as a standard daemon process. The following documents provide information about configuring and using Vouch: + + - [Getting started](docs/getting_started.md) starting Vouch for the first time + - [Prometheus metrics](docs/metrics/prometheus.md) Prometheus metrics + - [Configuration](docs/configuration.md) Sample annotated configuration file + - [Account manager](docs/accountmanager.md) Details of the supported account managers + - [Graffiti](docs/graffiti.md) Details of the graffiti provider + +## Maintainers + +Jim McDonald: [@mcdee](https://github.com/mcdee). + +## Contribute + +Contributions welcome. Please check out [the issues](https://github.com/attestantio/dirk/issues). + +## License + +[Apache-2.0](LICENSE) © 2020 Attestant Limited. diff --git a/clients.go b/clients.go new file mode 100644 index 0000000..2afcc1a --- /dev/null +++ b/clients.go @@ -0,0 +1,51 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "sync" + + eth2client "github.com/attestantio/go-eth2-client" + autoclient "github.com/attestantio/go-eth2-client/auto" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +var clients map[string]eth2client.Service +var clientsMu sync.Mutex + +// fetchClient fetches a client service, instantiating it if required. +func fetchClient(ctx context.Context, address string) (eth2client.Service, error) { + clientsMu.Lock() + defer clientsMu.Unlock() + if clients == nil { + clients = make(map[string]eth2client.Service) + } + + var client eth2client.Service + var exists bool + if client, exists = clients[address]; !exists { + var err error + client, err = autoclient.New(ctx, + autoclient.WithLogLevel(logLevel(viper.GetString("eth2client.log-level"))), + autoclient.WithTimeout(viper.GetDuration("eth2client.timeout")), + autoclient.WithAddress(address)) + if err != nil { + return nil, errors.Wrap(err, "failed to initiate client") + } + clients[address] = client + } + return client, nil +} diff --git a/docs/accountmanager.md b/docs/accountmanager.md new file mode 100644 index 0000000..017e8a0 --- /dev/null +++ b/docs/accountmanager.md @@ -0,0 +1,82 @@ +# Account managers +Account managers are the interface between Vouch and the accounts for which it validates. Account managers provide the list of validating accounts and carry out signing operations. + +Vouch currently supports two account managers: Dirk and wallet. + +## `dirk` +The `dirk` account manager obtains account information from [Dirk](https://github.com/attestantio/dirk), and uses Dirk for remote signing. It is important to understand that this account manager never holds the private keys, instead it sends the data to sign to the Dirk server, which carries out signing as well as slashing prevention. + +The basic configuration for using Dirk is as follows: + +```YAML +accountmanager: + dirk: + endpoints: + - signer.example.com:8881 + client-cert: file:///home/me/certs/validator.example.com.crt + client-key: file:///home/me/certs/validator.example.com.key + ca-cert: file:///home/me/certs/ca.crt + accounts: + - my valdiators +``` + +Each item is explained in more detail below. + +### endpoints +`endpoints` is a list of addresses that host Dirk servers that can respond to your requests. There can be multiple Dirk servers, for example: + - the servers hold different accounts + - the servers are part of a signing threshold group + +At least one endpoint is required for the Dirk account manager. + +### client-cert +Dirk requires all clients to use certificates to identify themselves. Creating these certificates is detailed in the relevant [Dirk documentation](https://github.com/attestantio/dirk/blob/master/docs/getting_started.md#creating-certificates). `client-cert` is the client certificate that identifies this Vouch instance. This is required. + +### client-key +`client-key` is the client key that identifies this Vouch instance. This is required. + +### ca-cert +`ca-cert` is the certificate of the certificate authority by Dirk to sign the client certificate. This is required if Dirk is using its own certificate authority to generate client certificates (which is the usual case). + +### accounts +`accounts` is a list of accounts that Vouch will request from Dirk. This is an account specifier, and can be supplied in various forms for example: + + - **`wallet`** will return all accounts in _wallet_ + - **`wallet/Validator.*`** will return all accounts in _wallet_ starting with _Validator_ + - **`wallet/Validator.*[02468]`** will return all accounts in _wallet_ starting with _Validator_ and ending in an even number + +At least one account specifier is required for the Dirk account manager. + +## `wallet` +The `wallet` account manager obtains account information from local wallets, and signs locally. It supports wallets created by [ethdo](https://github.com/wealdtech/ethdo). + +The basic configuration for using wallet is as follows: +```YAML +accountmanager: + wallet: + locations: + - /home/me/wallets + accounts: + - my valdiators + passphrases: + - file:///home/me/secrets/passphrase +``` + +Each item is explained in more detail below. + +### locations +`locations` is the list of locations to search for local wallets. + +If no locations are supplied, the [default location for wallets](https://github.com/wealdtech/go-eth2-wallet-store-filesystem#usage) will be used. + +### accounts +`accounts` is the list of accounts that Vouch will request from Dirk. This is an account specifier, and can be supplied in various forms for example: + + - **`wallet`** will return all accounts in _wallet_ + - **`wallet/Validator.*`** will return all accounts in _wallet_ starting with _Validator_ + - **`wallet/Validator.*[02468]`** will return all accounts in _wallet_ starting with _Validator_ and ending in an even number + +At least one account specifier is required for the Dirk account manager. + +### passphrases +`passphrases` is a list of passphrases that will be used to unlock the accounts. Each item in the list is a [Majordomo](https://github.com/wealdtech/go-majordomo) URL. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..2086b72 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,77 @@ +# Configuration +Vouch can be configured through environment, command-line or configuration file. In the case of conflicting configuration the order of precedence is: + + - command-line; then + - environment; then + - configuration file. + +# The configuration file +Vouch's configuration file can be written in JSON or YAML. The file can either be in the user's home directory, in which case it will be called `.vouch.json` (or `.vouch.yml`), or it can be in a directory specified by the command line option `--base-dir` or environment variable `VOUCH_BASE_DIR`, in which case it will be called `vouch.json` (or `vouch.yml`). + +A sample configuration file in YAML with is shown below: + +``` +# log-file is the location for Vouch log output. If this is not provided logs will be written to the console. +log-file: /home/me/vouch.log +# log-level is the global log level for Vouch logging. +log-level: Debug + +# beacon-node-address is the address of the beacon node. Can be prysm, lighthouse, teku +beacon-node-address: localhost:4000 + +# metrics is the module that logs metrics, in this case using prometheus. +metrics: + prometheus: + # log-level is the log level for this module, over-riding the global level. + log-level: warn + # listen-address is the address on which prometheus listens for metrics requests. + listen-address: 0.0.0.0:8081 + +# graffiti provides graffiti data. Full details are in the separate document. +graffiti: + static: + value: My graffiti + +# strategies provide advanced strategies for dealing with multiple beacon nodes +strategies: + beaconblockproposal: + # style can be 'best', which obtains blocks from all nodes and compares them, or 'first', which uses the first returned + style: best + # beacon-node-addresses are the addresses of beacon nodes to use for this strategy. + beacon-node-addresses: + - localhost:4000 + - localhost:5051 + - localhost:5052 +``` + +## Logging +Vouch has a modular logging system that allows different modules to log at different levels. The available log levels are: + + - **Fatal**: messages that result in Vouch stopping immediately; + - **Error**: messages due to Vouch being unable to fulfil a valid process; + - **Warning**: messages that result in Vouch not completing a process due to transient or user issues; + - **Information**: messages that are part of Vouch's normal startup and shutdown process; + - **Debug**: messages when one of Vouch's processes diverge from normal operations; + - **Trace**: messages that detail the flow of Vouch's normal operations; or + - **None**: no messages are written. + +### Global level +The global level is used for all modules that do not have an explicit log level. This can be configured using the command line option `--log-level`, the environment variable `VOUCH_LOG_LEVEL` or the configuration option `log-level`. + +### Module levels +Modules levels are used for each module, overriding the global log level. The available modules are: + + - **accountmanager** access to validating accounts + - **attestationaggregator** aggregating attestations + - **attester** attesting to blocks + - **beaconcommitteesubscriber** subscribing to beacon committees + - **beaconblockproposer** proposing beacon blocks + - **chaintime** calculations for time on the blockchain (start of slot, first slot in an epoch _etc._) + - **controller** control of which jobs occur when + - **graffiti** provision of graffiti for proposed blocks + - **majordomo** accesss to secrets + - **scheduler** starting internal jobs such as proposing a block at the appropriate time + - **strategies.submitter** decisions on how to submit information to multiple beacon nodes + - **strategies.beaconblockproposer** decisions on how to obtain information from multiple beacon nodes + +This can be configured using the environment variables `VOUCH__LOG_LEVEL` or the configuration option `.log-level`. For example, the controller module logging could be configured using the environment variable `VOUCH_CONTROLLER_LOG_LEVEL` or the configuration option `controller.log-level`. diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..d34d16e --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,38 @@ +# Getting started +This document provides steps to set up a Vouch instance using validators in a local wallet. + +It assumes there is a local wallet called "Validators" that has been created by `ethdo`, that the wallet has one or more accounts in it, and that those accounts have been configured as validators on an Ethereum 2 network. + +It also assumes there is an accessible instance of the Ethereum 2 beacon chain that is fully synced with the current state of the chain. + +#### Configuring Vouch +A basic configuration file can be created in the user's home directory with the name `.vouch.yml` and the following contents: + +```YAML +beacon-node-address: localhost:4000 +accountmanager: + wallet: + accounts: + - Validators + passphrases: + - secret +``` + +`beacon-node-address` should be changed to access a suitable beacon node (Prysm, Lighthouse or Teku). + +`secret` should be changed to be the passphrase you used to secure the accounts. + +#### Starting Vouch + +To start Vouch type: +``` +$ vouch +{"level":"info","version":"v0.6.0","time":"2020-09-25T14:46:45+01:00","message":"Starting vouch"} +{"level":"info","time":"2020-09-25T14:46:46+01:00","message":"Starting standard submitter strategy"} +{"level":"info","time":"2020-09-25T14:46:46+01:00","message":"Starting simple beacon block proposal strategy"} +{"level":"info","service":"controller","impl":"standard","accounts":2,"time":"2020-09-25T14:46:46+01:00","message":"Initial validating accounts"} +{"level":"info","time":"2020-09-25T14:46:46+01:00","message":"All services operational"} +``` + + +At this point Vouch is operational and validation for the configured validators should begin. diff --git a/docs/graffiti.md b/docs/graffiti.md new file mode 100644 index 0000000..b76cc13 --- /dev/null +++ b/docs/graffiti.md @@ -0,0 +1,36 @@ +# Graffiti providers + +## Static +The static graffiti provider uses the same single value for all proposed blocks. The graffiti is supplied in the "graffiti.static.value" configuration parameter. For example, to specify the graffiti "my graffiti" in a YAML configuration file the configuration would be: + +```YAML +graffiti: + static: + value: my graffiti +``` + +Note that Ethereum 2 block graffiti is a maximum of 32 bytes in length. + +## Dynamic +The dynamic graffiti provider uses a majordomo URL to obtain one or more values for proposed blocks. The graffiti is supplied in the "graffiti.dynamic.location" configuration parameter. For example, to fetch graffiti from the file "/home/me/graffiti.txt" in a YAML configuration file the configuration would be: + +```YAML +graffiti: + dyanmic: + location: file:///home/me/graffiti.txt +``` + +The location is a [majordomo](majordomo.md) URL. It re-evalulated each time a block is proposed, so if in the above example the contents of the file change the graffiti in the next proposed block will alter likewise. + +The dynamic graffiti provider has a number of additional features. Firstly, the majordomo URL undergoes variable replacement. The variables that are available for replacement are: + + - {{SLOT}} the slot of the block being proposed + - {{VALIDATORINDEX}} the index of the validator proposing the block + +For example, if the majordomo URL was `file:///home/me/graffiti-{{VALIDATORINDEX}}.txt` then a block being propopsed by validator 15 would result in the majordomo URL `file:///home/me/graffiti-15.txt` being used to fetch the graffiti. + +Once the majordomo URL has been resolved the resultant data is separated in to multiple lines (blank lines are removed). If there is more than one line in the data then one of the lines is picked at random (note that vouch retains no memory of which lines have or have not been selected, so it is possible for the same line to be picked multiple times before another line is picked once). + +The graffiti line also undergoes variable replacement, as per above. At this point the final result is used as the graffiti for the proposed block. + +Note that Ethereum 2 block graffiti is a maximum of 32 bytes in length. diff --git a/docs/majordomo.md b/docs/majordomo.md new file mode 100644 index 0000000..88179df --- /dev/null +++ b/docs/majordomo.md @@ -0,0 +1,48 @@ +# Majordomo + +Vouch uses [majordomo](https://github.com/wealdtech/go-majordomo) for many of its data fetching features. This document describes the supported confidants, and their configuration options. + +## Direct confidant +The direct confidant supplies values directly within the URL. The format of the URL is `direct://key`. For example, the URL `direct://Text` would provide the value "Text". + +The direct confidant is configured automatically by Vouch. + +## File confidant +The file confidant fetches values from the location specified by URL. The format of the URL is `file://key` For example, the URL `file:///home/me/file.txt` would provide the contents of the file "/home/me/file.txt". + +The file confidant is configured automatically by Vouch. + +## Google Secret Manager confidant +The Google Secret Manager (GSM) confidant fetches values from [Google Secret Manager](https://cloud.google.com/secret-manager). The format of the URL is `gsm://id@project/key` For example, the URL `gsm:///me@myproject/mysecret` would provide the contents of the secret labelled "mykey" in the project "myproject". + +The GSM confidant has two configuration options. Credentials are required to allow majordomo to access the secrets. These are service account credentials in JSON format, available from the Google cloud console. The path to the credentials file is supplied in the "majordomo.gsm.credentials" configuration parameter. + +The second configuration option is the project ID. This is optional, and can be supplied directly in the majordomo URL if required as seen above. If the project ID is supplied as a configuration option the majordomo URL can be shorted to the form `gsm://id/key`. + +For example, to specify the GSM credentials and project in a YAML configuration file the configuration would be: + +```YAML +majordomo: + gsm: + credentials: /home/me/gsmcredentials.json + project: my_project +``` + +## AWS Secrets Manager confidant +The AWS Secrets Manager (ASM) confidant fetches values from [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/). The format of the URL is `asm://id:secret@region/key` For example, the URL `asm:///AKIAITXFKX5JWOXJDJKA:8R06MHGKayTFHkuK8@eu-central-1/mysecret` would provide the contents of the secret "mysecret" from the region "eu-central-1". + +The ASM confidant has three configuration options. Region is required to inform majordomo form where to fetch secrets. This is an Amazon region, such as "us-east-1" or "ap-southeast-2". The region is supplied in the "majordomo.asm.region" configuration parameter. + +The second and third configuration options are the ID and secret of an AWS account that has access to read the secrets. These values are supplied in the "majordomo.asm.id" and "majordomo.asm.secret" configuration parameters, respectively. + +If the parameters are supplied in the configuration they are not required to be supplied in the majordomo URL as well. If all parameters are supplied in the configuration then the URLs can simply be of the form `asm://key`. + +For example, to specify the ASM credentials and region in a YAML configuration file the configuration would be: + +```YAML +majordomo: + asm: + id: AKIAITXFKX5JWOXJDJKA + secret: 8R06MHGKayTFHkuK8 + region: eu-central-1 +``` diff --git a/docs/metrics/prometheus.md b/docs/metrics/prometheus.md new file mode 100644 index 0000000..72028c9 --- /dev/null +++ b/docs/metrics/prometheus.md @@ -0,0 +1,69 @@ +# Prometheus metrics +vouch provides a number of metrics to check the health and performance of its activities. vouch's default implementation uses Prometheus to provide these metrics. The metrics server listens on the address provided by the `metrics.address` configuration value. + +## Health +Health metrics provide a mechanism to confirm if vouch is active. Due to vouch's nature there are multiple metrics that can be used to provide health and activity information that can be monitored. + +`vouch_start_time_secs` is the Unix timestamp at which vouch was started. This value will remain the same throughout a run of vouch; if it increments it implies that vouch has restarted. + +`vouch_epochs_processed_total` the number of epochs vouch has processed. This number resets to 0 when vouch restarts, and increments every time vouch starts to process an epoch; if it fails to increment it implies that vouch has stopped processing. + +`vouch_accountmanager_accounts_total` is the number of accounts for which vouch is validating. This metric has one label, `state`, which can take one of the following values: + - `unknown` the validator is not known to the Ethereum 2 network + - `pending_initialized` the validator is known to the Ethereum 2 network but not yet in the queue to be activated + - `pending_queued` the validator is in the queue to be activated + - `active_ongoing` the validator is active + - `active_exiting` the validator is active but stopping its duties + - `exited_unslashed` the validator has exited without being slashed + - `exited_slashed` the validator has exited after being slashed + - `withdrawal_possible` the validator's funds are applicable for withdrawal (although withdrawal is not possible in phase 0) + +Vouch will attest for accounts that are either `active_ongoing` or `active_exiting`. Any increase in `active_exiting` should be matched with valid exit requests. Any increase in `active_slashed` suggests a problem with the validator setup that should be investigated as a matter of urgency. + +There are also counts for each process. The specific metrics are: + + - `vouch_beaconblockproposal_process_requests_total` number of beacon block proposal processes; + - `vouch_attestation_process_requests_total` number of attestation processes; + - `vouch_beaconcommitteesubscription_process_requests_total` number of beacon committee subscription processes; and + - `vouch_attestationaggregation_process_failure_total` number of attestation aggregation processes. + +All of the metrics have the label "result" with the value either "succeeded" or "failed2. Any increase in the latter values implies the validator is not completing all of its activities, and should be investigated. + +## Performance +Performance metrics provide a mechanism to understand how quickly vouch is carrying out its activities. The following information is provided: + + - `vouch_beaconblockproposal_process_duration_seconds` time taken to carry out the beacon block proposal process; + - `vouch_attestation_process_duration_seconds` time taken to carry out the attestation process; + - `vouch_beaconcommitteesubscription_process_duration_seconds` time taken to carry out the beacon committee subscription process; and + - `vouch_attestationaggregation_process_duration_seconds` time taken to carry out the attestation aggregation process. + +These metrics are provided as histograms, with buckets in increments of 0.1 seconds up to 1 second. + +## Operations +Operations metrics provide information about numbers of operations performed. These are generally lower-level information that can be useful to monitor activities for fine-tuning of server parameters, comparing one instance to another, _etc._ + +Vouch's job scheduler provides a number of metrics. The specific metrics are: + + - `vouch_scheduler_jobs_scheduled_total` number of jobs scheduled; + - `vouch_scheduler_jobs_cancelled_total` number of jobs cancelled; and + - `vouch_scheduler_jobs_started_total` number of jobs started. This has a label `trigger` which can be "timer" if the job ran due to reaching its designated start time or "signal" if the job ran due to being triggered before its designated start time. + +## Client operations +Client operations metrics provide information about the response time of beacon nodes, as well as if they returned + +`vouch_client_opeation_duration_seconds` is provided as a histogram, with buckets in increments of 0.1 seconds up to 4 seconds. It has two labels: + + - `proposer` is the endpoint for the operation + - `operation` is the operation that took place (_e.g._ "beacon block proposal") + +There is also a companion metric `vouch_client_operation_requests_total`, which is a simple count of the number of operations that have taken place. It has three labels: + + - `proposer` is the endpoint for the operation + - `operation` is the operation that took place (_e.g._ "beacon block proposal") + - `result` is the result of the operation, either "succeeded" or "failed" + +## Network +Network metrics provide information about the network from vouch's point of view. Although these are not under vouch's control, they have an impact on the performance of the validator. The specific metrics are: + + - `vouch_block_receipt_delay_seconds` the delay between the start of a slot and the arrival of the block for that slot. This metric is provided as a histogram, with buckets in increments of 0.1 seconds up to 4 seconds. + - `vouch_attestationaggregation_coverage_ratio` the ratio of the number of attestations included in the aggregate to the total number of attestations for the aggregate. This metric is provided as a histogram, with buckets in increments of 0.1 up to 1. diff --git a/go.mod b/go.mod index 5e7c55c..1ef5031 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,53 @@ module github.com/attestantio/vouch go 1.14 require ( + cloud.google.com/go v0.66.0 // indirect + github.com/OneOfOne/xxhash v1.2.5 // indirect + github.com/attestantio/go-eth2-client v0.6.4 + github.com/aws/aws-sdk-go v1.34.31 + github.com/ferranbt/fastssz v0.0.0-20200826142241-3a913c5a1313 + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.15.0 // indirect + github.com/herumi/bls-eth-go-binary v0.0.0-20200923072303-32b29e5d8cbf + github.com/magiconair/properties v1.8.4 // indirect + github.com/minio/highwayhash v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 - github.com/rs/zerolog v1.18.0 - github.com/spf13/pflag v1.0.3 - github.com/spf13/viper v1.7.0 + github.com/opentracing/opentracing-go v1.2.0 + github.com/pelletier/go-toml v1.8.1 // indirect + github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.7.1 + github.com/prometheus/common v0.14.0 // indirect + github.com/prometheus/procfs v0.2.0 // indirect + github.com/prysmaticlabs/ethereumapis v0.0.0-20200923224139-64c46fb1b0fa + github.com/prysmaticlabs/go-bitfield v0.0.0-20200618145306-2ae0807bef65 + github.com/rs/zerolog v1.20.0 + github.com/sasha-s/go-deadlock v0.2.0 + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/afero v1.4.0 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.6.1 + github.com/uber/jaeger-client-go v2.25.0+incompatible + github.com/uber/jaeger-lib v2.3.0+incompatible // indirect + github.com/wealdtech/go-bytesutil v1.1.1 + github.com/wealdtech/go-eth2-types/v2 v2.5.0 + github.com/wealdtech/go-eth2-wallet v1.14.0 + github.com/wealdtech/go-eth2-wallet-dirk v1.0.3 + github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.1 + github.com/wealdtech/go-eth2-wallet-types/v2 v2.7.0 + github.com/wealdtech/go-majordomo v1.0.1 + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321 // indirect + golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 + golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d // indirect + google.golang.org/api v0.32.0 // indirect + google.golang.org/genproto v0.0.0-20200925023002-c2d885f95484 // indirect + google.golang.org/grpc v1.32.0 + gopkg.in/ini.v1 v1.61.0 // indirect ) + +replace github.com/attestantio/go-eth2-client => ../go-eth2-client diff --git a/go.sum b/go.sum index b38abe8..395d902 100644 --- a/go.sum +++ b/go.sum @@ -5,71 +5,247 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0 h1:RmDygqvj27Zf3fCQjQRtLyC7KwFcHkeJitcO0OoGOcA= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.66.0 h1:DZeAkuQGQqnm9Xv36SbMJEU8aFBz4wL04UpMWPWwjzg= +cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI= +github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/attestantio/go-eth2-client v0.6.4 h1:EfnOCWlPrmLXBxMyctXUOEIcn61/QnYvnIBz5aIH8Bg= +github.com/attestantio/go-eth2-client v0.6.4/go.mod h1:lYEayGHzZma9HMUJgyxFIzDWRck8n2IedP7KTkIwe0g= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.33.5/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.33.17 h1:vngPRchZs603qLtJH7lh2pBCDqiFxA9+9nDWJ5WYJ5A= +github.com/aws/aws-sdk-go v1.33.17/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.34.31 h1:408wh5EHKzxyby8JpYfnn1w3fsF26AIU0o1kbJoRy7E= +github.com/aws/aws-sdk-go v1.34.31/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +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/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 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/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= +github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI= +github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/ferranbt/fastssz v0.0.0-20200514094935-99fccaf93472 h1:maoKvILdMk6CSWHanFcUdxXIZGKD9YpWIaVbUQ/4kfg= +github.com/ferranbt/fastssz v0.0.0-20200514094935-99fccaf93472/go.mod h1:LlFXPmgrgVYsuoFDwV8rDJ9tvt1pLQdjKvU1b5IRES0= +github.com/ferranbt/fastssz v0.0.0-20200728110133-0b6e349af87a/go.mod h1:DyEu2iuLBnb/T51BlsiO3yLYdJC6UbGMrIkqK1KmQxM= +github.com/ferranbt/fastssz v0.0.0-20200826142241-3a913c5a1313 h1:8DS7uDmUkGF6UKNU1HivEsjrTusxLPb05KUr/D8ONWQ= +github.com/ferranbt/fastssz v0.0.0-20200826142241-3a913c5a1313/go.mod h1:DyEu2iuLBnb/T51BlsiO3yLYdJC6UbGMrIkqK1KmQxM= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 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-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/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.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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 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/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.13.0 h1:sBDQoHXrOlfPobnKw69FIKa1wg9qsLLvvQ/Y19WtFgI= +github.com/grpc-ecosystem/grpc-gateway v1.13.0/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/grpc-ecosystem/grpc-gateway v1.14.7 h1:Nk5kuHrnWUTf/0GL1a/vchH/om9Ap2/HnVna+jYZgTY= +github.com/grpc-ecosystem/grpc-gateway v1.14.7/go.mod h1:oYZKL012gGh6LMyg/xA7Q2yq6j8bu0wa+9w14EEthWU= +github.com/grpc-ecosystem/grpc-gateway v1.15.0 h1:ntPNC9TD/6l2XDenJZe6T5lSMg95thpV9sGAqHX4WU8= +github.com/grpc-ecosystem/grpc-gateway v1.15.0/go.mod h1:vO11I9oWA+KsxmfFQPhLnnIb1VDE24M+pdxZFiuZcA8= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -80,6 +256,7 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 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= @@ -89,24 +266,72 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/herumi/bls-eth-go-binary v0.0.0-20200706085701-832d8c2c0f7d h1:P8yaFmLwc5ZlUx2sHuawcdQvpv5/0GM+WEGJ07ljN3g= +github.com/herumi/bls-eth-go-binary v0.0.0-20200706085701-832d8c2c0f7d/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= +github.com/herumi/bls-eth-go-binary v0.0.0-20200722032157-41fc56eba7b4 h1:TfBVK1MJ9vhrMXWVHu5p/MlVHZTeCGgDAEu5RykVZeI= +github.com/herumi/bls-eth-go-binary v0.0.0-20200722032157-41fc56eba7b4/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= +github.com/herumi/bls-eth-go-binary v0.0.0-20200923072303-32b29e5d8cbf h1:Lw7EOMVxu3O+7Ro5bqn9M20a7GwuCqZQsmdXNzmcKE4= +github.com/herumi/bls-eth-go-binary v0.0.0-20200923072303-32b29e5d8cbf/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= +github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= +github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= +github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0= +github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +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/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -117,73 +342,297 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +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/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 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/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/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.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.13.0 h1:vJlpe9wPgDRM1Z+7Wj3zUUjY1nr6/1jNKyl7llliccg= +github.com/prometheus/common v0.13.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= +github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/protolambda/zssz v0.1.3/go.mod h1:a4iwOX5FE7/JkKA+J/PH0Mjo9oXftN6P8NZyL28gpag= +github.com/protolambda/zssz v0.1.5 h1:7fjJjissZIIaa2QcvmhS/pZISMX21zVITt49sW1ouek= +github.com/protolambda/zssz v0.1.5/go.mod h1:a4iwOX5FE7/JkKA+J/PH0Mjo9oXftN6P8NZyL28gpag= +github.com/prysmaticlabs/ethereumapis v0.0.0-20200812153649-a842fc47c2c3 h1:0f++UXRfp4/Mrmlfj3UaCnYj2lPr6El0gWWTBb9MD2Y= +github.com/prysmaticlabs/ethereumapis v0.0.0-20200812153649-a842fc47c2c3/go.mod h1:k7b2dxy6RppCG6kmOJkNOXzRpEoTdsPygc2aQhsUsZk= +github.com/prysmaticlabs/ethereumapis v0.0.0-20200826112054-b9e8bd145725 h1:ui38FW8BoWEXQ8VnDGI+4LTrQvRncyojGP7ZCrclIcM= +github.com/prysmaticlabs/ethereumapis v0.0.0-20200826112054-b9e8bd145725/go.mod h1:k7b2dxy6RppCG6kmOJkNOXzRpEoTdsPygc2aQhsUsZk= +github.com/prysmaticlabs/ethereumapis v0.0.0-20200923224139-64c46fb1b0fa h1:UuO9ndtrESi7kHJZZ17aXqpdwA6VDSqgfoQXWKI3dco= +github.com/prysmaticlabs/ethereumapis v0.0.0-20200923224139-64c46fb1b0fa/go.mod h1:k7b2dxy6RppCG6kmOJkNOXzRpEoTdsPygc2aQhsUsZk= +github.com/prysmaticlabs/go-bitfield v0.0.0-20191017011753-53b773adde52/go.mod h1:hCwmef+4qXWjv0jLDbQdWnL0Ol7cS7/lCSS26WR+u6s= +github.com/prysmaticlabs/go-bitfield v0.0.0-20200322041314-62c2aee71669 h1:cX6YRZnZ9sgMqM5U14llxUiXVNJ3u07Res1IIjTOgtI= +github.com/prysmaticlabs/go-bitfield v0.0.0-20200322041314-62c2aee71669/go.mod h1:hCwmef+4qXWjv0jLDbQdWnL0Ol7cS7/lCSS26WR+u6s= +github.com/prysmaticlabs/go-bitfield v0.0.0-20200618145306-2ae0807bef65 h1:hJfAWrlxx7SKpn4S/h2JGl2HHwA1a2wSS3HAzzZ0F+U= +github.com/prysmaticlabs/go-bitfield v0.0.0-20200618145306-2ae0807bef65/go.mod h1:hCwmef+4qXWjv0jLDbQdWnL0Ol7cS7/lCSS26WR+u6s= +github.com/prysmaticlabs/go-ssz v0.0.0-20200101200214-e24db4d9e963/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc= +github.com/prysmaticlabs/go-ssz v0.0.0-20200612203617-6d5c9aa213ae h1:7qd0Af1ozWKBU3c93YW2RH+/09hJns9+ftqWUZyts9c= +github.com/prysmaticlabs/go-ssz v0.0.0-20200612203617-6d5c9aa213ae/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8= -github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/rs/zerolog v1.19.0 h1:hYz4ZVdUgjXTBUmrkrw55j1nHx68LfOKIQk5IYtyScg= +github.com/rs/zerolog v1.19.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sasha-s/go-deadlock v0.2.0 h1:lMqc+fUb7RrFS3gQLtoQsJ7/6TV/pAIFvBsqX73DK8Y= +github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w= +github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +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 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.3.4 h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc= +github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8= +github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 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/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U= +github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= +github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/uber/jaeger-lib v2.3.0+incompatible h1:B/kUIXcj6kIU3WSXgeJ7/uYj94I/r0LDa//JKgN/Sf0= +github.com/uber/jaeger-lib v2.3.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/wealdtech/eth2-signer-api v1.5.2 h1:3jw8MW0r7KlX9bme0q6j+QMa8osRhEnKLkgkECH6xcU= +github.com/wealdtech/eth2-signer-api v1.5.2/go.mod h1:5wlLQ7NO7nbXo3znJOwIWHN8S4C3xHcZ0uOg9Ue4mvg= +github.com/wealdtech/go-bytesutil v1.0.1/go.mod h1:jENeMqeTEU8FNZyDFRVc7KqBdRKSnJ9CCh26TcuNb9s= +github.com/wealdtech/go-bytesutil v1.1.1 h1:ocEg3Ke2GkZ4vQw5lp46rmO+pfqCCTgq35gqOy8JKVc= +github.com/wealdtech/go-bytesutil v1.1.1/go.mod h1:jENeMqeTEU8FNZyDFRVc7KqBdRKSnJ9CCh26TcuNb9s= +github.com/wealdtech/go-ecodec v1.1.0 h1:yggrTSckcPJRaxxOxQF7FPm21kgE8WA6+f5jdq5Kr8o= +github.com/wealdtech/go-ecodec v1.1.0/go.mod h1:PSdBFEB6cltdT7V4E1jbboufMZTZXcQOKG/2PeEjKK4= +github.com/wealdtech/go-eth2-types/v2 v2.5.0 h1:L8sl3yoICAbn3134CBLNUt0o5h2voe0Es2KD5O9r8YQ= +github.com/wealdtech/go-eth2-types/v2 v2.5.0/go.mod h1:321w9X26lAnNa/lQJi2A6Lap5IsNORoLwFPoJ1i8QvY= +github.com/wealdtech/go-eth2-util v1.5.0/go.mod h1:0PGWeWWc6qjky/aNjdPdguJdZ2HSEHHCA+3cTjvT+Hk= +github.com/wealdtech/go-eth2-util v1.6.0 h1:l2OR0SqfYdEnb1I1Ggnk0w+B9/LA5aHdQ2KK2FPnGkY= +github.com/wealdtech/go-eth2-util v1.6.0/go.mod h1:0PGWeWWc6qjky/aNjdPdguJdZ2HSEHHCA+3cTjvT+Hk= +github.com/wealdtech/go-eth2-wallet v1.12.0/go.mod h1:ouV+YSMbzk2dyecmofm8jhaMKdSigdIPMSnSqmWEfW8= +github.com/wealdtech/go-eth2-wallet v1.14.0 h1:eZjopWDMlCHCE9SLydAXBr0Cqtm2HWL3O56ELfeGK9c= +github.com/wealdtech/go-eth2-wallet v1.14.0/go.mod h1:HrJ4hLcTPZPjhdkZKZd07OFxA+r7d3i+7XKjYJbxdxk= +github.com/wealdtech/go-eth2-wallet-dirk v1.0.2 h1:ZxAdF6iTOzYHtQlWd1nzVevZ+HtXS/LLn580t+NXT3A= +github.com/wealdtech/go-eth2-wallet-dirk v1.0.2/go.mod h1:5jK/aEAjYAVRBKKjYAvJWSmOWxiECs4asYXHwloNI+w= +github.com/wealdtech/go-eth2-wallet-dirk v1.0.3 h1:NWwxzYjFG3k7S9mzVOvj02A5S9QOak4uh+MJF2Ae37w= +github.com/wealdtech/go-eth2-wallet-dirk v1.0.3/go.mod h1:WIy6Xx0FgTNG5bP7IytxsEt8y1Yl46HbsWVFbE4Yc94= +github.com/wealdtech/go-eth2-wallet-distributed v1.1.0 h1:OZjjuxcIYo+EhAfph7lYP1z+VeNs9ruOI32kqtYe1Jg= +github.com/wealdtech/go-eth2-wallet-distributed v1.1.0/go.mod h1:8r06Vpg/315/7Hl9CXq0ShQP8/cgUrBGzKKo6ywA4yQ= +github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.0 h1:CWb82xeNaZQt1Z829RyDALUy7UZbc6VOfTS+82jRdEQ= +github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.0/go.mod h1:JelKMM10UzDJNXdIcojMj6SCIsHC8NYn4c1S2FFk7OQ= +github.com/wealdtech/go-eth2-wallet-hd/v2 v2.3.0/go.mod h1:Kc/8WcqMTczfH2xy5mDfCRd0NI/ca/j2jXmqJ7gz8yk= +github.com/wealdtech/go-eth2-wallet-hd/v2 v2.5.0 h1:OhJm7hn6vlO+dazs5S1EBrZu/ZVQUQcaNw1ncfy0/xI= +github.com/wealdtech/go-eth2-wallet-hd/v2 v2.5.0/go.mod h1:LfgcOnQeBcqBEoHd4VNqwZhwqzz0Xh1DqnDmjHWONGs= +github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.0 h1:L1aPK9nc+8Ctcw+8I05vM6408weFc4a5RtLQDUeS0eE= +github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.0/go.mod h1:e2q2uuEdq5+B3GE7jk+Mi9oz9V5nPPKXcXRg1XYavsU= +github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.1 h1:l9YV6OBqcxp5fjscK63lzuCUIye8ANACjJdpm5ULGS8= +github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.1/go.mod h1:Zxhj/4i8nRpk4LTTqFKbfI2KyvO3uqLMerNXqKZKDK0= +github.com/wealdtech/go-eth2-wallet-store-s3 v1.8.0/go.mod h1:OxYD+d79StAOHigNaI5bWuvjhanEyrD4MqTj8hIvt2Y= +github.com/wealdtech/go-eth2-wallet-store-s3 v1.9.0 h1:O5211UskLbK1WDecTXwugUlINDBQ26MqtiFn6u66fmA= +github.com/wealdtech/go-eth2-wallet-store-s3 v1.9.0/go.mod h1:dcQPLsRRYDiMV0DFYzTX6HRpP9WP+gWreAX5SLBOJ0I= +github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.0/go.mod h1:XtXHbl4OV/XenQsvGmXbh+bVXaGS788oa30DB7kDInA= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.5.0 h1:J29mbkSCUMl2xdu8Lg6U+JptFGfmli6xl04DAHtq9aM= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.5.0/go.mod h1:X9kYUH/E5YMqFMZ4xL6MJanABUkJGaH/yPZRT2o+yYA= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.6.0 h1:vBrH5icPPSeb14cdShA7/P2PBZOgZscJ2IhBlTIaFrA= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.6.0/go.mod h1:X9kYUH/E5YMqFMZ4xL6MJanABUkJGaH/yPZRT2o+yYA= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.7.0 h1:pquFQdIWEiSYrpIpFuvsRuialI8t9KhFsPvbIBPnzic= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.7.0/go.mod h1:X9kYUH/E5YMqFMZ4xL6MJanABUkJGaH/yPZRT2o+yYA= +github.com/wealdtech/go-indexer v1.0.0 h1:/S4rfWQbSOnnYmwnvuTVatDibZ8o1s9bmTCHO16XINg= +github.com/wealdtech/go-indexer v1.0.0/go.mod h1:u1cjsbsOXsm5jzJDyLmZY7GsrdX8KYXKBXkZcAmk3Zg= +github.com/wealdtech/go-majordomo v1.0.1 h1:wo4e0hZmCquhz/l8T/PHLr6hF9+hPY65BM1uRdFXtc8= +github.com/wealdtech/go-majordomo v1.0.1/go.mod h1:QoT4S1nUQwdQK19+CfepDwV+Yr7cc3dbF+6JFdQnIqY= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +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.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191105034135-c7e5f84aec59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -193,54 +642,146 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 h1:2mqDk8w/o6UmeUCu5Qiq2y7iMf6anbx+YA8d1JFoFrs= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs= +golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321 h1:lleNcKRbcaC8MqgLwghIkzZ2JBQAb7QQ9MiwRt1BisA= +golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 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 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/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-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6 h1:X9xIZ1YU8bLZA3l6gqDUHSFiD0GFI9S548h6C8nDtOY= +golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200819171115-d785dc25833f h1:KJuwZVtZBVzDmEDtB2zro9CXkD9O0dpCv4o2LHbQIAw= +golang.org/x/sys v0.0.0-20200819171115-d785dc25833f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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= @@ -249,6 +790,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -256,42 +798,199 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7 h1:LHW24ah7B+uV/OePwNP0p/t889F3QSyLvY8Sg/bK0SY= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo= +google.golang.org/api v0.32.0 h1:Le77IccnTqEa8ryp9wIpX5W3zYm7Gf9LhOp9PHcwFts= +google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200528191852-705c0b31589b h1:nl5tymnV+50ACFZUDAP+xFCe3Zh3SWdMDx+ernZSKNA= +google.golang.org/genproto v0.0.0-20200528191852-705c0b31589b/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200722002428-88e341933a54 h1:ASrBgpl9XvkNTP0m39/j18mid7aoF21npu2ioIBxYnY= +google.golang.org/genproto v0.0.0-20200722002428-88e341933a54/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200731012542-8145dea6a485/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70 h1:wboULUXGF3c5qdUnKp+6gLAccE6PRpa/czkYvQ4UXv8= +google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200925023002-c2d885f95484 h1:Rr9EZdYRq2WLckzJQVtN3ISKoP7dvgwi7jbglILNZ34= +google.golang.org/genproto v0.0.0-20200925023002-c2d885f95484/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60= +gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10= +gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/loggers/jaeger.go b/loggers/jaeger.go new file mode 100644 index 0000000..8813658 --- /dev/null +++ b/loggers/jaeger.go @@ -0,0 +1,38 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loggers + +import ( + "github.com/rs/zerolog" +) + +// JaegerLogger provides the Jaeger logging interface with a zerolog backend. +type JaegerLogger struct { + log zerolog.Logger +} + +// NewJaegerLogger creates a new Jaeger logger with a zerolog backend. +func NewJaegerLogger(log zerolog.Logger) *JaegerLogger { + return &JaegerLogger{log: log} +} + +// Error logs an error. +func (l *JaegerLogger) Error(msg string) { + l.log.Error().Msg(msg) +} + +// Infof logs information. +func (l *JaegerLogger) Infof(msg string, args ...interface{}) { + l.log.Info().Msgf(msg, args...) +} diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..b0ae8ef --- /dev/null +++ b/logging.go @@ -0,0 +1,71 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// log. +var log zerolog.Logger + +// initLogging initialises logging. +func initLogging() error { + // We set the global logging level to trace, because if the global log level is higher than the + // local log level the local level is ignored. It is then overridden for each module. + zerolog.SetGlobalLevel(zerolog.TraceLevel) + + // Change the output file. + if viper.GetString("log-file") != "" { + f, err := os.OpenFile(resolvePath(viper.GetString("log-file")), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return errors.Wrap(err, "failed to open log file") + } + zerologger.Logger = zerologger.Logger.Output(f) + } + + // Set the local logger from the global logger. + log = zerologger.Logger.With().Logger().Level(logLevel(viper.GetString("log-level"))) + + return nil +} + +// logLevel converts a string to a log level. +// It returns the user-supplied level by default. +func logLevel(input string) zerolog.Level { + switch strings.ToLower(input) { + case "none": + return zerolog.Disabled + case "trace": + return zerolog.TraceLevel + case "debug": + return zerolog.DebugLevel + case "warn", "warning": + return zerolog.WarnLevel + case "info", "information": + return zerolog.InfoLevel + case "err", "error": + return zerolog.ErrorLevel + case "fatal": + return zerolog.FatalLevel + default: + return log.GetLevel() + } +} diff --git a/main.go b/main.go index d26bca0..ab410e0 100644 --- a/main.go +++ b/main.go @@ -14,84 +14,636 @@ package main import ( + "context" "fmt" + "io" "net/http" + "os" + "os/signal" + "path/filepath" "runtime" + "runtime/debug" "strings" + "syscall" + "time" + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/loggers" + "github.com/attestantio/vouch/services/accountmanager" + dirkaccountmanager "github.com/attestantio/vouch/services/accountmanager/dirk" + walletaccountmanager "github.com/attestantio/vouch/services/accountmanager/wallet" + standardattestationaggregator "github.com/attestantio/vouch/services/attestationaggregator/standard" + standardattester "github.com/attestantio/vouch/services/attester/standard" + standardbeaconblockproposer "github.com/attestantio/vouch/services/beaconblockproposer/standard" + standardbeaconcommitteesubscriber "github.com/attestantio/vouch/services/beaconcommitteesubscriber/standard" + standardchaintime "github.com/attestantio/vouch/services/chaintime/standard" + standardcontroller "github.com/attestantio/vouch/services/controller/standard" + "github.com/attestantio/vouch/services/graffitiprovider" + dynamicgraffitiprovider "github.com/attestantio/vouch/services/graffitiprovider/dynamic" + staticgraffitiprovider "github.com/attestantio/vouch/services/graffitiprovider/static" + "github.com/attestantio/vouch/services/metrics" + nullmetrics "github.com/attestantio/vouch/services/metrics/null" + prometheusmetrics "github.com/attestantio/vouch/services/metrics/prometheus" + basicscheduler "github.com/attestantio/vouch/services/scheduler/basic" + "github.com/attestantio/vouch/services/submitter" + immediatesubmitter "github.com/attestantio/vouch/services/submitter/immediate" + multinodesubmitter "github.com/attestantio/vouch/services/submitter/multinode" + bestbeaconblockproposalstrategy "github.com/attestantio/vouch/strategies/beaconblockproposal/best" + firstbeaconblockproposalstrategy "github.com/attestantio/vouch/strategies/beaconblockproposal/first" + "github.com/aws/aws-sdk-go/aws/credentials" homedir "github.com/mitchellh/go-homedir" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" + "github.com/opentracing/opentracing-go" + "github.com/pkg/errors" + zerologger "github.com/rs/zerolog/log" "github.com/spf13/pflag" "github.com/spf13/viper" + jaegerconfig "github.com/uber/jaeger-client-go/config" + e2types "github.com/wealdtech/go-eth2-types/v2" + majordomo "github.com/wealdtech/go-majordomo" + asmconfidant "github.com/wealdtech/go-majordomo/confidants/asm" + directconfidant "github.com/wealdtech/go-majordomo/confidants/direct" + fileconfidant "github.com/wealdtech/go-majordomo/confidants/file" + gsmconfidant "github.com/wealdtech/go-majordomo/confidants/gsm" + standardmajordomo "github.com/wealdtech/go-majordomo/standard" ) func main() { - fetchConfig() + ctx, cancel := context.WithCancel(context.Background()) - initLogging() + if err := fetchConfig(); err != nil { + zerologger.Fatal().Err(err).Msg("Failed to fetch configuration") + } - initProfiling() + if err := initLogging(); err != nil { + log.Fatal().Err(err).Msg("Failed to initialise logging") + } - log.Info().Str("version", "v0.1.0").Msg("Starting vouch") + majordomo, err := initMajordomo(ctx) + if err != nil { + log.Fatal().Err(err).Msg("Failed to initialise majordomo") + } + + logModules() + log.Info().Str("version", "v0.6.0").Msg("Starting vouch") + + if err := initProfiling(); err != nil { + log.Fatal().Err(err).Msg("Failed to initialise profiling") + } + + closer, err := initTracing() + if err != nil { + log.Fatal().Err(err).Msg("Failed to initialise tracing") + } + if closer != nil { + defer closer.Close() + } + + runtime.GOMAXPROCS(runtime.NumCPU() * 8) + + if err := e2types.InitBLS(); err != nil { + log.Fatal().Err(err).Msg("Failed to initialise BLS library") + } + + if err := startServices(ctx, majordomo); err != nil { + log.Fatal().Err(err).Msg("Failed to initialise services") + } + + log.Info().Msg("All services operational") + + // Wait for signal. + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + for { + sig := <-sigCh + if sig == syscall.SIGINT || sig == syscall.SIGTERM || sig == os.Interrupt || sig == os.Kill { + cancel() + break + } + } + + log.Info().Msg("Stopping vouch") } // fetchConfig fetches configuration from various sources. -func fetchConfig() { - // Configuration. +func fetchConfig() error { + pflag.String("base-dir", "", "base directory for configuration files") pflag.String("log-level", "info", "minimum level of messsages to log") - pflag.String("profile-address", "", "Address on which to run profile server") + pflag.String("log-file", "", "redirect log output to a file") + pflag.String("profile-address", "", "Address on which to run Go profile server") + pflag.String("tracing-address", "", "Address to which to send tracing data") + pflag.String("beacon-node-address", "localhost:4000", "Address on which to contact the beacon node") pflag.Parse() if err := viper.BindPFlags(pflag.CommandLine); err != nil { - panic(fmt.Sprintf("Failed to bind pflags to viper: %v", err)) + return errors.Wrap(err, "failed to bind pflags to viper") } - cfgFile := "" - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) + if viper.GetString("base-dir") != "" { + // User-defined base directory. + viper.AddConfigPath(resolvePath("")) + viper.SetConfigName("vouch") } else { - // Find home directory. + // Home directory. home, err := homedir.Dir() if err != nil { - panic(fmt.Sprintf("Failed to obtain home directory: %v", err)) + return errors.Wrap(err, "failed to obtain home directory") } - - // Search config in home directory with name ".vouch" (without extension). viper.AddConfigPath(home) viper.SetConfigName(".vouch") } + // Environment settings. viper.SetEnvPrefix("VOUCH") - viper.AutomaticEnv() // read in environment variables that match + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + viper.AutomaticEnv() + + // Defaults. + viper.Set("process-concurrency", 16) + viper.Set("eth2client.timeout", 2*time.Minute) if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - panic(fmt.Sprintf("Failed to read configuration file: %v", err)) + return errors.Wrap(err, "failed to read configuration file") + } + } + + return nil +} + +// initTracing initialises the tracing system. +func initTracing() (io.Closer, error) { + tracingAddress := viper.GetString("tracing-address") + if tracingAddress == "" { + return nil, nil + } + cfg := &jaegerconfig.Configuration{ + ServiceName: "vouch", + Sampler: &jaegerconfig.SamplerConfig{ + Type: "probabilistic", + Param: 0.1, + }, + Reporter: &jaegerconfig.ReporterConfig{ + LogSpans: true, + LocalAgentHostPort: tracingAddress, + }, + } + tracer, closer, err := cfg.NewTracer(jaegerconfig.Logger(loggers.NewJaegerLogger(log))) + if err != nil { + return nil, err + } + if tracer != nil { + opentracing.SetGlobalTracer(tracer) + } + return closer, nil +} + +// initProfiling initialises the profiling server. +func initProfiling() error { + profileAddress := viper.GetString("profile-address") + if profileAddress != "" { + go func() { + log.Info().Str("profile_address", profileAddress).Msg("Starting profile server") + runtime.SetMutexProfileFraction(1) + if err := http.ListenAndServe(profileAddress, nil); err != nil { + log.Warn().Str("profile_address", profileAddress).Err(err).Msg("Failed to run profile server") + } + }() + } + return nil +} + +func startServices(ctx context.Context, majordomo majordomo.Service) error { + log.Trace().Msg("Starting metrics service") + monitor, err := startMonitor(ctx) + if err != nil { + return errors.Wrap(err, "failed to start metrics service") + } + + log.Trace().Msg("Starting Ethereum 2 client service") + eth2Client, err := fetchClient(ctx, viper.GetString("beacon-node-address")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to fetch client %q", viper.GetString("beacon-node-address"))) + } + + log.Trace().Msg("Starting chain time service") + chainTime, err := standardchaintime.New(ctx, + standardchaintime.WithLogLevel(logLevel(viper.GetString("chaintime.log-level"))), + standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)), + standardchaintime.WithSlotDurationProvider(eth2Client.(eth2client.SlotDurationProvider)), + standardchaintime.WithSlotsPerEpochProvider(eth2Client.(eth2client.SlotsPerEpochProvider)), + ) + if err != nil { + return errors.Wrap(err, "failed to start chain time service") + } + + log.Trace().Msg("Starting scheduler") + scheduler, err := basicscheduler.New(ctx, + basicscheduler.WithLogLevel(logLevel(viper.GetString("scheduler.log-level"))), + basicscheduler.WithMonitor(monitor.(metrics.SchedulerMonitor)), + ) + if err != nil { + return errors.Wrap(err, "failed to start scheduler service") + } + + log.Trace().Msg("Starting account manager") + accountManager, err := startAccountManager(ctx, monitor, eth2Client, majordomo) + if err != nil { + return errors.Wrap(err, "failed to start account manager") + } + + log.Trace().Msg("Selecting submitter strategy") + submitterStrategy, err := selectSubmitterStrategy(ctx, eth2Client) + if err != nil { + return errors.Wrap(err, "failed to select submitter") + } + + log.Trace().Msg("Starting graffiti provider") + graffitiProvider, err := startGraffitiProvider(ctx, monitor, majordomo) + if err != nil { + return errors.Wrap(err, "failed to start graffiti provider") + } + + log.Trace().Msg("Selecting beacon block proposal provider") + beaconBlockProposalProvider, err := selectBeaconBlockProposalProvider(ctx, monitor, eth2Client) + if err != nil { + return errors.Wrap(err, "failed to select beacon block proposal provider") + } + + log.Trace().Msg("Starting beacon block proposer") + beaconBlockProposer, err := standardbeaconblockproposer.New(ctx, + standardbeaconblockproposer.WithLogLevel(logLevel(viper.GetString("beaconblockproposer.log-level"))), + standardbeaconblockproposer.WithProposalDataProvider(beaconBlockProposalProvider), + standardbeaconblockproposer.WithValidatingAccountsProvider(accountManager.(accountmanager.ValidatingAccountsProvider)), + standardbeaconblockproposer.WithGraffitiProvider(graffitiProvider), + standardbeaconblockproposer.WithMonitor(monitor.(metrics.BeaconBlockProposalMonitor)), + standardbeaconblockproposer.WithBeaconBlockSubmitter(submitterStrategy.(submitter.BeaconBlockSubmitter)), + ) + if err != nil { + return errors.Wrap(err, "failed to start beacon block proposer service") + } + + log.Trace().Msg("Starting beacon block attester") + attester, err := standardattester.New(ctx, + standardattester.WithLogLevel(logLevel(viper.GetString("attester.log-level"))), + standardattester.WithProcessConcurrency(viper.GetInt64("process-concurrency")), + standardattester.WithSlotsPerEpochProvider(eth2Client.(eth2client.SlotsPerEpochProvider)), + standardattester.WithAttestationDataProvider(eth2Client.(eth2client.AttestationDataProvider)), + standardattester.WithAttestationSubmitter(submitterStrategy.(submitter.AttestationSubmitter)), + standardattester.WithMonitor(monitor.(metrics.AttestationMonitor)), + standardattester.WithValidatingAccountsProvider(accountManager.(accountmanager.ValidatingAccountsProvider)), + ) + if err != nil { + return errors.Wrap(err, "failed to start beacon block attester service") + } + + log.Trace().Msg("Starting beacon attestation aggregator") + attestationAggregator, err := standardattestationaggregator.New(ctx, + standardattestationaggregator.WithLogLevel(logLevel(viper.GetString("attestationaggregator.log-level"))), + standardattestationaggregator.WithTargetAggregatorsPerCommitteeProvider(eth2Client.(eth2client.TargetAggregatorsPerCommitteeProvider)), + standardattestationaggregator.WithAggregateAttestationDataProvider(eth2Client.(eth2client.NonSpecAggregateAttestationProvider)), + standardattestationaggregator.WithAggregateAttestationSubmitter(submitterStrategy.(submitter.AggregateAttestationSubmitter)), + standardattestationaggregator.WithMonitor(monitor.(metrics.AttestationAggregationMonitor)), + standardattestationaggregator.WithValidatingAccountsProvider(accountManager.(accountmanager.ValidatingAccountsProvider)), + ) + if err != nil { + return errors.Wrap(err, "failed to start beacon attestation aggregator service") + } + + log.Trace().Msg("Starting beacon committee subscriber service") + beaconCommitteeSubscriber, err := standardbeaconcommitteesubscriber.New(ctx, + standardbeaconcommitteesubscriber.WithLogLevel(logLevel(viper.GetString("beaconcommiteesubscriber.log-level"))), + standardbeaconcommitteesubscriber.WithProcessConcurrency(viper.GetInt64("process-concurrency")), + standardbeaconcommitteesubscriber.WithMonitor(monitor.(metrics.BeaconCommitteeSubscriptionMonitor)), + standardbeaconcommitteesubscriber.WithAttesterDutiesProvider(eth2Client.(eth2client.AttesterDutiesProvider)), + standardbeaconcommitteesubscriber.WithAttestationAggregator(attestationAggregator), + standardbeaconcommitteesubscriber.WithBeaconCommitteeSubmitter(submitterStrategy.(submitter.BeaconCommitteeSubscriptionsSubmitter)), + ) + if err != nil { + return errors.Wrap(err, "failed to start beacon committee subscriber service") + } + + log.Trace().Msg("Starting controller") + _, err = standardcontroller.New(ctx, + standardcontroller.WithLogLevel(logLevel(viper.GetString("controller.log-level"))), + standardcontroller.WithMonitor(monitor.(metrics.ControllerMonitor)), + standardcontroller.WithSlotDurationProvider(eth2Client.(eth2client.SlotDurationProvider)), + standardcontroller.WithSlotsPerEpochProvider(eth2Client.(eth2client.SlotsPerEpochProvider)), + standardcontroller.WithChainTimeService(chainTime), + standardcontroller.WithProposerDutiesProvider(eth2Client.(eth2client.ProposerDutiesProvider)), + standardcontroller.WithAttesterDutiesProvider(eth2Client.(eth2client.AttesterDutiesProvider)), + standardcontroller.WithBeaconChainHeadUpdatedSource(eth2Client.(eth2client.BeaconChainHeadUpdatedSource)), + standardcontroller.WithScheduler(scheduler), + standardcontroller.WithValidatingAccountsProvider(accountManager.(accountmanager.ValidatingAccountsProvider)), + standardcontroller.WithAttester(attester), + standardcontroller.WithBeaconBlockProposer(beaconBlockProposer), + standardcontroller.WithAttestationAggregator(attestationAggregator), + standardcontroller.WithBeaconCommitteeSubscriber(beaconCommitteeSubscriber), + ) + if err != nil { + return errors.Wrap(err, "failed to start controller service") + } + + return nil +} + +func logModules() { + buildInfo, ok := debug.ReadBuildInfo() + if ok { + log.Trace().Str("path", buildInfo.Path).Msg("Main package") + for _, dep := range buildInfo.Deps { + log := log.Trace() + if dep.Replace == nil { + log = log.Str("path", dep.Path).Str("version", dep.Version) + } else { + log = log.Str("path", dep.Replace.Path).Str("version", dep.Replace.Version) + } + log.Msg("Dependency") } } } -// initLogging initialises logging. -func initLogging() { - if strings.ToLower(viper.GetString("log-level")) == "debug" { - zerolog.SetGlobalLevel(zerolog.DebugLevel) +// resolvePath resolves a potentially relative path to an absolute path. +func resolvePath(path string) string { + if filepath.IsAbs(path) { + return path + } + baseDir := viper.GetString("base-dir") + if baseDir == "" { + homeDir, err := homedir.Dir() + if err != nil { + log.Fatal().Err(err).Msg("Could not determine a home directory") + } + baseDir = homeDir + } + return filepath.Join(baseDir, path) +} + +func initMajordomo(ctx context.Context) (majordomo.Service, error) { + majordomo, err := standardmajordomo.New(ctx, + standardmajordomo.WithLogLevel(logLevel(viper.GetString("majordomo.log-level"))), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create majordomo service") + } + + directConfidant, err := directconfidant.New(ctx, + directconfidant.WithLogLevel(logLevel(viper.GetString("majordomo.confidants.direct.log-level"))), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create direct confidant") + } + if err := majordomo.RegisterConfidant(ctx, directConfidant); err != nil { + return nil, errors.Wrap(err, "failed to register direct confidant") + } + + fileConfidant, err := fileconfidant.New(ctx, + fileconfidant.WithLogLevel(logLevel(viper.GetString("majordomo.confidants.file.log-level"))), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create file confidant") + } + if err := majordomo.RegisterConfidant(ctx, fileConfidant); err != nil { + return nil, errors.Wrap(err, "failed to register file confidant") + } + + if viper.GetString("majordomo.asm.region") != "" { + var asmCredentials *credentials.Credentials + if viper.GetString("majordomo.asm.id") != "" { + asmCredentials = credentials.NewStaticCredentials(viper.GetString("majordomo.asm.id"), viper.GetString("majordomo.asm.secret"), "") + } + asmConfidant, err := asmconfidant.New(ctx, + asmconfidant.WithLogLevel(logLevel(viper.GetString("majordomo.confidants.asm.log-level"))), + asmconfidant.WithCredentials(asmCredentials), + asmconfidant.WithRegion(viper.GetString("majordomo.asm.region")), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create AWS secrets manager confidant") + } + if err := majordomo.RegisterConfidant(ctx, asmConfidant); err != nil { + return nil, errors.Wrap(err, "failed to register AWS secrets manager confidant") + } + } + + if viper.GetString("majordomo.gsm.credentials") != "" { + gsmConfidant, err := gsmconfidant.New(ctx, + gsmconfidant.WithLogLevel(logLevel(viper.GetString("majordomo.confidants.gsm.log-level"))), + gsmconfidant.WithCredentialsPath(resolvePath(viper.GetString("majordomo.gsm.credentials"))), + gsmconfidant.WithProject(viper.GetString("majordomo.gsm.project")), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create Google secret manager confidant") + } + if err := majordomo.RegisterConfidant(ctx, gsmConfidant); err != nil { + return nil, errors.Wrap(err, "failed to register Google secret manager confidant") + } + } + + return majordomo, nil +} + +func startMonitor(ctx context.Context) (metrics.Service, error) { + log.Trace().Msg("Starting metrics service") + var monitor metrics.Service + if viper.Get("metrics.prometheus") != nil { + var err error + monitor, err = prometheusmetrics.New(ctx, + prometheusmetrics.WithLogLevel(logLevel(viper.GetString("metrics.prometheus.log-level"))), + prometheusmetrics.WithAddress(viper.GetString("metrics.prometheus.listen-address")), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to start prometheus metrics service") + } + log.Info().Str("listen_address", viper.GetString("metrics.prometheus.listen-address")).Msg("Started prometheus metrics service") } else { - zerolog.SetGlobalLevel(zerolog.InfoLevel) + log.Debug().Msg("No metrics service supplied; monitor not starting") + monitor = nullmetrics.New(ctx) + } + return monitor, nil +} + +func startGraffitiProvider(ctx context.Context, monitor metrics.Service, majordomo majordomo.Service) (graffitiprovider.Service, error) { + switch { + case viper.Get("graffiti.dynamic") != nil: + return dynamicgraffitiprovider.New(ctx, + dynamicgraffitiprovider.WithMajordomo(majordomo), + dynamicgraffitiprovider.WithLogLevel(logLevel(viper.GetString("graffiti.dynamic.log-level"))), + dynamicgraffitiprovider.WithLocation(viper.GetString("graffiti.dynamic.location")), + ) + default: + return staticgraffitiprovider.New(ctx, + staticgraffitiprovider.WithLogLevel(logLevel(viper.GetString("graffiti.static.log-level"))), + staticgraffitiprovider.WithGraffiti([]byte(viper.GetString("graffiti.static.value"))), + ) } } -// initProfiling initialises the profiling server. -func initProfiling() { - profileAddress := viper.GetString("profile-address") - if profileAddress != "" { - go func() { - runtime.SetMutexProfileFraction(1) - if err := http.ListenAndServe(profileAddress, nil); err != nil { - log.Warn().Str("profileAddress", profileAddress).Err(err).Msg("Failed to start profile server") - } else { - log.Info().Str("profileAddress", profileAddress).Msg("Started profile server") +func startAccountManager(ctx context.Context, monitor metrics.Service, eth2Client eth2client.Service, majordomo majordomo.Service) (accountmanager.Service, error) { + var accountManager accountmanager.Service + if viper.Get("accountmanager.dirk") != nil { + certPEMBlock, err := majordomo.Fetch(ctx, viper.GetString("accountmanager.dirk.client-cert")) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain server certificate") + } + keyPEMBlock, err := majordomo.Fetch(ctx, viper.GetString("accountmanager.dirk.client-key")) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain server key") + } + var caPEMBlock []byte + if viper.GetString("accountmanager.dirk.ca-cert") != "" { + caPEMBlock, err = majordomo.Fetch(ctx, viper.GetString("accountmanager.dirk.ca-cert")) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain client CA certificate") } - }() + } + accountManager, err = dirkaccountmanager.New(ctx, + dirkaccountmanager.WithLogLevel(logLevel(viper.GetString("accountmanager.dirk.log-level"))), + dirkaccountmanager.WithMonitor(monitor.(metrics.AccountManagerMonitor)), + dirkaccountmanager.WithValidatorsProvider(eth2Client.(eth2client.ValidatorsProvider)), + dirkaccountmanager.WithEndpoints(viper.GetStringSlice("accountmanager.dirk.endpoints")), + dirkaccountmanager.WithAccountPaths(viper.GetStringSlice("accountmanager.dirk.accounts")), + dirkaccountmanager.WithSlotsPerEpochProvider(eth2Client.(eth2client.SlotsPerEpochProvider)), + dirkaccountmanager.WithBeaconProposerDomainProvider(eth2Client.(eth2client.BeaconProposerDomainProvider)), + dirkaccountmanager.WithBeaconAttesterDomainProvider(eth2Client.(eth2client.BeaconAttesterDomainProvider)), + dirkaccountmanager.WithRANDAODomainProvider(eth2Client.(eth2client.RANDAODomainProvider)), + dirkaccountmanager.WithSelectionProofDomainProvider(eth2Client.(eth2client.SelectionProofDomainProvider)), + dirkaccountmanager.WithAggregateAndProofDomainProvider(eth2Client.(eth2client.AggregateAndProofDomainProvider)), + dirkaccountmanager.WithSignatureDomainProvider(eth2Client.(eth2client.SignatureDomainProvider)), + dirkaccountmanager.WithClientCert(certPEMBlock), + dirkaccountmanager.WithClientKey(keyPEMBlock), + dirkaccountmanager.WithCACert(caPEMBlock), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to start dirk account manager service") + } } + + if viper.Get("accountmanager.wallet") != nil { + var err error + passphrases := make([][]byte, 0) + for _, passphraseURL := range viper.GetStringSlice("accountmanager.wallet.passphrases") { + passphrase, err := majordomo.Fetch(ctx, passphraseURL) + if err != nil { + log.Error().Err(err).Str("url", string(passphrase)).Msg("failed to obtain passphrase") + continue + } + passphrases = append(passphrases, passphrase) + } + if len(passphrases) == 0 { + return nil, errors.New("no passphrases for wallet supplied") + } + accountManager, err = walletaccountmanager.New(ctx, + walletaccountmanager.WithLogLevel(logLevel(viper.GetString("accountmanager.wallet.log-level"))), + walletaccountmanager.WithMonitor(monitor.(metrics.AccountManagerMonitor)), + walletaccountmanager.WithValidatorsProvider(eth2Client.(eth2client.ValidatorsProvider)), + walletaccountmanager.WithAccountPaths(viper.GetStringSlice("accountmanager.wallet.accounts")), + walletaccountmanager.WithPassphrases(passphrases), + walletaccountmanager.WithLocations(viper.GetStringSlice("accountmanager.wallet.locations")), + walletaccountmanager.WithSlotsPerEpochProvider(eth2Client.(eth2client.SlotsPerEpochProvider)), + walletaccountmanager.WithBeaconProposerDomainProvider(eth2Client.(eth2client.BeaconProposerDomainProvider)), + walletaccountmanager.WithBeaconAttesterDomainProvider(eth2Client.(eth2client.BeaconAttesterDomainProvider)), + walletaccountmanager.WithRANDAODomainProvider(eth2Client.(eth2client.RANDAODomainProvider)), + walletaccountmanager.WithSelectionProofDomainProvider(eth2Client.(eth2client.SelectionProofDomainProvider)), + walletaccountmanager.WithAggregateAndProofDomainProvider(eth2Client.(eth2client.AggregateAndProofDomainProvider)), + walletaccountmanager.WithSignatureDomainProvider(eth2Client.(eth2client.SignatureDomainProvider)), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to start wallet account manager service") + } + } + + return accountManager, nil +} + +func selectBeaconBlockProposalProvider(ctx context.Context, + monitor metrics.Service, + eth2Client eth2client.Service, +) (eth2client.BeaconBlockProposalProvider, error) { + var beaconBlockProposalProvider eth2client.BeaconBlockProposalProvider + var err error + switch viper.GetString("strategies.beaconblockproposal.style") { + case "best": + log.Info().Msg("Starting best beacon block proposal strategy") + beaconBlockProposalProviders := make(map[string]eth2client.BeaconBlockProposalProvider) + for _, address := range viper.GetStringSlice("strategies.beaconblockproposal.beacon-node-addresses") { + client, err := fetchClient(ctx, address) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to fetch client %q for beacon block proposal strategy", address)) + } + beaconBlockProposalProviders[address] = client.(eth2client.BeaconBlockProposalProvider) + } + beaconBlockProposalProvider, err = bestbeaconblockproposalstrategy.New(ctx, + bestbeaconblockproposalstrategy.WithClientMonitor(monitor.(metrics.ClientMonitor)), + bestbeaconblockproposalstrategy.WithProcessConcurrency(viper.GetInt64("process-concurrency")), + bestbeaconblockproposalstrategy.WithLogLevel(logLevel(viper.GetString("strategies.beaconblockproposal.log-level"))), + bestbeaconblockproposalstrategy.WithBeaconBlockProposalProviders(beaconBlockProposalProviders), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to start best beacon block proposal strategy") + } + case "first": + log.Info().Msg("Starting first beacon block proposal strategy") + beaconBlockProposalProviders := make(map[string]eth2client.BeaconBlockProposalProvider) + for _, address := range viper.GetStringSlice("strategies.beaconblockproposal.beacon-node-addresses") { + client, err := fetchClient(ctx, address) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to fetch client %q for beacon block proposal strategy", address)) + } + beaconBlockProposalProviders[address] = client.(eth2client.BeaconBlockProposalProvider) + } + beaconBlockProposalProvider, err = firstbeaconblockproposalstrategy.New(ctx, + firstbeaconblockproposalstrategy.WithClientMonitor(monitor.(metrics.ClientMonitor)), + firstbeaconblockproposalstrategy.WithLogLevel(logLevel(viper.GetString("strategies.beaconblockproposal.log-level"))), + firstbeaconblockproposalstrategy.WithBeaconBlockProposalProviders(beaconBlockProposalProviders), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to start first beacon block proposal strategy") + } + default: + log.Info().Msg("Starting simple beacon block proposal strategy") + beaconBlockProposalProvider = eth2Client.(eth2client.BeaconBlockProposalProvider) + } + + return beaconBlockProposalProvider, nil +} + +func selectSubmitterStrategy(ctx context.Context, eth2Client eth2client.Service) (submitter.Service, error) { + var submitter submitter.Service + var err error + switch viper.GetString("strategies.submitter.style") { + case "all": + log.Info().Msg("Starting multinode submitter strategy") + beaconBlockSubmitters := make(map[string]eth2client.BeaconBlockSubmitter) + attestationSubmitters := make(map[string]eth2client.AttestationSubmitter) + aggregateAttestationSubmitters := make(map[string]eth2client.AggregateAttestationsSubmitter) + beaconCommitteeSubscriptionsSubmitters := make(map[string]eth2client.BeaconCommitteeSubscriptionsSubmitter) + for _, address := range viper.GetStringSlice("strategies.submitter.all.beacon-node-addresses") { + client, err := fetchClient(ctx, address) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to fetch client %q for submitter strategy", address)) + } + beaconBlockSubmitters[address] = client.(eth2client.BeaconBlockSubmitter) + attestationSubmitters[address] = client.(eth2client.AttestationSubmitter) + aggregateAttestationSubmitters[address] = client.(eth2client.AggregateAttestationsSubmitter) + } + submitter, err = multinodesubmitter.New(ctx, + multinodesubmitter.WithProcessConcurrency(viper.GetInt64("process-concurrency")), + multinodesubmitter.WithLogLevel(logLevel(viper.GetString("strategies.submitter.log-level"))), + multinodesubmitter.WithBeaconBlockSubmitters(beaconBlockSubmitters), + multinodesubmitter.WithAttestationSubmitters(attestationSubmitters), + multinodesubmitter.WithAggregateAttestationsSubmitters(aggregateAttestationSubmitters), + multinodesubmitter.WithBeaconCommitteeSubscriptionsSubmitters(beaconCommitteeSubscriptionsSubmitters), + ) + default: + log.Info().Msg("Starting standard submitter strategy") + submitter, err = immediatesubmitter.New(ctx, + immediatesubmitter.WithLogLevel(logLevel(viper.GetString("strategies.submitter.log-level"))), + immediatesubmitter.WithBeaconBlockSubmitter(eth2Client.(eth2client.BeaconBlockSubmitter)), + immediatesubmitter.WithAttestationSubmitter(eth2Client.(eth2client.AttestationSubmitter)), + immediatesubmitter.WithBeaconCommitteeSubscriptionsSubmitter(eth2Client.(eth2client.BeaconCommitteeSubscriptionsSubmitter)), + immediatesubmitter.WithAggregateAttestationsSubmitter(eth2Client.(eth2client.AggregateAttestationsSubmitter)), + ) + } + if err != nil { + return nil, errors.Wrap(err, "failed to start submitter service") + } + return submitter, nil } diff --git a/media/architecture.svg b/media/architecture.svg new file mode 100644 index 0000000..5deaf0a --- /dev/null +++ b/media/architecture.svg @@ -0,0 +1,1022 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Scheduler + Submitter + Accountmanager + Controller + Beacon chain communications + Beacon chain communications + Beacon blockproposer + Beacon blockattester + Beacon attestationaggregator + Beacon chaincommunications + + diff --git a/mock/accountmanager.go b/mock/accountmanager.go new file mode 100644 index 0000000..5b8421a --- /dev/null +++ b/mock/accountmanager.go @@ -0,0 +1,216 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "strings" + + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/herumi/bls-eth-go-binary/bls" + "github.com/pkg/errors" +) + +type validatingAccount struct { + index uint64 + key *bls.SecretKey +} + +func (a *validatingAccount) PubKey(ctx context.Context) ([]byte, error) { + return a.key.GetPublicKey().Serialize(), nil +} + +func (a *validatingAccount) Index(ctx context.Context) (uint64, error) { + return a.index, nil +} + +func (a *validatingAccount) State() api.ValidatorState { + return api.ValidatorStateActiveOngoing +} + +func (a *validatingAccount) SignSlotSelection(ctx context.Context, slot uint64, signatureDomain []byte) ([]byte, error) { + slotBytes := make([]byte, 32) + binary.LittleEndian.PutUint64(slotBytes, slot) + + hash := sha256.New() + n, err := hash.Write(slotBytes) + if err != nil { + return nil, errors.Wrap(err, "failed to write slot") + } + if n != 32 { + return nil, errors.Wrap(err, "failed to write all slot bytes") + } + n, err = hash.Write(signatureDomain) + if err != nil { + return nil, errors.Wrap(err, "failed to write signature domain") + } + if n != 32 { + return nil, errors.Wrap(err, "failed to write all signature domain bytes") + } + + root := hash.Sum(nil) + + sig := a.key.SignByte(root) + return sig.Serialize(), nil +} + +// ValidatingAccountsProvider is a mock for accountmanager.ValidatingAccountsProvider. +type ValidatingAccountsProvider struct { + validatingAccounts []accountmanager.ValidatingAccount +} + +func _secretKey(input string) *bls.SecretKey { + bytes, err := hex.DecodeString(strings.TrimPrefix(input, "0x")) + if err != nil { + panic(err) + } + var key bls.SecretKey + if err := key.Deserialize(bytes); err != nil { + panic(err) + } + return &key +} + +// NewValidatingAccountsProvider returns a mock account manager with pre-configured keys. +func NewValidatingAccountsProvider() accountmanager.ValidatingAccountsProvider { + validatingAccounts := []accountmanager.ValidatingAccount{ + &validatingAccount{ + index: 5184, + key: _secretKey("0x01e748d098d3bcb477d636f19d510399ae18205fadf9814ee67052f88c1f77c0"), + }, + &validatingAccount{ + index: 5221, + key: _secretKey("0x376880b8079dca3bbd06c93958b5208929cbc169c9ce4caf8731be10e94f710e"), + }, + &validatingAccount{ + index: 14499, + key: _secretKey("0x3fb0a5e8ec5f9f421b682d8956e08e02af5ed921e7f82a78cc6258869c283500"), + }, + &validatingAccount{ + index: 14096, + key: _secretKey("0x1432f616d724ebe44ba92c603496627dc9a4899ffa7956948caa4a9cebaac171"), + }, + &validatingAccount{ + index: 14407, + key: _secretKey("0x5dc083116a71299cbd8582ab0da28c32f64359039d1f5f5f4a664d2c7deb258e"), + }, + &validatingAccount{ + index: 13885, + key: _secretKey("0x1d6b5e0a9c9c05b7a318602ac1f204c83b6fd2ff8e7b7a3de0aa5e9ff42df071"), + }, + &validatingAccount{ + index: 13743, + key: _secretKey("0x6f91850e101d59b80cc77faa3658c730653827413e653dbdaf9ecfd727cb72e7"), + }, + &validatingAccount{ + index: 13594, + key: _secretKey("0x63510d1383f4ab8285a5b47f6e36da895c2eabd892d580f9820d1c2cf65bc2a9"), + }, + &validatingAccount{ + index: 13796, + key: _secretKey("0x1f9a5ceb86e03e0e94154b4bb7774d8e0cb0dafbe52c8c82acec775723a1e288"), + }, + &validatingAccount{ + index: 14201, + key: _secretKey("0x11d9711e3d67e6b4ea5bf3485babcd365eef48bb9c69b1c89f689e31d5cf5fe2"), + }, + &validatingAccount{ + index: 13790, + key: _secretKey("0x10c0c8b5ca8fdfba14819373e13f4c980f125a075f4c4edce3b32ad037c93740"), + }, + &validatingAccount{ + index: 13981, + key: _secretKey("0x28939bb5986f4074172417273f4174e4cddf75a1f88595cd9d4b6082cbf476fa"), + }, + &validatingAccount{ + index: 13643, + key: _secretKey("0x3662b248e8cfe57e99e73a9e57e7fe0ee9244880b5c9284e8d878c64aca6b5fc"), + }, + &validatingAccount{ + index: 13536, + key: _secretKey("0x281c019804bf23792963095041d1db1f8b79df49d31b07c9cbed1994ff794974"), + }, + &validatingAccount{ + index: 13673, + key: _secretKey("0x15d98ae5d17b78b159dd7feee9aee7b3a7dbaf4777de92da004eb3b46101c5a1"), + }, + &validatingAccount{ + index: 14032, + key: _secretKey("0x6099d69ff55e3dfeba26a4c7db572b7d34792e090704f0eef9ae149260de909f"), + }, + &validatingAccount{ + index: 14370, + key: _secretKey("0x4e693b831328f20818df32fafd50be61daf7cb7de6b96a8767fc183a8e9bfa76"), + }, + &validatingAccount{ + index: 14368, + key: _secretKey("0x0e0c93d7fe17ef80ced6f431dd482abd02530f29294b2f47318da24d82fb54ef"), + }, + } + + return &ValidatingAccountsProvider{ + validatingAccounts: validatingAccounts, + } +} + +// Accounts returns accounts. +func (m *ValidatingAccountsProvider) Accounts(ctx context.Context) ([]accountmanager.ValidatingAccount, error) { + return m.validatingAccounts, nil +} + +// AccountsByIndex returns accounts. +func (m *ValidatingAccountsProvider) AccountsByIndex(ctx context.Context, indices []uint64) ([]accountmanager.ValidatingAccount, error) { + indexMap := make(map[uint64]bool) + for _, index := range indices { + indexMap[index] = true + } + + res := make([]accountmanager.ValidatingAccount, 0) + for _, validatingAccount := range m.validatingAccounts { + index, err := validatingAccount.Index(ctx) + if err != nil { + continue + } + if _, required := indexMap[index]; required { + res = append(res, validatingAccount) + } + } + return res, nil +} + +// AccountsByPubKey returns accounts. +func (m *ValidatingAccountsProvider) AccountsByPubKey(ctx context.Context, pubKeys [][]byte) ([]accountmanager.ValidatingAccount, error) { + keyMap := make(map[string]bool) + for _, pubKey := range pubKeys { + keyMap[fmt.Sprintf("%x", pubKey)] = true + } + + res := make([]accountmanager.ValidatingAccount, 0) + for _, validatingAccount := range m.validatingAccounts { + publicKey, err := validatingAccount.PubKey(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain public key of account") + } + pubKey := fmt.Sprintf("%x", publicKey) + if _, required := keyMap[pubKey]; required { + res = append(res, validatingAccount) + } + } + return res, nil +} diff --git a/mock/eth2client.go b/mock/eth2client.go new file mode 100644 index 0000000..1647ee4 --- /dev/null +++ b/mock/eth2client.go @@ -0,0 +1,125 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "context" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// GenesisTimeProvider is a mock for eth2client.GenesisTimeProvider. +type GenesisTimeProvider struct { + genesisTime time.Time +} + +// NewGenesisTimeProvider returns a mock genesis time provider with the provided value. +func NewGenesisTimeProvider(genesisTime time.Time) eth2client.GenesisTimeProvider { + return &GenesisTimeProvider{ + genesisTime: genesisTime, + } +} + +// GenesisTime is a mock. +func (m *GenesisTimeProvider) GenesisTime(ctx context.Context) (time.Time, error) { + return m.genesisTime, nil +} + +// SlotDurationProvider is a mock for eth2client.SlotDurationProvider. +type SlotDurationProvider struct { + slotDuration time.Duration +} + +// NewSlotDurationProvider returns a mock slot duration provider with the provided value. +func NewSlotDurationProvider(slotDuration time.Duration) eth2client.SlotDurationProvider { + return &SlotDurationProvider{ + slotDuration: slotDuration, + } +} + +// SlotDuration is a mock. +func (m *SlotDurationProvider) SlotDuration(ctx context.Context) (time.Duration, error) { + return m.slotDuration, nil +} + +// SlotsPerEpochProvider is a mock for eth2client.SlotsPerEpochProvider. +type SlotsPerEpochProvider struct { + slotsPerEpoch uint64 +} + +// NewSlotsPerEpochProvider returns a mock slots per epoch provider with the provided value. +func NewSlotsPerEpochProvider(slotsPerEpoch uint64) eth2client.SlotsPerEpochProvider { + return &SlotsPerEpochProvider{ + slotsPerEpoch: slotsPerEpoch, + } +} + +// SlotsPerEpoch is a mock. +func (m *SlotsPerEpochProvider) SlotsPerEpoch(ctx context.Context) (uint64, error) { + return m.slotsPerEpoch, nil +} + +// AttestationSubmitter is a mock for eth2client.AttestationSubmitter. +type AttestationSubmitter struct{} + +// NewAttestationSubmitter returns a mock attestation submitter. +func NewAttestationSubmitter() eth2client.AttestationSubmitter { + return &AttestationSubmitter{} +} + +// SubmitAttestation is a mock. +func (m *AttestationSubmitter) SubmitAttestation(ctx context.Context, attestation *spec.Attestation) error { + return nil +} + +// BeaconBlockSubmitter is a mock for eth2client.BeaconBlockSubmitter. +type BeaconBlockSubmitter struct{} + +// NewBeaconBlockSubmitter returns a mock beacon block submitter. +func NewBeaconBlockSubmitter() eth2client.BeaconBlockSubmitter { + return &BeaconBlockSubmitter{} +} + +// SubmitBeaconBlock is a mock. +func (m *BeaconBlockSubmitter) SubmitBeaconBlock(ctx context.Context, bloc *spec.SignedBeaconBlock) error { + return nil +} + +// AggregateAttestationsSubmitter is a mock for eth2client.AggregateAttestationsSubmitter. +type AggregateAttestationsSubmitter struct{} + +// NewAggregateAttestationsSubmitter returns a mock aggregate attestation submitter. +func NewAggregateAttestationsSubmitter() eth2client.AggregateAttestationsSubmitter { + return &AggregateAttestationsSubmitter{} +} + +// SubmitAggregateAttestations is a mock. +func (m *AggregateAttestationsSubmitter) SubmitAggregateAttestations(ctx context.Context, aggregateAndProofs []*spec.SignedAggregateAndProof) error { + return nil +} + +// BeaconCommitteeSubscriptionsSubmitter is a mock for eth2client.BeaconCommitteeSubscriptionsSubmitter. +type BeaconCommitteeSubscriptionsSubmitter struct{} + +// NewBeaconCommitteeSubscriptionsSubmitter returns a mock beacon committee subscriptions submitter. +func NewBeaconCommitteeSubscriptionsSubmitter() eth2client.BeaconCommitteeSubscriptionsSubmitter { + return &BeaconCommitteeSubscriptionsSubmitter{} +} + +// SubmitBeaconCommitteeSubscriptions is a mock. +func (m *BeaconCommitteeSubscriptionsSubmitter) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*eth2client.BeaconCommitteeSubscription) error { + return nil +} diff --git a/services/accountmanager/dirk/parameters.go b/services/accountmanager/dirk/parameters.go new file mode 100644 index 0000000..b6172ac --- /dev/null +++ b/services/accountmanager/dirk/parameters.go @@ -0,0 +1,209 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dirk + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/metrics" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.AccountManagerMonitor + endpoints []string + accountPaths []string + clientCert []byte + clientKey []byte + caCert []byte + slotsPerEpochProvider eth2client.SlotsPerEpochProvider + beaconProposerDomainProvider eth2client.BeaconProposerDomainProvider + beaconAttesterDomainProvider eth2client.BeaconAttesterDomainProvider + randaoDomainProvider eth2client.RANDAODomainProvider + selectionProofDomainProvider eth2client.SelectionProofDomainProvider + aggregateAndProofDomainProvider eth2client.AggregateAndProofDomainProvider + signatureDomainProvider eth2client.SignatureDomainProvider + validatorsProvider eth2client.ValidatorsProvider +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithMonitor sets the monitor for the module. +func WithMonitor(monitor metrics.AccountManagerMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithEndpoints sets the endpoints to communicate with dirk. +func WithEndpoints(endpoints []string) Parameter { + return parameterFunc(func(p *parameters) { + p.endpoints = endpoints + }) +} + +// WithAccountPaths sets the accounts paths for which to validate. +func WithAccountPaths(accountPaths []string) Parameter { + return parameterFunc(func(p *parameters) { + p.accountPaths = accountPaths + }) +} + +// WithClientCert sets the bytes of the client TLS certificate. +func WithClientCert(cert []byte) Parameter { + return parameterFunc(func(p *parameters) { + p.clientCert = cert + }) +} + +// WithClientKey sets the bytes of the client TLS key. +func WithClientKey(key []byte) Parameter { + return parameterFunc(func(p *parameters) { + p.clientKey = key + }) +} + +// WithCACert sets the bytes of the certificate authority TLS certificate. +func WithCACert(cert []byte) Parameter { + return parameterFunc(func(p *parameters) { + p.caCert = cert + }) +} + +// WithValidatorsProvider sets the validator status provider. +func WithValidatorsProvider(provider eth2client.ValidatorsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatorsProvider = provider + }) +} + +// WithSlotsPerEpochProvider sets the slots per epoch provider. +func WithSlotsPerEpochProvider(provider eth2client.SlotsPerEpochProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.slotsPerEpochProvider = provider + }) +} + +// WithBeaconProposerDomainProvider sets the beacon proposer domain provider. +func WithBeaconProposerDomainProvider(provider eth2client.BeaconProposerDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconProposerDomainProvider = provider + }) +} + +// WithBeaconAttesterDomainProvider sets the beacon attester domain provider. +func WithBeaconAttesterDomainProvider(provider eth2client.BeaconAttesterDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconAttesterDomainProvider = provider + }) +} + +// WithRANDAODomainProvider sets the RANDAO domain provider. +func WithRANDAODomainProvider(provider eth2client.RANDAODomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.randaoDomainProvider = provider + }) +} + +// WithSelectionProofDomainProvider sets the RANDAO domain provider. +func WithSelectionProofDomainProvider(provider eth2client.SelectionProofDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.selectionProofDomainProvider = provider + }) +} + +// WithAggregateAndProofDomainProvider sets the aggregate and proof domain provider. +func WithAggregateAndProofDomainProvider(provider eth2client.AggregateAndProofDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.aggregateAndProofDomainProvider = provider + }) +} + +// WithSignatureDomainProvider sets the signature domain provider. +func WithSignatureDomainProvider(provider eth2client.SignatureDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.signatureDomainProvider = provider + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.endpoints == nil { + return nil, errors.New("no endpoints specified") + } + if parameters.accountPaths == nil { + return nil, errors.New("no account paths specified") + } + if parameters.clientCert == nil { + return nil, errors.New("no client certificate specified") + } + if parameters.clientKey == nil { + return nil, errors.New("no client key specified") + } + if parameters.validatorsProvider == nil { + return nil, errors.New("no validators provider specified") + } + if parameters.slotsPerEpochProvider == nil { + return nil, errors.New("no slots per epoch provider specified") + } + if parameters.beaconProposerDomainProvider == nil { + return nil, errors.New("no beacon proposer domain provider specified") + } + if parameters.beaconAttesterDomainProvider == nil { + return nil, errors.New("no beacon attester domain provider specified") + } + if parameters.randaoDomainProvider == nil { + return nil, errors.New("no RANDAO domain provider specified") + } + if parameters.selectionProofDomainProvider == nil { + return nil, errors.New("no selection proof domain provider specified") + } + if parameters.aggregateAndProofDomainProvider == nil { + return nil, errors.New("no aggregate and proof domain provider specified") + } + if parameters.signatureDomainProvider == nil { + return nil, errors.New("no signature domain provider specified") + } + + return ¶meters, nil +} diff --git a/services/accountmanager/dirk/service.go b/services/accountmanager/dirk/service.go new file mode 100644 index 0000000..f1b171f --- /dev/null +++ b/services/accountmanager/dirk/service.go @@ -0,0 +1,411 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dirk + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "regexp" + "strconv" + "strings" + "sync" + + eth2client "github.com/attestantio/go-eth2-client" + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/metrics" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "github.com/wealdtech/go-bytesutil" + dirk "github.com/wealdtech/go-eth2-wallet-dirk" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" + "google.golang.org/grpc/credentials" +) + +// Service is the manager for dirk accounts. +type Service struct { + mutex sync.RWMutex + monitor metrics.AccountManagerMonitor + endpoints []*dirk.Endpoint + accountPaths []string + credentials credentials.TransportCredentials + accounts map[[48]byte]*ValidatingAccount + validatorsProvider eth2client.ValidatorsProvider + slotsPerEpoch uint64 + beaconProposerDomain []byte + beaconAttesterDomain []byte + randaoDomain []byte + selectionProofDomain []byte + aggregateAndProofDomain []byte + signatureDomainProvider eth2client.SignatureDomainProvider +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new dirk account manager. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "accountmanager").Str("impl", "dirk").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + credentials, err := credentialsFromCerts(ctx, parameters.clientCert, parameters.clientKey, parameters.caCert) + if err != nil { + return nil, errors.Wrap(err, "failed to build credentials") + } + + endpoints := make([]*dirk.Endpoint, 0, len(parameters.endpoints)) + for _, endpoint := range parameters.endpoints { + endpointParts := strings.Split(endpoint, ":") + if len(endpointParts) != 2 { + log.Warn().Str("endpoint", endpoint).Msg("Malformed endpoint") + continue + } + port, err := strconv.ParseUint(endpointParts[1], 10, 32) + if err != nil { + log.Warn().Str("endpoint", endpoint).Err(err).Msg("Malformed port") + continue + } + if port == 0 { + log.Warn().Str("endpoint", endpoint).Msg("Invalid port") + continue + } + endpoints = append(endpoints, dirk.NewEndpoint(endpointParts[0], uint32(port))) + } + + slotsPerEpoch, err := parameters.slotsPerEpochProvider.SlotsPerEpoch(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain slots per epoch") + } + beaconAttesterDomain, err := parameters.beaconAttesterDomainProvider.BeaconAttesterDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain beacon attester domain") + } + beaconProposerDomain, err := parameters.beaconProposerDomainProvider.BeaconProposerDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain beacon proposer domain") + } + randaoDomain, err := parameters.randaoDomainProvider.RANDAODomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain RANDAO domain") + } + selectionProofDomain, err := parameters.selectionProofDomainProvider.SelectionProofDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain selection proof domain") + } + aggregateAndProofDomain, err := parameters.aggregateAndProofDomainProvider.AggregateAndProofDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain aggregate and proof domain") + } + + s := &Service{ + monitor: parameters.monitor, + endpoints: endpoints, + accountPaths: parameters.accountPaths, + credentials: credentials, + slotsPerEpoch: slotsPerEpoch, + beaconAttesterDomain: beaconAttesterDomain, + beaconProposerDomain: beaconProposerDomain, + randaoDomain: randaoDomain, + selectionProofDomain: selectionProofDomain, + aggregateAndProofDomain: aggregateAndProofDomain, + signatureDomainProvider: parameters.signatureDomainProvider, + validatorsProvider: parameters.validatorsProvider, + } + + if err := s.RefreshAccounts(ctx); err != nil { + return nil, errors.Wrap(err, "failed to fetch validating keys") + } + + return s, nil +} + +// UpdateAccountsState updates account state with the latest information from the beacon chain. +// This should be run at the beginning of each epoch to ensure that any newly-activated accounts are registered. +func (s *Service) UpdateAccountsState(ctx context.Context) error { + validatorIDs := make([]eth2client.ValidatorIDProvider, 0, len(s.accounts)) + for _, account := range s.accounts { + if !account.state.IsAttesting() { + validatorIDs = append(validatorIDs, account) + } + } + if len(validatorIDs) == 0 { + // Nothing to do. + log.Trace().Msg("No unactivated keys") + return nil + } + validators, err := s.validatorsProvider.Validators(ctx, "head", validatorIDs) + if err != nil { + return errors.Wrap(err, "failed to obtain validators") + } + + s.mutex.Lock() + s.updateAccountStates(ctx, s.accounts, validators) + s.mutex.Unlock() + + return nil +} + +// RefreshAccounts refreshes the entire list of validating keys. +func (s *Service) RefreshAccounts(ctx context.Context) error { + // Create the relevant wallets. + wallets := make(map[string]e2wtypes.Wallet) + pathsByWallet := make(map[string][]string) + for _, path := range s.accountPaths { + pathBits := strings.Split(path, "/") + + var paths []string + var exists bool + if paths, exists = pathsByWallet[pathBits[0]]; !exists { + paths = make([]string, 0) + } + pathsByWallet[pathBits[0]] = append(paths, path) + wallet, err := dirk.OpenWallet(ctx, pathBits[0], s.credentials, s.endpoints) + if err != nil { + log.Warn().Err(err).Str("wallet", pathBits[0]).Msg("Failed to open wallet") + } else { + wallets[wallet.Name()] = wallet + } + } + + verificationRegexes := accountPathsToVerificationRegexes(s.accountPaths) + // Fetch accounts for each wallet. + accounts := make(map[[48]byte]*ValidatingAccount) + for _, wallet := range wallets { + // if _, isProvider := wallet.(e2wtypes.WalletAccountsByPathProvider); isProvider { + // fmt.Printf("TODO: fetch accounts by path") + // } else { + s.fetchAccountsForWallet(ctx, wallet, accounts, verificationRegexes) + //} + } + + validatorIDs := make([]eth2client.ValidatorIDProvider, 0, len(accounts)) + for _, account := range accounts { + if !account.state.IsAttesting() { + validatorIDs = append(validatorIDs, account) + } + } + log.Trace().Int("keys", len(accounts)).Msg("Keys obtained") + if len(validatorIDs) == 0 { + log.Warn().Msg("No accounts obtained") + return nil + } + + validators, err := s.validatorsProvider.Validators(ctx, "head", validatorIDs) + if err != nil { + return errors.Wrap(err, "failed to obtain validators") + } + s.updateAccountStates(ctx, accounts, validators) + + s.mutex.Lock() + s.accounts = accounts + s.mutex.Unlock() + + return nil +} + +func credentialsFromCerts(ctx context.Context, clientCert []byte, clientKey []byte, caCert []byte) (credentials.TransportCredentials, error) { + clientPair, err := tls.X509KeyPair(clientCert, clientKey) + if err != nil { + return nil, errors.Wrap(err, "failed to load client keypair") + } + + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{clientPair}, + MinVersion: tls.VersionTLS13, + } + + if caCert != nil { + cp := x509.NewCertPool() + if !cp.AppendCertsFromPEM(caCert) { + return nil, errors.Wrap(err, "failed to add CA certificate") + } + tlsCfg.RootCAs = cp + } + + return credentials.NewTLS(tlsCfg), nil +} + +// Accounts returns all attesting accounts. +func (s *Service) Accounts(ctx context.Context) ([]accountmanager.ValidatingAccount, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + accounts := make([]accountmanager.ValidatingAccount, 0, len(s.accounts)) + for _, account := range s.accounts { + if account.state.IsAttesting() { + accounts = append(accounts, account) + } + } + + return accounts, nil +} + +// AccountsByIndex returns attesting accounts. +func (s *Service) AccountsByIndex(ctx context.Context, indices []uint64) ([]accountmanager.ValidatingAccount, error) { + indexMap := make(map[uint64]bool) + for _, index := range indices { + indexMap[index] = true + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + accounts := make([]accountmanager.ValidatingAccount, 0, len(s.accounts)) + for _, account := range s.accounts { + if !account.state.IsAttesting() { + continue + } + index, err := account.Index(ctx) + if err != nil { + log.Error().Err(err).Msg("No index for account") + continue + } + if _, exists := indexMap[index]; exists { + accounts = append(accounts, account) + + } + } + + return accounts, nil +} + +// AccountsByPubKey returns validating accounts. +func (s *Service) AccountsByPubKey(ctx context.Context, pubKeys [][]byte) ([]accountmanager.ValidatingAccount, error) { + pubKeyMap := make(map[[48]byte]bool) + for _, pubKey := range pubKeys { + var mapKey [48]byte + copy(mapKey[:], pubKey) + pubKeyMap[mapKey] = true + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + accounts := make([]accountmanager.ValidatingAccount, 0, len(s.accounts)) + for pubKey, account := range s.accounts { + if !account.state.IsAttesting() { + continue + } + if _, exists := pubKeyMap[pubKey]; exists { + accounts = append(accounts, account) + } + } + + return accounts, nil +} + +// accountPathsToVerificationRegexes turns account paths in to regexes to allow verification. +func accountPathsToVerificationRegexes(paths []string) []*regexp.Regexp { + regexes := make([]*regexp.Regexp, 0, len(paths)) + for _, path := range paths { + log := log.With().Str("path", path).Logger() + parts := strings.Split(path, "/") + if len(parts) == 0 || len(parts[0]) == 0 { + log.Debug().Msg("Invalid path") + continue + } + if len(parts) == 1 { + parts = append(parts, ".*") + } + parts[1] = strings.TrimPrefix(parts[1], "^") + var specifier string + if strings.HasSuffix(parts[1], "$") { + specifier = fmt.Sprintf("^%s/%s", parts[0], parts[1]) + } else { + specifier = fmt.Sprintf("^%s/%s$", parts[0], parts[1]) + } + regex, err := regexp.Compile(specifier) + if err != nil { + log.Warn().Str("specifier", specifier).Err(err).Msg("Invalid path regex") + continue + } + regexes = append(regexes, regex) + } + return regexes +} + +func (s *Service) updateAccountStates(ctx context.Context, accounts map[[48]byte]*ValidatingAccount, validators map[uint64]*api.Validator) { + validatorsByPubKey := make(map[[48]byte]*api.Validator, len(validators)) + for _, validator := range validators { + var pubKey [48]byte + copy(pubKey[:], validator.Validator.PublicKey) + validatorsByPubKey[pubKey] = validator + } + + validatorStateCounts := make(map[string]uint64) + for pubKey, account := range accounts { + validator, exists := validatorsByPubKey[pubKey] + if exists { + account.index = validator.Index + account.state = validator.State + } + validatorStateCounts[strings.ToLower(account.state.String())]++ + } + for state, count := range validatorStateCounts { + s.monitor.Accounts(state, count) + } + + if e := log.Trace(); e.Enabled() { + for _, account := range accounts { + log.Trace(). + Str("name", account.account.Name()). + Str("public_key", fmt.Sprintf("%x", account.account.PublicKey().Marshal())). + Str("state", account.state.String()). + Msg("Validating account") + } + } +} + +func (s *Service) fetchAccountsForWallet(ctx context.Context, wallet e2wtypes.Wallet, accounts map[[48]byte]*ValidatingAccount, verificationRegexes []*regexp.Regexp) { + for account := range wallet.Accounts(ctx) { + // Ensure the name matches one of our account paths. + name := fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) + verified := false + for _, verificationRegex := range verificationRegexes { + if verificationRegex.Match([]byte(name)) { + verified = true + break + } + } + if !verified { + log.Debug().Str("account", name).Msg("Received unwanted account from server; ignoring") + continue + } + + var pubKey []byte + if provider, isProvider := account.(e2wtypes.AccountCompositePublicKeyProvider); isProvider { + pubKey = provider.CompositePublicKey().Marshal() + } else { + pubKey = account.PublicKey().Marshal() + } + + // Set up account as unknown to beacon chain. + accounts[bytesutil.ToBytes48(pubKey)] = &ValidatingAccount{ + account: account, + accountManager: s, + signatureDomainProvider: s.signatureDomainProvider, + } + } +} diff --git a/services/accountmanager/dirk/validatingaccount.go b/services/accountmanager/dirk/validatingaccount.go new file mode 100644 index 0000000..1eecdf0 --- /dev/null +++ b/services/accountmanager/dirk/validatingaccount.go @@ -0,0 +1,185 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dirk + +import ( + "context" + "encoding/binary" + + eth2client "github.com/attestantio/go-eth2-client" + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/opentracing/opentracing-go" + "github.com/pkg/errors" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// ValidatingAccount is a wrapper around the dirk account that implements ValidatingAccount. +type ValidatingAccount struct { + account e2wtypes.Account + index uint64 + state api.ValidatorState + accountManager *Service + signatureDomainProvider eth2client.SignatureDomainProvider +} + +// PubKey returns the public key of the validating account. +func (d *ValidatingAccount) PubKey(ctx context.Context) ([]byte, error) { + if provider, isProvider := d.account.(e2wtypes.AccountCompositePublicKeyProvider); isProvider { + return provider.CompositePublicKey().Marshal(), nil + } + return d.account.PublicKey().Marshal(), nil +} + +// Index returns the index of the validating account. +func (d *ValidatingAccount) Index(ctx context.Context) (uint64, error) { + return d.index, nil +} + +// State returns the state of the validating account. +func (d *ValidatingAccount) State() api.ValidatorState { + return d.state +} + +// SignSlotSelection returns a slot selection signature. +// This signs a slot with the "selection proof" domain. +func (d *ValidatingAccount) SignSlotSelection(ctx context.Context, slot uint64) ([]byte, error) { + span, ctx := opentracing.StartSpanFromContext(ctx, "dirk.SignSlotSelection") + defer span.Finish() + + // Calculate the signature domain. + signatureDomain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.selectionProofDomain, + slot/d.accountManager.slotsPerEpoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for selection proof") + } + + slotBytes := make([]byte, 32) + binary.LittleEndian.PutUint64(slotBytes, slot) + + sig, err := d.account.(e2wtypes.AccountProtectingSigner).SignGeneric(ctx, slotBytes, signatureDomain) + if err != nil { + return nil, errors.Wrap(err, "failed to sign slot") + } + return sig.Marshal(), nil +} + +// SignRANDAOReveal returns a RANDAO reveal signature. +// This signs an epoch with the "RANDAO reveal" domain. +// N.B. This passes in a slot, not an epoch. +func (d *ValidatingAccount) SignRANDAOReveal(ctx context.Context, slot uint64) ([]byte, error) { + span, ctx := opentracing.StartSpanFromContext(ctx, "dirk.SignRANDAOReveal") + defer span.Finish() + + epoch := slot / d.accountManager.slotsPerEpoch + // Obtain the RANDAO reveal signature domain. + signatureDomain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.randaoDomain, + epoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for RANDAO reveal") + } + + epochBytes := make([]byte, 32) + binary.LittleEndian.PutUint64(epochBytes, epoch) + + sig, err := d.account.(e2wtypes.AccountProtectingSigner).SignGeneric(ctx, epochBytes, signatureDomain) + if err != nil { + return nil, errors.Wrap(err, "failed to sign RANDO reveal") + } + return sig.Marshal(), nil +} + +// SignBeaconBlockProposal signs a beacon block proposal item. +func (d *ValidatingAccount) SignBeaconBlockProposal(ctx context.Context, + slot uint64, + proposerIndex uint64, + parentRoot []byte, + stateRoot []byte, + bodyRoot []byte) ([]byte, error) { + + // Fetch the signature domain. + signatureDomain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.beaconProposerDomain, + slot/d.accountManager.slotsPerEpoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for beacon proposal") + } + + sig, err := d.account.(e2wtypes.AccountProtectingSigner).SignBeaconProposal(ctx, + slot, + proposerIndex, + parentRoot, + stateRoot, + bodyRoot, + signatureDomain) + if err != nil { + return nil, errors.Wrap(err, "failed to sign beacon block proposal") + } + return sig.Marshal(), nil +} + +// SignBeaconAttestation signs a beacon attestation item. +func (d *ValidatingAccount) SignBeaconAttestation(ctx context.Context, + slot uint64, + committeeIndex uint64, + blockRoot []byte, + sourceEpoch uint64, + sourceRoot []byte, + targetEpoch uint64, + targetRoot []byte) ([]byte, error) { + span, ctx := opentracing.StartSpanFromContext(ctx, "dirk.SignBeaconAttestation") + defer span.Finish() + + signatureDomain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.beaconAttesterDomain, + slot/d.accountManager.slotsPerEpoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for beacon attestation") + } + + sig, err := d.account.(e2wtypes.AccountProtectingSigner).SignBeaconAttestation(ctx, + slot, + committeeIndex, + blockRoot, + sourceEpoch, + sourceRoot, + targetEpoch, + targetRoot, + signatureDomain) + if err != nil { + return nil, errors.Wrap(err, "failed to sign beacon attestation") + } + return sig.Marshal(), nil +} + +// SignAggregateAndProof signs an aggregate and proof item. +func (d *ValidatingAccount) SignAggregateAndProof(ctx context.Context, slot uint64, aggregateAndProofRoot []byte) ([]byte, error) { + span, ctx := opentracing.StartSpanFromContext(ctx, "dirk.SignAggregateAndProof") + defer span.Finish() + + // Fetch the signature domain. + signatureDomain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.aggregateAndProofDomain, + slot) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for beacon aggregate and proof") + } + + sig, err := d.account.(e2wtypes.AccountProtectingSigner).SignGeneric(ctx, aggregateAndProofRoot, signatureDomain) + if err != nil { + return nil, errors.Wrap(err, "failed to aggregate and proof") + } + return sig.Marshal(), nil +} diff --git a/services/accountmanager/service.go b/services/accountmanager/service.go new file mode 100644 index 0000000..d954942 --- /dev/null +++ b/services/accountmanager/service.go @@ -0,0 +1,132 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accountmanager is a package that manages validator accounts from multiple sources. +package accountmanager + +import ( + "context" + + api "github.com/attestantio/go-eth2-client/api/v1" +) + +// Service is the generic accountmanager service. +type Service interface{} + +// ValidatingAccountsProvider provides methods for valdiating accounts. +type ValidatingAccountsProvider interface { + // Accounts provides information about all accounts that are configured to validate through this instance. + Accounts(ctx context.Context) ([]ValidatingAccount, error) + + // AccountsByIndex provides information about the specific accounts that are configured to validate through this instance. + AccountsByIndex(ctx context.Context, indices []uint64) ([]ValidatingAccount, error) + + // AccountsByPubKey provides information about the specific accounts that are configured to validate through this instance. + AccountsByPubKey(ctx context.Context, pubKeys [][]byte) ([]ValidatingAccount, error) +} + +// ValidatingAccountPubKeyProvider provides methods for obtaining public keys from accounts. +type ValidatingAccountPubKeyProvider interface { + // PubKey() provides the public key for this account. + PubKey(ctx context.Context) ([]byte, error) +} + +// ValidatingAccountIndexProvider provides methods for obtaining indices from accounts. +type ValidatingAccountIndexProvider interface { + // Index() provides the validator index for this account. + // Returns an error if there is no index for this validator. + Index(ctx context.Context) (uint64, error) +} + +// ValidatingAccountStateProvider provides methods for obtaining state from accounts. +type ValidatingAccountStateProvider interface { + // State() provides the validator state for this account. + State() api.ValidatorState +} + +// ValidatingAccount is a composite interface for common validating account features. +type ValidatingAccount interface { + ValidatingAccountPubKeyProvider + ValidatingAccountIndexProvider + ValidatingAccountStateProvider +} + +// AccountsFetcher fetches accounts from the remote source. +type AccountsFetcher interface { + // RefreshAccounts refreshes the list of relevant accounts known by the account manager. + RefreshAccounts(ctx context.Context) error +} + +// AccountsUpdater manages updates to accounts in line with internal and external changes. +type AccountsUpdater interface { + // UpdateAccountsState updates account state with the latest information from the beacon chain. + UpdateAccountsState(ctx context.Context) error +} + +// IsAggregatorProvider provides methods for obtaining aggregation status from accounts. +type IsAggregatorProvider interface { +} + +// RANDAORevealSigner provides methods to sign RANDAO reveals. +type RANDAORevealSigner interface { + // SignRANDAOReveal returns a RANDAO signature. + // This signs an epoch with the "RANDAO" domain. + // N.B. This passes in a slot, not an epoch. + SignRANDAOReveal(ctx context.Context, slot uint64) ([]byte, error) +} + +// SlotSelectionSigner provides methods to sign slot selections. +type SlotSelectionSigner interface { + // SignSlotSelection returns a slot selection signature. + // This signs a slot with the "selection proof" domain. + SignSlotSelection(ctx context.Context, slot uint64) ([]byte, error) +} + +// BeaconBlockSigner provides methods to sign beacon blocks. +type BeaconBlockSigner interface { + // SignBeaconBlockProposal signs a beacon block proposal. + SignBeaconBlockProposal(ctx context.Context, + slot uint64, + proposerIndex uint64, + parentRoot []byte, + stateRoot []byte, + bodyRoot []byte) ([]byte, error) +} + +// BeaconAttestationSigner provides methods to sign beacon attestations. +type BeaconAttestationSigner interface { + // SignBeaconAttestation signs a beacon attestation. + SignBeaconAttestation(ctx context.Context, + slot uint64, + committeeIndex uint64, + blockRoot []byte, + sourceEpoch uint64, + sourceRoot []byte, + targetEpoch uint64, + targetRoot []byte) ([]byte, error) +} + +// AggregateAndProofSigner provides methods to sign aggregate and proofs. +type AggregateAndProofSigner interface { + // SignAggregateAndProof signs an aggregate attestation for given slot and root. + SignAggregateAndProof(ctx context.Context, slot uint64, root []byte) ([]byte, error) +} + +// Signer is a composite interface for all signer operations. +type Signer interface { + RANDAORevealSigner + SlotSelectionSigner + BeaconBlockSigner + BeaconAttestationSigner + AggregateAndProofSigner +} diff --git a/services/accountmanager/wallet/generate.go b/services/accountmanager/wallet/generate.go new file mode 100644 index 0000000..dfda8e9 --- /dev/null +++ b/services/accountmanager/wallet/generate.go @@ -0,0 +1,17 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wallet + +// Need to `go get github.com/ferranbt/fastssz/sszgen` for this to work. +//go:generate sszgen --path . --objs SigningContainer diff --git a/services/accountmanager/wallet/parameters.go b/services/accountmanager/wallet/parameters.go new file mode 100644 index 0000000..2454fcb --- /dev/null +++ b/services/accountmanager/wallet/parameters.go @@ -0,0 +1,187 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wallet + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/metrics" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.AccountManagerMonitor + locations []string + accountPaths []string + passphrases [][]byte + validatorsProvider eth2client.ValidatorsProvider + slotsPerEpochProvider eth2client.SlotsPerEpochProvider + beaconProposerDomainProvider eth2client.BeaconProposerDomainProvider + beaconAttesterDomainProvider eth2client.BeaconAttesterDomainProvider + randaoDomainProvider eth2client.RANDAODomainProvider + selectionProofDomainProvider eth2client.SelectionProofDomainProvider + aggregateAndProofDomainProvider eth2client.AggregateAndProofDomainProvider + signatureDomainProvider eth2client.SignatureDomainProvider +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithMonitor sets the monitor for the module. +func WithMonitor(monitor metrics.AccountManagerMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithLocations sets the locations to look for wallets. +func WithLocations(locations []string) Parameter { + return parameterFunc(func(p *parameters) { + p.locations = locations + }) +} + +// WithAccountPaths sets the accounts paths for which to validate. +func WithAccountPaths(accountPaths []string) Parameter { + return parameterFunc(func(p *parameters) { + p.accountPaths = accountPaths + }) +} + +// WithPassphrases sets the passphrases to unlock accounts. +func WithPassphrases(passphrases [][]byte) Parameter { + return parameterFunc(func(p *parameters) { + p.passphrases = passphrases + }) +} + +// WithValidatorsProvider sets the validator status provider. +func WithValidatorsProvider(provider eth2client.ValidatorsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatorsProvider = provider + }) +} + +// WithSlotsPerEpochProvider sets the slots per epoch provider. +func WithSlotsPerEpochProvider(provider eth2client.SlotsPerEpochProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.slotsPerEpochProvider = provider + }) +} + +// WithBeaconProposerDomainProvider sets the beacon proposer domain provider. +func WithBeaconProposerDomainProvider(provider eth2client.BeaconProposerDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconProposerDomainProvider = provider + }) +} + +// WithBeaconAttesterDomainProvider sets the beacon attester domain provider. +func WithBeaconAttesterDomainProvider(provider eth2client.BeaconAttesterDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconAttesterDomainProvider = provider + }) +} + +// WithRANDAODomainProvider sets the RANDAO domain provider. +func WithRANDAODomainProvider(provider eth2client.RANDAODomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.randaoDomainProvider = provider + }) +} + +// WithSelectionProofDomainProvider sets the RANDAO domain provider. +func WithSelectionProofDomainProvider(provider eth2client.SelectionProofDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.selectionProofDomainProvider = provider + }) +} + +// WithAggregateAndProofDomainProvider sets the aggregate and proof domain provider. +func WithAggregateAndProofDomainProvider(provider eth2client.AggregateAndProofDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.aggregateAndProofDomainProvider = provider + }) +} + +// WithSignatureDomainProvider sets the signature domain provider. +func WithSignatureDomainProvider(provider eth2client.SignatureDomainProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.signatureDomainProvider = provider + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.accountPaths == nil { + return nil, errors.New("no account paths specified") + } + if len(parameters.passphrases) == 0 { + return nil, errors.New("no passphrases specified") + } + if parameters.validatorsProvider == nil { + return nil, errors.New("no validators provider specified") + } + if parameters.slotsPerEpochProvider == nil { + return nil, errors.New("no slots per epoch provider specified") + } + if parameters.beaconProposerDomainProvider == nil { + return nil, errors.New("no beacon proposer domain provider specified") + } + if parameters.beaconAttesterDomainProvider == nil { + return nil, errors.New("no beacon attester domain provider specified") + } + if parameters.randaoDomainProvider == nil { + return nil, errors.New("no RANDAO domain provider specified") + } + if parameters.selectionProofDomainProvider == nil { + return nil, errors.New("no selection proof domain provider specified") + } + if parameters.aggregateAndProofDomainProvider == nil { + return nil, errors.New("no aggregate and proof domain provider specified") + } + if parameters.signatureDomainProvider == nil { + return nil, errors.New("no signature domain provider specified") + } + + return ¶meters, nil +} diff --git a/services/accountmanager/wallet/service.go b/services/accountmanager/wallet/service.go new file mode 100644 index 0000000..2f1bfa4 --- /dev/null +++ b/services/accountmanager/wallet/service.go @@ -0,0 +1,393 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wallet + +import ( + "context" + "fmt" + "regexp" + "strings" + "sync" + + eth2client "github.com/attestantio/go-eth2-client" + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/metrics" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "github.com/wealdtech/go-bytesutil" + e2wallet "github.com/wealdtech/go-eth2-wallet" + filesystem "github.com/wealdtech/go-eth2-wallet-store-filesystem" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// Service is the manager for wallet accounts. +type Service struct { + mutex sync.RWMutex + monitor metrics.AccountManagerMonitor + stores []e2wtypes.Store + accountPaths []string + passphrases [][]byte + accounts map[[48]byte]*ValidatingAccount + validatorsProvider eth2client.ValidatorsProvider + slotsPerEpoch uint64 + beaconProposerDomain []byte + beaconAttesterDomain []byte + randaoDomain []byte + selectionProofDomain []byte + aggregateAndProofDomain []byte + signatureDomainProvider eth2client.SignatureDomainProvider +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new wallet account manager. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "accountmanager").Str("impl", "wallet").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + stores := make([]e2wtypes.Store, 0, len(parameters.locations)) + if len(parameters.locations) == 0 { + // Use default location. + stores = append(stores, filesystem.New()) + } else { + for _, location := range parameters.locations { + stores = append(stores, filesystem.New(filesystem.WithLocation(location))) + } + } + + slotsPerEpoch, err := parameters.slotsPerEpochProvider.SlotsPerEpoch(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain slots per epoch") + } + beaconAttesterDomain, err := parameters.beaconAttesterDomainProvider.BeaconAttesterDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain beacon attester domain") + } + beaconProposerDomain, err := parameters.beaconProposerDomainProvider.BeaconProposerDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain beacon proposer domain") + } + randaoDomain, err := parameters.randaoDomainProvider.RANDAODomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain RANDAO domain") + } + selectionProofDomain, err := parameters.selectionProofDomainProvider.SelectionProofDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain selection proof domain") + } + aggregateAndProofDomain, err := parameters.aggregateAndProofDomainProvider.AggregateAndProofDomain(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain aggregate and proof domain") + } + s := &Service{ + monitor: parameters.monitor, + stores: stores, + accountPaths: parameters.accountPaths, + passphrases: parameters.passphrases, + validatorsProvider: parameters.validatorsProvider, + slotsPerEpoch: slotsPerEpoch, + beaconAttesterDomain: beaconAttesterDomain, + beaconProposerDomain: beaconProposerDomain, + randaoDomain: randaoDomain, + selectionProofDomain: selectionProofDomain, + aggregateAndProofDomain: aggregateAndProofDomain, + signatureDomainProvider: parameters.signatureDomainProvider, + } + + if err := s.RefreshAccounts(ctx); err != nil { + return nil, errors.Wrap(err, "failed to fetch validating keys") + } + + return s, nil +} + +// UpdateAccountsState updates account state with the latest information from the beacon chain. +// This should be run at the beginning of each epoch to ensure that any newly-activated accounts are registered. +func (s *Service) UpdateAccountsState(ctx context.Context) error { + validatorIDs := make([]eth2client.ValidatorIDProvider, 0, len(s.accounts)) + for _, account := range s.accounts { + if !account.state.IsAttesting() { + validatorIDs = append(validatorIDs, account) + } + } + if len(validatorIDs) == 0 { + // Nothing to do. + log.Trace().Msg("No unactivated keys") + return nil + } + validators, err := s.validatorsProvider.Validators(ctx, "head", validatorIDs) + if err != nil { + return errors.Wrap(err, "failed to obtain validators") + } + + s.mutex.Lock() + s.updateAccountStates(ctx, s.accounts, validators) + s.mutex.Unlock() + + return nil +} + +// RefreshAccounts refreshes the entire list of validating keys. +func (s *Service) RefreshAccounts(ctx context.Context) error { + // Find the relevant wallets. + wallets := make(map[string]e2wtypes.Wallet) + pathsByWallet := make(map[string][]string) + for _, path := range s.accountPaths { + pathBits := strings.Split(path, "/") + + var paths []string + var exists bool + if paths, exists = pathsByWallet[pathBits[0]]; !exists { + paths = make([]string, 0) + } + pathsByWallet[pathBits[0]] = append(paths, path) + // Try each store in turn. + found := false + for _, store := range s.stores { + wallet, err := e2wallet.OpenWallet(pathBits[0], e2wallet.WithStore(store)) + if err == nil { + wallets[wallet.Name()] = wallet + found = true + break + } + } + if !found { + log.Warn().Str("wallet", pathBits[0]).Msg("Failed to find wallet in any store") + } + } + + verificationRegexes := accountPathsToVerificationRegexes(s.accountPaths) + // Fetch accounts for each wallet. + accounts := make(map[[48]byte]*ValidatingAccount) + for _, wallet := range wallets { + // if _, isProvider := wallet.(e2wtypes.WalletAccountsByPathProvider); isProvider { + // fmt.Printf("TODO: fetch accounts by path") + // } else { + s.fetchAccountsForWallet(ctx, wallet, accounts, verificationRegexes) + //} + } + + validatorIDs := make([]eth2client.ValidatorIDProvider, 0, len(accounts)) + for _, account := range accounts { + if !account.state.IsAttesting() { + validatorIDs = append(validatorIDs, account) + } + } + log.Trace().Int("keys", len(accounts)).Msg("Keys obtained") + if len(validatorIDs) == 0 { + log.Warn().Msg("No accounts obtained") + return nil + } + + validators, err := s.validatorsProvider.Validators(ctx, "head", validatorIDs) + if err != nil { + return errors.Wrap(err, "failed to obtain validators") + } + s.updateAccountStates(ctx, accounts, validators) + + s.mutex.Lock() + s.accounts = accounts + s.mutex.Unlock() + + return nil +} + +// Accounts returns all attesting accounts. +func (s *Service) Accounts(ctx context.Context) ([]accountmanager.ValidatingAccount, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + accounts := make([]accountmanager.ValidatingAccount, 0, len(s.accounts)) + for _, account := range s.accounts { + if account.state.IsAttesting() { + accounts = append(accounts, account) + } + } + + return accounts, nil +} + +// AccountsByIndex returns attesting accounts. +func (s *Service) AccountsByIndex(ctx context.Context, indices []uint64) ([]accountmanager.ValidatingAccount, error) { + indexMap := make(map[uint64]bool) + for _, index := range indices { + indexMap[index] = true + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + accounts := make([]accountmanager.ValidatingAccount, 0, len(s.accounts)) + for _, account := range s.accounts { + if !account.state.IsAttesting() { + continue + } + index, err := account.Index(ctx) + if err != nil { + log.Error().Err(err).Msg("No index for account") + continue + } + if _, exists := indexMap[index]; exists { + accounts = append(accounts, account) + + } + } + + return accounts, nil +} + +// AccountsByPubKey returns validating accounts. +func (s *Service) AccountsByPubKey(ctx context.Context, pubKeys [][]byte) ([]accountmanager.ValidatingAccount, error) { + pubKeyMap := make(map[[48]byte]bool) + for _, pubKey := range pubKeys { + var mapKey [48]byte + copy(mapKey[:], pubKey) + pubKeyMap[mapKey] = true + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + accounts := make([]accountmanager.ValidatingAccount, 0, len(s.accounts)) + for pubKey, account := range s.accounts { + if !account.state.IsAttesting() { + continue + } + if _, exists := pubKeyMap[pubKey]; exists { + accounts = append(accounts, account) + } + } + + return accounts, nil +} + +// accountPathsToVerificationRegexes turns account paths in to regexes to allow verification. +func accountPathsToVerificationRegexes(paths []string) []*regexp.Regexp { + regexes := make([]*regexp.Regexp, 0, len(paths)) + for _, path := range paths { + log := log.With().Str("path", path).Logger() + parts := strings.Split(path, "/") + if len(parts) == 0 || len(parts[0]) == 0 { + log.Debug().Msg("Invalid path") + continue + } + if len(parts) == 1 { + parts = append(parts, ".*") + } + parts[1] = strings.TrimPrefix(parts[1], "^") + var specifier string + if strings.HasSuffix(parts[1], "$") { + specifier = fmt.Sprintf("^%s/%s", parts[0], parts[1]) + } else { + specifier = fmt.Sprintf("^%s/%s$", parts[0], parts[1]) + } + regex, err := regexp.Compile(specifier) + if err != nil { + log.Warn().Str("specifier", specifier).Err(err).Msg("Invalid path regex") + continue + } + regexes = append(regexes, regex) + } + return regexes +} + +func (s *Service) updateAccountStates(ctx context.Context, accounts map[[48]byte]*ValidatingAccount, validators map[uint64]*api.Validator) { + validatorsByPubKey := make(map[[48]byte]*api.Validator, len(validators)) + for _, validator := range validators { + var pubKey [48]byte + copy(pubKey[:], validator.Validator.PublicKey) + validatorsByPubKey[pubKey] = validator + } + + validatorStateCounts := make(map[string]uint64) + for pubKey, account := range accounts { + validator, exists := validatorsByPubKey[pubKey] + if exists { + account.index = validator.Index + account.state = validator.State + } + validatorStateCounts[strings.ToLower(account.state.String())]++ + } + for state, count := range validatorStateCounts { + s.monitor.Accounts(state, count) + } + + if e := log.Trace(); e.Enabled() { + for _, account := range accounts { + log.Trace(). + Str("name", account.account.Name()). + Str("public_key", fmt.Sprintf("%x", account.account.PublicKey().Marshal())). + Str("state", account.state.String()). + Msg("Validating account") + } + } +} + +func (s *Service) fetchAccountsForWallet(ctx context.Context, wallet e2wtypes.Wallet, accounts map[[48]byte]*ValidatingAccount, verificationRegexes []*regexp.Regexp) { + for account := range wallet.Accounts(ctx) { + // Ensure the name matches one of our account paths. + name := fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) + verified := false + for _, verificationRegex := range verificationRegexes { + if verificationRegex.Match([]byte(name)) { + verified = true + break + } + } + if !verified { + log.Debug().Str("account", name).Msg("Received unwanted account from server; ignoring") + continue + } + + var pubKey []byte + if provider, isProvider := account.(e2wtypes.AccountCompositePublicKeyProvider); isProvider { + pubKey = provider.CompositePublicKey().Marshal() + } else { + pubKey = account.PublicKey().Marshal() + } + + // Ensure we can unlock the account with a known passphrase. + if unlocker, isUnlocker := account.(e2wtypes.AccountLocker); isUnlocker { + unlocked := false + for _, passphrase := range s.passphrases { + if err := unlocker.Unlock(ctx, passphrase); err == nil { + unlocked = true + break + } + } + if !unlocked { + log.Warn().Str("account", name).Msg("Failed to unlock account with any passphrase") + continue + } + } + + // Set up account as unknown to beacon chain. + accounts[bytesutil.ToBytes48(pubKey)] = &ValidatingAccount{ + account: account, + accountManager: s, + signatureDomainProvider: s.signatureDomainProvider, + } + } +} diff --git a/services/accountmanager/wallet/signingcontainer.go b/services/accountmanager/wallet/signingcontainer.go new file mode 100644 index 0000000..d1ddd33 --- /dev/null +++ b/services/accountmanager/wallet/signingcontainer.go @@ -0,0 +1,20 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wallet + +// SigningContainer is the container for signing roots with a domain. +type SigningContainer struct { + Root []byte `ssz-size:"32"` + Domain []byte `ssz-size:"32"` +} diff --git a/services/accountmanager/wallet/signingcontainer_encoding.go b/services/accountmanager/wallet/signingcontainer_encoding.go new file mode 100644 index 0000000..4c04a0e --- /dev/null +++ b/services/accountmanager/wallet/signingcontainer_encoding.go @@ -0,0 +1,88 @@ +// Code generated by fastssz. DO NOT EDIT. +package wallet + +import ( + ssz "github.com/ferranbt/fastssz" +) + +// MarshalSSZ ssz marshals the SigningContainer object +func (s *SigningContainer) MarshalSSZ() ([]byte, error) { + return ssz.MarshalSSZ(s) +} + +// MarshalSSZTo ssz marshals the SigningContainer object to a target array +func (s *SigningContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + + // Field (0) 'Root' + if len(s.Root) != 32 { + err = ssz.ErrBytesLength + return + } + dst = append(dst, s.Root...) + + // Field (1) 'Domain' + if len(s.Domain) != 32 { + err = ssz.ErrBytesLength + return + } + dst = append(dst, s.Domain...) + + return +} + +// UnmarshalSSZ ssz unmarshals the SigningContainer object +func (s *SigningContainer) UnmarshalSSZ(buf []byte) error { + var err error + size := uint64(len(buf)) + if size != 64 { + return ssz.ErrSize + } + + // Field (0) 'Root' + if cap(s.Root) == 0 { + s.Root = make([]byte, 0, len(buf[0:32])) + } + s.Root = append(s.Root, buf[0:32]...) + + // Field (1) 'Domain' + if cap(s.Domain) == 0 { + s.Domain = make([]byte, 0, len(buf[32:64])) + } + s.Domain = append(s.Domain, buf[32:64]...) + + return err +} + +// SizeSSZ returns the ssz encoded size in bytes for the SigningContainer object +func (s *SigningContainer) SizeSSZ() (size int) { + size = 64 + return +} + +// HashTreeRoot ssz hashes the SigningContainer object +func (s *SigningContainer) HashTreeRoot() ([32]byte, error) { + return ssz.HashWithDefaultHasher(s) +} + +// HashTreeRootWith ssz hashes the SigningContainer object with a hasher +func (s *SigningContainer) HashTreeRootWith(hh *ssz.Hasher) (err error) { + indx := hh.Index() + + // Field (0) 'Root' + if len(s.Root) != 32 { + err = ssz.ErrBytesLength + return + } + hh.PutBytes(s.Root) + + // Field (1) 'Domain' + if len(s.Domain) != 32 { + err = ssz.ErrBytesLength + return + } + hh.PutBytes(s.Domain) + + hh.Merkleize(indx) + return +} diff --git a/services/accountmanager/wallet/validatingaccount.go b/services/accountmanager/wallet/validatingaccount.go new file mode 100644 index 0000000..c5009a0 --- /dev/null +++ b/services/accountmanager/wallet/validatingaccount.go @@ -0,0 +1,188 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wallet + +import ( + "context" + "encoding/binary" + + eth2client "github.com/attestantio/go-eth2-client" + api "github.com/attestantio/go-eth2-client/api/v1" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// ValidatingAccount is a wrapper around the wallet account that implements ValidatingAccount. +type ValidatingAccount struct { + account e2wtypes.Account + index uint64 + state api.ValidatorState + accountManager *Service + signatureDomainProvider eth2client.SignatureDomainProvider +} + +// PubKey returns the public key of the validating account. +func (d *ValidatingAccount) PubKey(ctx context.Context) ([]byte, error) { + if provider, isProvider := d.account.(e2wtypes.AccountCompositePublicKeyProvider); isProvider { + return provider.CompositePublicKey().Marshal(), nil + } + return d.account.PublicKey().Marshal(), nil +} + +// Index returns the index of the validating account. +func (d *ValidatingAccount) Index(ctx context.Context) (uint64, error) { + return d.index, nil +} + +// State returns the state of the validating account. +func (d *ValidatingAccount) State() api.ValidatorState { + return d.state +} + +// SignSlotSelection returns a slot selection signature. +// This signs a slot with the "selection proof" domain. +func (d *ValidatingAccount) SignSlotSelection(ctx context.Context, slot uint64) ([]byte, error) { + messageRoot := make([]byte, 32) + binary.LittleEndian.PutUint64(messageRoot, slot) + + // Calculate the signature domain. + domain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.selectionProofDomain, + slot/d.accountManager.slotsPerEpoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for selection proof") + } + + return d.sign(ctx, messageRoot, domain) +} + +// SignRANDAOReveal returns a RANDAO reveal signature. +// This signs an epoch with the "RANDAO reveal" domain. +// N.B. This passes in a slot, not an epoch. +func (d *ValidatingAccount) SignRANDAOReveal(ctx context.Context, slot uint64) ([]byte, error) { + messageRoot := make([]byte, 32) + epoch := slot / d.accountManager.slotsPerEpoch + binary.LittleEndian.PutUint64(messageRoot, epoch) + + // Obtain the RANDAO reveal signature domain. + domain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.randaoDomain, + epoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for RANDAO reveal") + } + + return d.sign(ctx, messageRoot, domain) +} + +// SignBeaconBlockProposal signs a beacon block proposal item. +func (d *ValidatingAccount) SignBeaconBlockProposal(ctx context.Context, + slot uint64, + proposerIndex uint64, + parentRoot []byte, + stateRoot []byte, + bodyRoot []byte) ([]byte, error) { + + message := &spec.BeaconBlockHeader{ + Slot: slot, + ProposerIndex: proposerIndex, + ParentRoot: parentRoot, + StateRoot: stateRoot, + BodyRoot: bodyRoot, + } + messageRoot, err := message.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "failed to obtain hash tree root of block") + } + + // Obtain the signature domain. + domain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.beaconProposerDomain, + slot/d.accountManager.slotsPerEpoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for beacon proposal") + } + + return d.sign(ctx, messageRoot[:], domain) +} + +// SignBeaconAttestation signs a beacon attestation item. +func (d *ValidatingAccount) SignBeaconAttestation(ctx context.Context, + slot uint64, + committeeIndex uint64, + blockRoot []byte, + sourceEpoch uint64, + sourceRoot []byte, + targetEpoch uint64, + targetRoot []byte) ([]byte, error) { + + message := &spec.AttestationData{ + Slot: slot, + Index: committeeIndex, + BeaconBlockRoot: blockRoot, + Source: &spec.Checkpoint{ + Epoch: sourceEpoch, + Root: sourceRoot, + }, + Target: &spec.Checkpoint{ + Epoch: targetEpoch, + Root: targetRoot, + }, + } + messageRoot, err := message.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "failed to obtain hash tree root of attestation data") + } + + domain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.beaconAttesterDomain, + slot/d.accountManager.slotsPerEpoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for beacon attestation") + } + + return d.sign(ctx, messageRoot[:], domain) +} + +// SignAggregateAndProof signs an aggregate and proof item. +func (d *ValidatingAccount) SignAggregateAndProof(ctx context.Context, slot uint64, aggregateAndProofRoot []byte) ([]byte, error) { + + // Fetch the signature domain. + domain, err := d.signatureDomainProvider.SignatureDomain(ctx, + d.accountManager.aggregateAndProofDomain, + slot) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain signature domain for beacon aggregate and proof") + } + + return d.sign(ctx, aggregateAndProofRoot, domain) +} + +func (d *ValidatingAccount) sign(ctx context.Context, messageRoot []byte, domain []byte) ([]byte, error) { + container := &SigningContainer{ + Root: messageRoot, + Domain: domain, + } + signingRoot, err := container.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "failed to generate hash tree root for signing container") + } + + sig, err := d.account.(e2wtypes.AccountSigner).Sign(ctx, signingRoot[:]) + if err != nil { + return nil, errors.Wrap(err, "failed to sign beacon block proposal") + } + return sig.Marshal(), nil +} diff --git a/services/attestationaggregator/service.go b/services/attestationaggregator/service.go new file mode 100644 index 0000000..7602afe --- /dev/null +++ b/services/attestationaggregator/service.go @@ -0,0 +1,80 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package attestationaggregator + +import ( + "context" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// Duty contains information about an attestation aggregation duty. +type Duty struct { + validatorIndex uint64 + validatorPubKey []byte + slotSignature []byte + attestation *spec.Attestation +} + +// NewDuty creates a new attestation aggregation duty. +func NewDuty(ctx context.Context, validatorIndex uint64, validatorPubKey []byte, attestation *spec.Attestation, slotSignature []byte) (*Duty, error) { + return &Duty{ + validatorIndex: validatorIndex, + validatorPubKey: validatorPubKey, + slotSignature: slotSignature, + attestation: attestation, + }, nil +} + +// Slot provides the slot for the attestaton aggregation. +func (d *Duty) Slot() uint64 { + return d.attestation.Data.Slot +} + +// CommitteeIndex provides the committee index for the attestaton aggregation. +func (d *Duty) CommitteeIndex() uint64 { + return d.attestation.Data.Index +} + +// ValidatorIndex provides the index of the validator carrying out the attestation aggregation. +func (d *Duty) ValidatorIndex() uint64 { + return d.validatorIndex +} + +// ValidatorPubKey provides the public key of the validator carrying out the attestation aggregation. +func (d *Duty) ValidatorPubKey() []byte { + return d.validatorPubKey +} + +// Attestation provides the attestation of the validator carrying out the attestation aggregation. +func (d *Duty) Attestation() *spec.Attestation { + return d.attestation +} + +// SlotSignature provides the slot signature of the validator carrying out the attestation aggregation. +func (d *Duty) SlotSignature() []byte { + return d.slotSignature +} + +// IsAggregatorProvider provides information about if a validator is an aggregator. +type IsAggregatorProvider interface { + // IsAggregator returns true if the given validator is an aggregator for the given committee at the given slot. + IsAggregator(ctx context.Context, validatorIndex uint64, committeeIndex uint64, slot uint64, committeeSize uint64) (bool, []byte, error) +} + +// Service is the attestation aggregation service. +type Service interface { + // Aggregate carries out aggregation for a slot and committee. + Aggregate(ctx context.Context, details interface{}) +} diff --git a/services/attestationaggregator/standard/parameters.go b/services/attestationaggregator/standard/parameters.go new file mode 100644 index 0000000..979aa73 --- /dev/null +++ b/services/attestationaggregator/standard/parameters.go @@ -0,0 +1,115 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.AttestationAggregationMonitor + targetAggregatorsPerCommitteeProvider eth2client.TargetAggregatorsPerCommitteeProvider + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + aggregateAttestationProvider eth2client.NonSpecAggregateAttestationProvider + aggregateAttestationSubmitter submitter.AggregateAttestationSubmitter +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithTargetAggregatorsPerCommitteeProvider sets the target aggregators per attestation provider. +func WithTargetAggregatorsPerCommitteeProvider(provider eth2client.TargetAggregatorsPerCommitteeProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.targetAggregatorsPerCommitteeProvider = provider + }) +} + +// WithMonitor sets the monitor for this module. +func WithMonitor(monitor metrics.AttestationAggregationMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithValidatingAccountsProvider sets the account manager. +func WithValidatingAccountsProvider(provider accountmanager.ValidatingAccountsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatingAccountsProvider = provider + }) +} + +// WithAggregateAttestationDataProvider sets the aggregate attestation provider. +func WithAggregateAttestationDataProvider(provider eth2client.NonSpecAggregateAttestationProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.aggregateAttestationProvider = provider + }) +} + +// WithAggregateAttestationSubmitter sets the aggregate attestation submitter. +func WithAggregateAttestationSubmitter(submitter submitter.AggregateAttestationSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.aggregateAttestationSubmitter = submitter + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.targetAggregatorsPerCommitteeProvider == nil { + return nil, errors.New("no target aggregators per committee provider specified") + } + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.validatingAccountsProvider == nil { + return nil, errors.New("no validating accounts provider specified") + } + if parameters.aggregateAttestationProvider == nil { + return nil, errors.New("no aggregate attestation provider specified") + } + if parameters.aggregateAttestationSubmitter == nil { + return nil, errors.New("no aggregate attestation submitter specified") + } + + return ¶meters, nil +} diff --git a/services/attestationaggregator/standard/service.go b/services/attestationaggregator/standard/service.go new file mode 100644 index 0000000..ff01c65 --- /dev/null +++ b/services/attestationaggregator/standard/service.go @@ -0,0 +1,198 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/attestationaggregator" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is an attestation aggregator. +type Service struct { + monitor metrics.AttestationAggregationMonitor + targetAggregatorsPerCommittee uint64 + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + aggregateAttestationProvider eth2client.NonSpecAggregateAttestationProvider + aggregateAttestationSubmitter submitter.AggregateAttestationSubmitter +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new attestation aggregator. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "attestationaggregator").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + targetAggregatorsPerCommittee, err := parameters.targetAggregatorsPerCommitteeProvider.TargetAggregatorsPerCommittee(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain target aggregators per committee") + } + + s := &Service{ + monitor: parameters.monitor, + targetAggregatorsPerCommittee: targetAggregatorsPerCommittee, + validatingAccountsProvider: parameters.validatingAccountsProvider, + aggregateAttestationProvider: parameters.aggregateAttestationProvider, + aggregateAttestationSubmitter: parameters.aggregateAttestationSubmitter, + } + + return s, nil +} + +// Aggregate aggregates the attestations for a given slot/committee combination. +func (s *Service) Aggregate(ctx context.Context, data interface{}) { + started := time.Now() + + duty, ok := data.(*attestationaggregator.Duty) + if !ok { + log.Error().Msg("Passed invalid data structure") + s.monitor.AttestationAggregationCompleted(started, "failed") + return + } + log := log.With().Uint64("slot", duty.Slot()).Uint64("committee_index", duty.CommitteeIndex()).Logger() + log.Trace().Msg("Aggregating") + + // Obtain the aggregate attestation. + aggregateAttestation, err := s.aggregateAttestationProvider.NonSpecAggregateAttestation(ctx, + duty.Attestation(), + duty.ValidatorPubKey(), + duty.SlotSignature()) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain aggregate attestation") + s.monitor.AttestationAggregationCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained aggregate attestation") + + // Fetch the validating account. + accounts, err := s.validatingAccountsProvider.AccountsByPubKey(ctx, [][]byte{duty.ValidatorPubKey()}) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain proposing validator account") + s.monitor.AttestationAggregationCompleted(started, "failed") + return + } + if len(accounts) != 1 { + log.Error().Err(err).Msg("Unknown proposing validator account") + s.monitor.AttestationAggregationCompleted(started, "failed") + return + } + account := accounts[0] + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained aggregating account") + + // Sign the aggregate attestation. + signer, isSigner := account.(accountmanager.AggregateAndProofSigner) + if !isSigner { + log.Error().Msg("Account is not an aggregate and proof signer") + s.monitor.AttestationAggregationCompleted(started, "failed") + return + } + aggregateAndProof := &spec.AggregateAndProof{ + AggregatorIndex: duty.ValidatorIndex(), + Aggregate: aggregateAttestation, + SelectionProof: duty.SlotSignature(), + } + + aggregateAndProofRoot, err := aggregateAndProof.HashTreeRoot() + if err != nil { + log.Error().Err(err).Msg("Failed to generate hash tree root of aggregate and proof") + } + sig, err := signer.SignAggregateAndProof(ctx, duty.Slot(), aggregateAndProofRoot[:]) + if err != nil { + log.Error().Err(err).Msg("Failed to sign aggregate and proof") + s.monitor.AttestationAggregationCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Signed aggregate attestation") + + // Submit the signed aggregate and proof. + signedAggregateAndProof := &spec.SignedAggregateAndProof{ + Message: aggregateAndProof, + Signature: sig, + } + if err := s.aggregateAttestationSubmitter.SubmitAggregateAttestation(ctx, signedAggregateAndProof); err != nil { + log.Error().Err(err).Msg("Failed to submit aggregate and proof") + s.monitor.AttestationAggregationCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Submitted aggregate attestation") + + frac := float64(aggregateAndProof.Aggregate.AggregationBits.Count()) / + float64(aggregateAndProof.Aggregate.AggregationBits.Len()) + s.monitor.AttestationAggregationCoverage(frac) + s.monitor.AttestationAggregationCompleted(started, "succeeded") +} + +// IsAggregator reports if we are an attestation aggregator for a given valdiator/committee/slot combination. +func (s *Service) IsAggregator(ctx context.Context, validatorIndex uint64, committeeIndex uint64, slot uint64, committeeSize uint64) (bool, []byte, error) { + modulo := committeeSize / s.targetAggregatorsPerCommittee + if modulo == 0 { + // Modulo must be at least 1. + modulo = 1 + } + + // Fetch the validator from the account manager. + accounts, err := s.validatingAccountsProvider.AccountsByIndex(ctx, []uint64{validatorIndex}) + if err != nil { + return false, nil, errors.Wrap(err, "failed to obtain validator") + } + if len(accounts) == 0 { + return false, nil, errors.Wrap(err, "validator unknown") + } + account := accounts[0] + + slotSelectionSigner, isSlotSelectionSigner := account.(accountmanager.SlotSelectionSigner) + if !isSlotSelectionSigner { + return false, nil, errors.New("validating account is not a slot selection signer") + } + + // Sign the slot. + signature, err := slotSelectionSigner.SignSlotSelection(ctx, slot) + if err != nil { + return false, nil, errors.Wrap(err, "failed to sign the slot") + } + + // Hash the signature. + sigHash := sha256.New() + n, err := sigHash.Write(signature) + if err != nil { + return false, nil, errors.Wrap(err, "failed to hash the slot signature") + } + if n != len(signature) { + return false, nil, errors.New("failed to write all bytes of the slot signature to the hash") + } + hash := sigHash.Sum(nil) + + return binary.LittleEndian.Uint64(hash[:8])%modulo == 0, signature, nil +} diff --git a/services/attester/helpers.go b/services/attester/helpers.go new file mode 100644 index 0000000..647fbbf --- /dev/null +++ b/services/attester/helpers.go @@ -0,0 +1,95 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package attester + +import ( + "context" + "sort" + + api "github.com/attestantio/go-eth2-client/api/v1" +) + +// MergeDuties merges attester duties given by an Ethereum 2 client into vouch's per-slot structure. +func MergeDuties(ctx context.Context, attesterDuties []*api.AttesterDuty) ([]*Duty, error) { + validatorIndices := make(map[uint64][]uint64) + committeeIndices := make(map[uint64][]uint64) + validatorCommitteeIndices := make(map[uint64][]uint64) + committeeLengths := make(map[uint64]map[uint64]uint64) + + // Set the base capacity for our arrays based on the number of attester duties. + // This is much higher than we need, but is overall minimal and avoids reallocations. + arrayCap := uint64(len(attesterDuties)) + + // Sort the response by slot, then committee index, then validator index. + sort.Slice(attesterDuties, func(i int, j int) bool { + if attesterDuties[i].Slot < attesterDuties[j].Slot { + return true + } + if attesterDuties[i].Slot > attesterDuties[j].Slot { + return false + } + if attesterDuties[i].CommitteeIndex < attesterDuties[j].CommitteeIndex { + return true + } + if attesterDuties[i].CommitteeIndex > attesterDuties[j].CommitteeIndex { + return false + } + return attesterDuties[i].ValidatorIndex < attesterDuties[j].ValidatorIndex + }) + + for _, duty := range attesterDuties { + // Future optimisation (maybe; depends how much effort it is to fetch validator status here): + // There are three states where a validator is given duties: active, exiting and slashing. + // However, if the validator is slashing its attestations are ignored by the network. + // Hence, if the validator is slashed we don't need to include its duty. + + _, exists := validatorIndices[duty.Slot] + if !exists { + validatorIndices[duty.Slot] = make([]uint64, 0, arrayCap) + committeeIndices[duty.Slot] = make([]uint64, 0, arrayCap) + committeeLengths[duty.Slot] = make(map[uint64]uint64) + } + validatorIndices[duty.Slot] = append(validatorIndices[duty.Slot], duty.ValidatorIndex) + committeeIndices[duty.Slot] = append(committeeIndices[duty.Slot], duty.CommitteeIndex) + committeeLengths[duty.Slot][duty.CommitteeIndex] = duty.CommitteeLength + validatorCommitteeIndices[duty.Slot] = append(validatorCommitteeIndices[duty.Slot], duty.ValidatorCommitteeIndex) + } + + duties := make([]*Duty, 0, len(validatorIndices)) + for slot := range validatorIndices { + if duty, err := NewDuty( + ctx, + slot, + validatorIndices[slot], + committeeIndices[slot], + validatorCommitteeIndices[slot], + committeeLengths[slot], + ); err == nil { + duties = append(duties, duty) + } + } + + // Order the attester duties by slot. + sort.Slice(duties, func(i int, j int) bool { + if duties[i].Slot() < duties[j].Slot() { + return true + } + if duties[i].Slot() > duties[j].Slot() { + return false + } + return true + }) + + return duties, nil +} diff --git a/services/attester/service.go b/services/attester/service.go new file mode 100644 index 0000000..ebcd83a --- /dev/null +++ b/services/attester/service.go @@ -0,0 +1,85 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package attester + +import ( + "context" + "fmt" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// Duty contains information about a beacon block attester duty. +type Duty struct { + slot uint64 + validatorIndices []uint64 + committeeIndices []uint64 + validatorCommitteeIndices []uint64 + committeeLengths map[uint64]uint64 +} + +// NewDuty creates a new beacon block attester duty. +func NewDuty(ctx context.Context, slot uint64, validatorIndices []uint64, committeeIndices []uint64, validatorCommitteeIndices []uint64, committeeLengths map[uint64]uint64) (*Duty, error) { + // Ensure there is a matching committee size for each committee index. + for i := range committeeIndices { + if _, exists := committeeLengths[committeeIndices[i]]; !exists { + return nil, fmt.Errorf("committee %d does not have a committee size; duty invalid", committeeIndices[i]) + } + } + + return &Duty{ + slot: slot, + validatorIndices: validatorIndices, + committeeIndices: committeeIndices, + validatorCommitteeIndices: validatorCommitteeIndices, + committeeLengths: committeeLengths, + }, nil +} + +// Slot provides the slot for the beacon block attester. +func (d *Duty) Slot() uint64 { + return d.slot +} + +// ValidatorIndices provides the validator indices for the beacon block attester. +func (d *Duty) ValidatorIndices() []uint64 { + return d.validatorIndices +} + +// CommitteeIndices provides the committee indices for the beacon block attester. +func (d *Duty) CommitteeIndices() []uint64 { + return d.committeeIndices +} + +// ValidatorCommitteeIndices provides the indices of validators within committees for the beacon block attester. +func (d *Duty) ValidatorCommitteeIndices() []uint64 { + return d.validatorCommitteeIndices +} + +// CommitteeSize provides the committee size for a given index. +func (d *Duty) CommitteeSize(committeeIndex uint64) uint64 { + return d.committeeLengths[committeeIndex] +} + +// String provides a friendly string for the struct's main details. +func (d *Duty) String() string { + return fmt.Sprintf("beacon block attester for slot %d with validators %v committee indices %v", d.slot, d.validatorIndices, d.committeeIndices) +} + +// Service is the beacon block attester service. +type Service interface { + // Attest carries out attestations for a slot. + // It returns a list of attestations made. + Attest(ctx context.Context, details interface{}) ([]*spec.Attestation, error) +} diff --git a/services/attester/standard/parameters.go b/services/attester/standard/parameters.go new file mode 100644 index 0000000..63a2a12 --- /dev/null +++ b/services/attester/standard/parameters.go @@ -0,0 +1,126 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + processConcurrency int64 + monitor metrics.AttestationMonitor + slotsPerEpochProvider eth2client.SlotsPerEpochProvider + attestationDataProvider eth2client.AttestationDataProvider + attestationSubmitter submitter.AttestationSubmitter + validatingAccountsProvider accountmanager.ValidatingAccountsProvider +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithProcessConcurrency sets the concurrency for the service. +func WithProcessConcurrency(concurrency int64) Parameter { + return parameterFunc(func(p *parameters) { + p.processConcurrency = concurrency + }) +} + +// WithSlotsPerEpochProvider sets the slots per epoch provider. +func WithSlotsPerEpochProvider(provider eth2client.SlotsPerEpochProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.slotsPerEpochProvider = provider + }) +} + +// WithAttestationDataProvider sets the attestation data provider. +func WithAttestationDataProvider(provider eth2client.AttestationDataProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.attestationDataProvider = provider + }) +} + +// WithAttestationSubmitter sets the attestation submitter. +func WithAttestationSubmitter(submitter submitter.AttestationSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.attestationSubmitter = submitter + }) +} + +// WithMonitor sets the monitor for this module. +func WithMonitor(monitor metrics.AttestationMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithValidatingAccountsProvider sets the account manager. +func WithValidatingAccountsProvider(provider accountmanager.ValidatingAccountsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatingAccountsProvider = provider + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.processConcurrency == 0 { + return nil, errors.New("no process concurrency specified") + } + if parameters.slotsPerEpochProvider == nil { + return nil, errors.New("no slots per epoch provider specified") + } + if parameters.attestationDataProvider == nil { + return nil, errors.New("no attestation data provider specified") + } + if parameters.attestationSubmitter == nil { + return nil, errors.New("no attestation submitter specified") + } + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.validatingAccountsProvider == nil { + return nil, errors.New("no validating accounts provider specified") + } + + return ¶meters, nil +} diff --git a/services/attester/standard/service.go b/services/attester/standard/service.go new file mode 100644 index 0000000..a78a5af --- /dev/null +++ b/services/attester/standard/service.go @@ -0,0 +1,216 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "fmt" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/attester" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/prysmaticlabs/go-bitfield" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "golang.org/x/sync/semaphore" +) + +// Service is a beacon block attester. +type Service struct { + monitor metrics.AttestationMonitor + processConcurrency int64 + slotsPerEpoch uint64 + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + attestationDataProvider eth2client.AttestationDataProvider + attestationSubmitter submitter.AttestationSubmitter +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new beacon block attester. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "attester").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + slotsPerEpoch, err := parameters.slotsPerEpochProvider.SlotsPerEpoch(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain slots per epoch") + } + + s := &Service{ + monitor: parameters.monitor, + processConcurrency: parameters.processConcurrency, + slotsPerEpoch: slotsPerEpoch, + validatingAccountsProvider: parameters.validatingAccountsProvider, + attestationDataProvider: parameters.attestationDataProvider, + attestationSubmitter: parameters.attestationSubmitter, + } + + return s, nil +} + +// Attest carries out attestations for a slot. +// It returns a map of attestations made, keyed on the validator index. +func (s *Service) Attest(ctx context.Context, data interface{}) ([]*spec.Attestation, error) { + started := time.Now() + + duty, ok := data.(*attester.Duty) + if !ok { + s.monitor.AttestationCompleted(started, "failed") + return nil, errors.New("passed invalid data structure") + } + log := log.With().Uint64("slot", duty.Slot()).Logger() + log.Trace().Uints64("validator_indices", duty.ValidatorIndices()).Msg("Attesting") + + attestations := make([]*spec.Attestation, 0, len(duty.ValidatorIndices())) + var attestationsMutex sync.Mutex + + // Fetch the attestation data. + attestationData, err := s.attestationDataProvider.AttestationData(ctx, duty.Slot(), duty.CommitteeIndices()[0]) + if err != nil { + s.monitor.AttestationCompleted(started, "failed") + return nil, errors.Wrap(err, "failed to obtain attestation data") + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained attestation data") + + if attestationData.Slot != duty.Slot() { + s.monitor.AttestationCompleted(started, "failed") + return nil, fmt.Errorf("attestation request for slot %d returned data for slot %d", duty.Slot(), attestationData.Slot) + } + + // Fetch the validating accounts. + accounts, err := s.validatingAccountsProvider.AccountsByIndex(ctx, duty.ValidatorIndices()) + if err != nil { + s.monitor.AttestationCompleted(started, "failed") + return nil, errors.New("failed to obtain attesting validator accounts") + } + log.Trace().Dur("elapsed", time.Since(started)).Uints64("validator_indices", duty.ValidatorIndices()).Msg("Obtained validating accounts") + + // Run the attestations in parallel, up to a concurrency limit. + validatorIndexToArrayIndexMap := make(map[uint64]int) + for i := range duty.ValidatorIndices() { + validatorIndexToArrayIndexMap[duty.ValidatorIndices()[i]] = i + } + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for _, account := range accounts { + wg.Add(1) + go func(sem *semaphore.Weighted, wg *sync.WaitGroup, account accountmanager.ValidatingAccount, attestations *[]*spec.Attestation, attestationsMutex *sync.Mutex) { + defer wg.Done() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + validatorIndex, err := account.Index(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain validator index") + return + } + log := log.With().Uint64("validator_index", validatorIndex).Logger() + attestation, err := s.attest(ctx, + duty.Slot(), + duty.CommitteeIndices()[validatorIndexToArrayIndexMap[validatorIndex]], + duty.ValidatorCommitteeIndices()[validatorIndexToArrayIndexMap[validatorIndex]], + duty.CommitteeSize(duty.CommitteeIndices()[validatorIndexToArrayIndexMap[validatorIndex]]), + account, + attestationData, + ) + if err != nil { + log.Warn().Err(err).Msg("Failed to attest") + s.monitor.AttestationCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Attested") + s.monitor.AttestationCompleted(started, "succeeded") + attestationsMutex.Lock() + *attestations = append(*attestations, attestation) + attestationsMutex.Unlock() + + }(sem, &wg, account, &attestations, &attestationsMutex) + } + wg.Wait() + + return attestations, nil +} + +func (s *Service) attest( + ctx context.Context, + slot uint64, + committeeIndex uint64, + validatorCommitteeIndex uint64, + committeeSize uint64, + account accountmanager.ValidatingAccount, + attestationData *spec.AttestationData, +) (*spec.Attestation, error) { + + // Sign the attestation. + signer, isSigner := account.(accountmanager.BeaconAttestationSigner) + if !isSigner { + return nil, errors.New("account is not a beacon attestation signer") + } + sig, err := signer.SignBeaconAttestation(ctx, + slot, + committeeIndex, + attestationData.BeaconBlockRoot, + attestationData.Source.Epoch, + attestationData.Source.Root, + attestationData.Target.Epoch, + attestationData.Target.Root) + if err != nil { + return nil, errors.Wrap(err, "failed to sign beacon attestation") + } + log.Trace().Msg("Signed") + + // Submit the attestation. + aggregationBits := bitfield.NewBitlist(committeeSize) + aggregationBits.SetBitAt(validatorCommitteeIndex, true) + attestation := &spec.Attestation{ + AggregationBits: aggregationBits, + Data: &spec.AttestationData{ + Slot: slot, + Index: committeeIndex, + BeaconBlockRoot: attestationData.BeaconBlockRoot, + Source: &spec.Checkpoint{ + Epoch: attestationData.Source.Epoch, + Root: attestationData.Source.Root, + }, + Target: &spec.Checkpoint{ + Epoch: attestationData.Target.Epoch, + Root: attestationData.Target.Root, + }, + }, + Signature: sig, + } + if err := s.attestationSubmitter.SubmitAttestation(ctx, attestation); err != nil { + return nil, errors.Wrap(err, "failed to submit attestation") + } + return attestation, nil +} diff --git a/services/beaconblockproposer/service.go b/services/beaconblockproposer/service.go index b44c3f4..1fe798f 100644 --- a/services/beaconblockproposer/service.go +++ b/services/beaconblockproposer/service.go @@ -13,14 +13,59 @@ package beaconblockproposer -// BeaconBlockProposerDuty contains information about a beacon block proposal. -type BeaconBlockProposerDuty interface { - Slot() uint64 - PublicKey() []byte +import ( + "context" + "fmt" +) + +// Duty contains information about a beacon block proposal duty. +type Duty struct { + // Details for the duty. + slot uint64 + validatorIndex uint64 + + // randaoReveal is required to be passed to the beacon node when proposing the block; can be pre-calculated. + randaoReveal []byte } -// BeaconBlockProposer defines an interface that proposes blocks. -type Proposer interface { - // Schedule passes a proposer schedule. - Schedule([]*BeaconBlockProposerDuty) +// NewDuty creates a new beacon block proposer duty. +func NewDuty(ctx context.Context, slot uint64, validatorIndex uint64) (*Duty, error) { + return &Duty{ + slot: slot, + validatorIndex: validatorIndex, + }, nil +} + +// Slot provides the slot for the beacon block proposer. +func (d *Duty) Slot() uint64 { + return d.slot +} + +// ValidatorIndex provides the validator index for the beacon block proposer. +func (d *Duty) ValidatorIndex() uint64 { + return d.validatorIndex +} + +// String provides a friendly string for the struct. +func (d *Duty) String() string { + return fmt.Sprintf("beacon block proposal %d@%d", d.validatorIndex, d.slot) +} + +// SetRandaoReveal sets the RANDAO reveal. +func (d *Duty) SetRandaoReveal(randaoReveal []byte) { + d.randaoReveal = randaoReveal +} + +// RANDAOReveal provides the RANDAO reveal. +func (d *Duty) RANDAOReveal() []byte { + return d.randaoReveal +} + +// Service is the beacon block proposer service. +type Service interface { + // Prepare prepares the proposal for a slot. + Prepare(ctx context.Context, details interface{}) error + + // Propose carries out the proposal for a slot. + Propose(ctx context.Context, details interface{}) } diff --git a/services/beaconblockproposer/standard/parameters.go b/services/beaconblockproposer/standard/parameters.go new file mode 100644 index 0000000..6510d52 --- /dev/null +++ b/services/beaconblockproposer/standard/parameters.go @@ -0,0 +1,114 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "errors" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/graffitiprovider" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.BeaconBlockProposalMonitor + proposalProvider eth2client.BeaconBlockProposalProvider + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + graffitiProvider graffitiprovider.Service + beaconBlockSubmitter submitter.BeaconBlockSubmitter +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithProposalDataProvider sets the proposal data provider. +func WithProposalDataProvider(provider eth2client.BeaconBlockProposalProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.proposalProvider = provider + }) +} + +// WithMonitor sets the monitor for this module. +func WithMonitor(monitor metrics.BeaconBlockProposalMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithValidatingAccountsProvider sets the account manager. +func WithValidatingAccountsProvider(provider accountmanager.ValidatingAccountsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatingAccountsProvider = provider + }) +} + +// WithGraffitiProvider sets the graffiti provider. +func WithGraffitiProvider(provider graffitiprovider.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.graffitiProvider = provider + }) +} + +// WithBeaconBlockSubmitter sets the beacon block submitter. +func WithBeaconBlockSubmitter(submitter submitter.BeaconBlockSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockSubmitter = submitter + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.proposalProvider == nil { + return nil, errors.New("no proposal data provider specified") + } + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.validatingAccountsProvider == nil { + return nil, errors.New("no validating accounts provider specified") + } + if parameters.beaconBlockSubmitter == nil { + return nil, errors.New("no beacon block submitter specified") + } + + return ¶meters, nil +} diff --git a/services/beaconblockproposer/standard/service.go b/services/beaconblockproposer/standard/service.go new file mode 100644 index 0000000..46cf698 --- /dev/null +++ b/services/beaconblockproposer/standard/service.go @@ -0,0 +1,211 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "fmt" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/beaconblockproposer" + "github.com/attestantio/vouch/services/graffitiprovider" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is a beacon block proposer. +type Service struct { + monitor metrics.BeaconBlockProposalMonitor + proposalProvider eth2client.BeaconBlockProposalProvider + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + graffitiProvider graffitiprovider.Service + beaconBlockSubmitter submitter.BeaconBlockSubmitter +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new beacon block proposer. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "beaconblockproposer").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + monitor: parameters.monitor, + proposalProvider: parameters.proposalProvider, + validatingAccountsProvider: parameters.validatingAccountsProvider, + graffitiProvider: parameters.graffitiProvider, + beaconBlockSubmitter: parameters.beaconBlockSubmitter, + } + + return s, nil +} + +// Prepare prepares for a beacon block proposal, carrying out activities that +// can be undertaken before the time the proposal is required. +func (s *Service) Prepare(ctx context.Context, data interface{}) error { + started := time.Now() + + duty, ok := data.(*beaconblockproposer.Duty) + if !ok { + return errors.New("passed invalid data structure") + } + log := log.With().Uint64("slot", duty.Slot()).Uint64("validator_index", duty.ValidatorIndex()).Logger() + log.Trace().Msg("Preparing") + + // Fetch the validating account. + accounts, err := s.validatingAccountsProvider.AccountsByIndex(ctx, []uint64{duty.ValidatorIndex()}) + if err != nil { + return errors.Wrap(err, "failed to obtain proposing validator account") + } + if len(accounts) != 1 { + return fmt.Errorf("unknown proposing validator account %d", duty.ValidatorIndex()) + } + account := accounts[0] + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained proposing account") + + revealSigner, isRevealSigner := account.(accountmanager.RANDAORevealSigner) + if !isRevealSigner { + return errors.New("account is not a RANDAO reveal signer") + } + randaoReveal, err := revealSigner.SignRANDAOReveal(ctx, duty.Slot()) + if err != nil { + return errors.Wrap(err, "failed to sign RANDAO reveal") + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Signed RANDAO reveal") + + duty.SetRandaoReveal(randaoReveal) + return nil +} + +// Propose proposes a block. +func (s *Service) Propose(ctx context.Context, data interface{}) { + started := time.Now() + + duty, ok := data.(*beaconblockproposer.Duty) + if !ok { + log.Error().Msg("Passed invalid data structure") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + log := log.With().Uint64("slot", duty.Slot()).Uint64("validator_index", duty.ValidatorIndex()).Logger() + log.Trace().Msg("Proposing") + + // Fetch the validating account. + accounts, err := s.validatingAccountsProvider.AccountsByIndex(ctx, []uint64{duty.ValidatorIndex()}) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain proposing validator account") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + if len(accounts) != 1 { + log.Error().Err(err).Msg("Unknown proposing validator account") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + account := accounts[0] + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained account") + + var graffiti []byte + if s.graffitiProvider != nil { + graffiti, err = s.graffitiProvider.Graffiti(ctx, duty.Slot(), duty.ValidatorIndex()) + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain graffiti") + graffiti = nil + } + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained graffiti") + + proposal, err := s.proposalProvider.BeaconBlockProposal(ctx, duty.Slot(), duty.RANDAOReveal(), graffiti) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain proposal data") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + if proposal == nil { + log.Error().Msg("Provider did not return beacon block proposal") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained proposal") + + if proposal.Slot != duty.Slot() { + log.Error().Uint64("proposal_slot", proposal.Slot).Msg("Proposal data for incorrect slot; not proceeding") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + + bodyRoot, err := proposal.Body.HashTreeRoot() + if err != nil { + log.Error().Err(err).Msg("Failed to calculate hash tree root of block") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + proposalData := ðpb.BeaconBlockHeader{ + Slot: proposal.Slot, + ProposerIndex: duty.ValidatorIndex(), + ParentRoot: proposal.ParentRoot, + StateRoot: proposal.StateRoot, + BodyRoot: bodyRoot[:], + } + + // Sign the block. + signer, isSigner := account.(accountmanager.BeaconBlockSigner) + if !isSigner { + log.Error().Msg("Account is not a beacon block signer") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + sig, err := signer.SignBeaconBlockProposal(ctx, + proposalData.Slot, + proposalData.ProposerIndex, + proposalData.ParentRoot, + proposalData.StateRoot, + proposalData.BodyRoot) + if err != nil { + log.Error().Err(err).Msg("Failed to sign beacon block proposal") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Signed proposal") + + signedBlock := &spec.SignedBeaconBlock{ + Message: proposal, + Signature: sig, + } + + // Submit the block. + if err := s.beaconBlockSubmitter.SubmitBeaconBlock(ctx, signedBlock); err != nil { + log.Error().Err(err).Msg("Failed to submit beacon block proposal") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Submitted proposal") + s.monitor.BeaconBlockProposalCompleted(started, "succeeded") +} diff --git a/services/beaconcommitteesubscriber/service.go b/services/beaconcommitteesubscriber/service.go new file mode 100644 index 0000000..796c649 --- /dev/null +++ b/services/beaconcommitteesubscriber/service.go @@ -0,0 +1,37 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package beaconcommitteesubscriber is a package that manages subscriptions for beacon committees. +package beaconcommitteesubscriber + +import ( + "context" + + "github.com/attestantio/vouch/services/accountmanager" +) + +// Subscription holds details of the committees to which we are subscribing. +type Subscription struct { + ValidatorIndex uint64 + ValidatorPubKey []byte + CommitteeSize uint64 + Signature []byte + Aggregate bool +} + +// Service is the beacon committee subscriber service. +type Service interface { + // Subscribe subscribes to beacon committees for a given epoch. + // It returns a map of slot => committee => subscription info. + Subscribe(ctx context.Context, epoch uint64, accounts []accountmanager.ValidatingAccount) (map[uint64]map[uint64]*Subscription, error) +} diff --git a/services/beaconcommitteesubscriber/standard/parameters.go b/services/beaconcommitteesubscriber/standard/parameters.go new file mode 100644 index 0000000..87dd371 --- /dev/null +++ b/services/beaconcommitteesubscriber/standard/parameters.go @@ -0,0 +1,115 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/attestationaggregator" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + processConcurrency int64 + monitor metrics.BeaconCommitteeSubscriptionMonitor + attesterDutiesProvider eth2client.AttesterDutiesProvider + beaconCommitteeSubmitter submitter.BeaconCommitteeSubscriptionsSubmitter + attestationAggregator attestationaggregator.Service +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithProcessConcurrency sets the concurrency for the service. +func WithProcessConcurrency(concurrency int64) Parameter { + return parameterFunc(func(p *parameters) { + p.processConcurrency = concurrency + }) +} + +// WithMonitor sets the monitor for the module. +func WithMonitor(monitor metrics.BeaconCommitteeSubscriptionMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithAttesterDutiesProvider sets the attester duties provider. +func WithAttesterDutiesProvider(provider eth2client.AttesterDutiesProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.attesterDutiesProvider = provider + }) +} + +// WithAttestationAggregator sets the attestation aggregator. +func WithAttestationAggregator(aggregator attestationaggregator.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.attestationAggregator = aggregator + }) +} + +// WithBeaconCommitteeSubmitter sets the beacon committee subscriptions submitter. +func WithBeaconCommitteeSubmitter(submitter submitter.BeaconCommitteeSubscriptionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconCommitteeSubmitter = submitter + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.processConcurrency == 0 { + return nil, errors.New("no process concurrency specified") + } + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.attesterDutiesProvider == nil { + return nil, errors.New("no attester duties provider specified") + } + if parameters.attestationAggregator == nil { + return nil, errors.New("no attestation aggregator specified") + } + if parameters.beaconCommitteeSubmitter == nil { + return nil, errors.New("no beacon committee submitter specified") + } + + return ¶meters, nil +} diff --git a/services/beaconcommitteesubscriber/standard/service.go b/services/beaconcommitteesubscriber/standard/service.go new file mode 100644 index 0000000..1ed36f6 --- /dev/null +++ b/services/beaconcommitteesubscriber/standard/service.go @@ -0,0 +1,232 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/attestationaggregator" + "github.com/attestantio/vouch/services/attester" + "github.com/attestantio/vouch/services/beaconcommitteesubscriber" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "github.com/sasha-s/go-deadlock" + "golang.org/x/sync/semaphore" +) + +// Service is an beacon committee subscriber. +type Service struct { + monitor metrics.BeaconCommitteeSubscriptionMonitor + processConcurrency int64 + attesterDutiesProvider eth2client.AttesterDutiesProvider + attestationAggregator attestationaggregator.Service + submitter submitter.BeaconCommitteeSubscriptionsSubmitter +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new beacon committee subscriber. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "beaconcommitteesubscriber").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + processConcurrency: parameters.processConcurrency, + monitor: parameters.monitor, + attesterDutiesProvider: parameters.attesterDutiesProvider, + attestationAggregator: parameters.attestationAggregator, + submitter: parameters.beaconCommitteeSubmitter, + } + + return s, nil +} + +// Subscribe subscribes to beacon committees for a given epoch. +// This returns data about the subnets to which we are subscribing. +func (s *Service) Subscribe(ctx context.Context, epoch uint64, accounts []accountmanager.ValidatingAccount) (map[uint64]map[uint64]*beaconcommitteesubscriber.Subscription, error) { + started := time.Now() + + log := log.With().Uint64("epoch", epoch).Logger() + log.Trace().Msg("Subscribing") + + idProviders := make([]eth2client.ValidatorIDProvider, len(accounts)) + for i, account := range accounts { + idProviders[i] = account.(eth2client.ValidatorIDProvider) + } + attesterDuties, err := s.attesterDutiesProvider.AttesterDuties(ctx, epoch, idProviders) + if err != nil { + s.monitor.BeaconCommitteeSubscriptionCompleted(started, "failed") + return nil, errors.Wrap(err, "failed to obtain attester duties") + } + log.Trace().Dur("elapsed", time.Since(started)).Int("accounts", len(idProviders)).Msg("Fetched attester duties") + duties, err := attester.MergeDuties(ctx, attesterDuties) + if err != nil { + s.monitor.BeaconCommitteeSubscriptionCompleted(started, "failed") + return nil, errors.Wrap(err, "failed to merge attester duties") + } + + subscriptionInfo, err := s.calculateSubscriptionInfo(ctx, epoch, accounts, duties) + if err != nil { + s.monitor.BeaconCommitteeSubscriptionCompleted(started, "failed") + return nil, errors.Wrap(err, "failed to calculate subscription duties") + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Calculated subscription info") + + // Update metrics. + subscriptions := 0 + aggregators := 0 + for _, v := range subscriptionInfo { + for _, v2 := range v { + subscriptions++ + if v2.Aggregate { + aggregators++ + } + } + } + s.monitor.BeaconCommitteeSubscribers(subscriptions) + s.monitor.BeaconCommitteeAggregators(aggregators) + + // Submit the subscription information. + go func() { + log.Trace().Msg("Submitting subscription") + subscriptions := make([]*submitter.BeaconCommitteeSubscription, 0, len(duties)) + for slot, slotInfo := range subscriptionInfo { + for committeeIndex, info := range slotInfo { + subscriptions = append(subscriptions, &submitter.BeaconCommitteeSubscription{ + Slot: slot, + CommitteeIndex: committeeIndex, + CommitteeSize: info.CommitteeSize, + ValidatorIndex: info.ValidatorIndex, + ValidatorPubKey: info.ValidatorPubKey, + Aggregate: info.Aggregate, + Signature: info.Signature, + }) + } + } + if err := s.submitter.SubmitBeaconCommitteeSubscriptions(ctx, subscriptions); err != nil { + log.Error().Err(err).Msg("Failed to submit beacon committees") + s.monitor.BeaconCommitteeSubscriptionCompleted(started, "failed") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Submitted subscription request") + s.monitor.BeaconCommitteeSubscriptionCompleted(started, "succeeded") + }() + + // Return the subscription info so the calling function knows the subnets to which we are subscribing. + return subscriptionInfo, nil +} + +// calculateSubscriptionInfo calculates our beacon block attesation subnet requirements given a set of duties. +// It returns a map of slot => committee => subscription information. +func (s *Service) calculateSubscriptionInfo(ctx context.Context, + epoch uint64, + accounts []accountmanager.ValidatingAccount, + duties []*attester.Duty, +) (map[uint64]map[uint64]*beaconcommitteesubscriber.Subscription, error) { + + // Map is slot => committee => info. + subscriptionInfo := make(map[uint64]map[uint64]*beaconcommitteesubscriber.Subscription) + subscriptionInfoMutex := deadlock.RWMutex{} + + // Map is validator ID => account. + accountMap := make(map[uint64]accountmanager.ValidatingAccount, len(accounts)) + for _, account := range accounts { + index, err := account.Index(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain account index for account map") + continue + } + accountMap[index] = account + } + + // Gather aggregators info in parallel. + // Note that it is possible for two validators to be aggregating for the same (slot,committee index) tuple, however + // once we have a validator aggregating for a tuple we ignore subsequent validators with the same tuple. + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for _, duty := range duties { + wg.Add(1) + go func(ctx context.Context, sem *semaphore.Weighted, wg *sync.WaitGroup, duty *attester.Duty) { + defer wg.Done() + for i := range duty.ValidatorIndices() { + wg.Add(1) + go func(ctx context.Context, sem *semaphore.Weighted, wg *sync.WaitGroup, duty *attester.Duty, i int) { + defer wg.Done() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to obtain semaphore") + } + defer sem.Release(1) + subscriptionInfoMutex.RLock() + info, exists := subscriptionInfo[duty.Slot()][duty.CommitteeIndices()[i]] + subscriptionInfoMutex.RUnlock() + if exists && info.Aggregate { + // Already an aggregator for this slot/committee; don't need to go further. + return + } + isAggregator, signature, err := s.attestationAggregator.(attestationaggregator.IsAggregatorProvider). + IsAggregator(ctx, + duty.ValidatorIndices()[i], + duty.CommitteeIndices()[i], + duty.Slot(), + duty.CommitteeSize(duty.CommitteeIndices()[i])) + if err != nil { + log.Error().Err(err).Msg("Failed to calculate if validator is an aggregator") + return + } + account, exists := accountMap[duty.ValidatorIndices()[i]] + if !exists { + log.Error().Uint64("validator_index", duty.ValidatorIndices()[i]).Msg("Failed to obtain validator account") + return + } + subscriptionInfoMutex.Lock() + if _, exists := subscriptionInfo[duty.Slot()]; !exists { + subscriptionInfo[duty.Slot()] = make(map[uint64]*beaconcommitteesubscriber.Subscription) + } + pubKey, err := account.PubKey(ctx) + if err != nil { + log.Error().Uint64("validator_index", duty.ValidatorIndices()[i]).Err(err).Msg("Failed to obtain validator public key") + return + } + subscriptionInfo[duty.Slot()][duty.CommitteeIndices()[i]] = &beaconcommitteesubscriber.Subscription{ + ValidatorIndex: duty.ValidatorIndices()[i], + ValidatorPubKey: pubKey, + CommitteeSize: duty.CommitteeSize(duty.CommitteeIndices()[i]), + Signature: signature, + Aggregate: isAggregator, + } + subscriptionInfoMutex.Unlock() + }(ctx, sem, wg, duty, i) + } + }(ctx, sem, &wg, duty) + } + wg.Wait() + + return subscriptionInfo, nil +} diff --git a/services/beaconnode/mock/service.go b/services/beaconnode/mock/service.go deleted file mode 100644 index 7d2ec04..0000000 --- a/services/beaconnode/mock/service.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright © 2020 Attestant Limited. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package mock - -// BeaconNode is a mock beacon node -type BeaconNode struct { - genesisTimestamp int64 - secondsPerSlot uint64 - slotsPerEpoch uint64 -} - -// New creates a new mock beacon node connection. -func New() (*BeaconNode, error) { - return &BeaconNode{ - genesisTimestamp: 1588291200, - secondsPerSlot: 12, - slotsPerEpoch: 32, - }, nil -} - -// FetchSecondsPerSlot fetches the number of seconds per slot for the attached beacon node. -func (bn *BeaconNode) FetchSecondsPerSlot() uint64 { - return bn.secondsPerSlot -} - -// FetchSlotsPerEpoch fetches the number of slots per epoch for the attached beacon node. -func (bn *BeaconNode) FetchSlotsPerEpoch() uint64 { - return bn.slotsPerEpoch -} - -// CalcTimestampOfSlot calculates the timestamp of the start of the given slot. -func (bn *BeaconNode) CalcTimestampOfSlot(slot uint64) int64 { - return bn.genesisTimestamp + int64(slot*bn.secondsPerSlot) -} - -// CalcTimestampOfEpoch calculates the timestamp of the start of the given epoch. -func (bn *BeaconNode) CalcTimestampOfEpoch(epoch uint64) int64 { - return bn.genesisTimestamp + int64(epoch*bn.secondsPerSlot*bn.slotsPerEpoch) -} diff --git a/services/chaintime/service.go b/services/chaintime/service.go new file mode 100644 index 0000000..82068d3 --- /dev/null +++ b/services/chaintime/service.go @@ -0,0 +1,34 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package chaintime + +import "time" + +// Service provides a number of functions for calculating chain-related times. +type Service interface { + // GenesisTime provides the time of the chain's genesis. + GenesisTime() time.Time + // StartOfSlot provides the time at which a given slot starts. + StartOfSlot(slot uint64) time.Time + // StartOfEpoch provides the time at which a given epoch starts. + StartOfEpoch(epoch uint64) time.Time + // CurrentSlot provides the current slot. + CurrentSlot() uint64 + // CurrentEpoch provides the current epoch. + CurrentEpoch() uint64 + // SlotToEpoch provides the epoch of the given slot. + SlotToEpoch(slot uint64) uint64 + // FirstSlotOfEpoch provides the first slot of the given epoch. + FirstSlotOfEpoch(epoch uint64) uint64 +} diff --git a/services/chaintime/standard/parameters.go b/services/chaintime/standard/parameters.go new file mode 100644 index 0000000..0b5ebcc --- /dev/null +++ b/services/chaintime/standard/parameters.go @@ -0,0 +1,90 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + genesisTimeProvider eth2client.GenesisTimeProvider + slotDurationProvider eth2client.SlotDurationProvider + slotsPerEpochProvider eth2client.SlotsPerEpochProvider +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithGenesisTimeProvider sets the genesis time provider. +func WithGenesisTimeProvider(provider eth2client.GenesisTimeProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.genesisTimeProvider = provider + }) +} + +// WithSlotDurationProvider sets the seconds per slot provider. +func WithSlotDurationProvider(provider eth2client.SlotDurationProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.slotDurationProvider = provider + }) +} + +// WithSlotsPerEpochProvider sets the slots per epoch provider. +func WithSlotsPerEpochProvider(provider eth2client.SlotsPerEpochProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.slotsPerEpochProvider = provider + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.genesisTimeProvider == nil { + return nil, errors.New("no genesis time provider specified") + } + if parameters.slotDurationProvider == nil { + return nil, errors.New("no slot duration provider specified") + } + if parameters.slotsPerEpochProvider == nil { + return nil, errors.New("no slots per epoch provider specified") + } + + return ¶meters, nil +} diff --git a/services/chaintime/standard/service.go b/services/chaintime/standard/service.go new file mode 100644 index 0000000..6f926a7 --- /dev/null +++ b/services/chaintime/standard/service.go @@ -0,0 +1,114 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service provides chain time services. +type Service struct { + genesisTime time.Time + slotDuration time.Duration + slotsPerEpoch uint64 +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new controller. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "chaintime").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + genesisTime, err := parameters.genesisTimeProvider.GenesisTime(ctx) + if err != nil { + return nil, errors.Wrap(nil, "failed to obtain genesis time") + } + log.Trace().Time("genesis_time", genesisTime).Msg("Obtained genesis time") + + slotDuration, err := parameters.slotDurationProvider.SlotDuration(ctx) + if err != nil { + return nil, errors.Wrap(nil, "failed to obtain slot duration") + } + log.Trace().Dur("slot_duration", slotDuration).Msg("Obtained slot duration") + + slotsPerEpoch, err := parameters.slotsPerEpochProvider.SlotsPerEpoch(ctx) + if err != nil { + return nil, errors.Wrap(nil, "failed to obtain slots per epoch") + } + log.Trace().Uint64("slots_per_epoch", slotsPerEpoch).Msg("Obtained slots per epoch") + + s := &Service{ + genesisTime: genesisTime, + slotDuration: slotDuration, + slotsPerEpoch: slotsPerEpoch, + } + + return s, nil +} + +// GenesisTime provides the time of the chain's genesis. +func (s *Service) GenesisTime() time.Time { + return s.genesisTime +} + +// StartOfSlot provides the time at which a given slot starts. +func (s *Service) StartOfSlot(slot uint64) time.Time { + return s.genesisTime.Add(time.Duration(slot) * s.slotDuration) +} + +// StartOfEpoch provides the time at which a given epoch starts. +func (s *Service) StartOfEpoch(epoch uint64) time.Time { + return s.genesisTime.Add(time.Duration(epoch*s.slotsPerEpoch) * s.slotDuration) +} + +// CurrentSlot provides the current slot. +func (s *Service) CurrentSlot() uint64 { + if s.genesisTime.After(time.Now()) { + return 0 + } + return uint64(time.Since(s.genesisTime).Seconds()) / uint64(s.slotDuration.Seconds()) +} + +// CurrentEpoch provides the current epoch. +func (s *Service) CurrentEpoch() uint64 { + if s.genesisTime.After(time.Now()) { + return 0 + } + return uint64(time.Since(s.genesisTime).Seconds()) / (uint64(s.slotDuration.Seconds()) * s.slotsPerEpoch) +} + +// SlotToEpoch provides the epoch of a given slot. +func (s *Service) SlotToEpoch(slot uint64) uint64 { + return slot / s.slotsPerEpoch +} + +// FirstSlotOfEpoch provides the first slot of the given epoch. +func (s *Service) FirstSlotOfEpoch(epoch uint64) uint64 { + return epoch * s.slotsPerEpoch +} diff --git a/services/chaintime/standard/service_test.go b/services/chaintime/standard/service_test.go new file mode 100644 index 0000000..9f04b10 --- /dev/null +++ b/services/chaintime/standard/service_test.go @@ -0,0 +1,174 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard_test + +import ( + "context" + "testing" + "time" + + "github.com/attestantio/vouch/mock" + "github.com/attestantio/vouch/services/chaintime/standard" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + genesisTime := time.Now() + slotDuration := 12 * time.Second + slotsPerEpoch := uint64(32) + mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + mockSlotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + mockSlotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + tests := []struct { + name string + params []standard.Parameter + err string + }{ + { + name: "GenesisTimeProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + }, + err: "problem with parameters: no genesis time provider specified", + }, + { + name: "SlotDurationProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + }, + err: "problem with parameters: no slot duration provider specified", + }, + { + name: "SlotsPerEpochProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + }, + err: "problem with parameters: no slots per epoch provider specified", + }, + { + name: "Good", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := standard.New(context.Background(), test.params...) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestGenesisTime(t *testing.T) { + genesisTime := time.Now() + slotDuration := 12 * time.Second + slotsPerEpoch := uint64(32) + mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + mockSlotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + mockSlotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + s, err := standard.New(context.Background(), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + ) + require.NoError(t, err) + + require.Equal(t, genesisTime, s.GenesisTime()) +} + +func TestStartOfSlot(t *testing.T) { + genesisTime := time.Now() + slotDuration := 12 * time.Second + slotsPerEpoch := uint64(32) + mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + mockSlotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + mockSlotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + s, err := standard.New(context.Background(), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + ) + require.NoError(t, err) + + require.Equal(t, genesisTime, s.StartOfSlot(0)) + require.Equal(t, genesisTime.Add(1000*slotDuration), s.StartOfSlot(1000)) +} + +func TestStartOfEpoch(t *testing.T) { + genesisTime := time.Now() + slotDuration := 12 * time.Second + slotsPerEpoch := uint64(32) + mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + mockSlotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + mockSlotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + s, err := standard.New(context.Background(), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + ) + require.NoError(t, err) + + require.Equal(t, genesisTime, s.StartOfEpoch(0)) + require.Equal(t, genesisTime.Add(time.Duration(1000*slotsPerEpoch)*slotDuration), s.StartOfEpoch(1000)) +} + +func TestCurrentSlot(t *testing.T) { + slotDuration := 12 * time.Second + slotsPerEpoch := uint64(32) + genesisTime := time.Now().Add(-5 * slotDuration) + mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + mockSlotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + mockSlotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + s, err := standard.New(context.Background(), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + ) + require.NoError(t, err) + + require.Equal(t, uint64(5), s.CurrentSlot()) +} + +func TestCurrentEpoch(t *testing.T) { + slotDuration := 12 * time.Second + slotsPerEpoch := uint64(32) + genesisTime := time.Now().Add(time.Duration(int64(-2)*int64(slotsPerEpoch)) * slotDuration) + mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + mockSlotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + mockSlotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + s, err := standard.New(context.Background(), + standard.WithGenesisTimeProvider(mockGenesisTimeProvider), + standard.WithSlotDurationProvider(mockSlotDurationProvider), + standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + ) + require.NoError(t, err) + + require.Equal(t, uint64(2), s.CurrentEpoch()) +} diff --git a/services/controller/standard/attester.go b/services/controller/standard/attester.go new file mode 100644 index 0000000..ce1c1a5 --- /dev/null +++ b/services/controller/standard/attester.go @@ -0,0 +1,158 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "fmt" + + eth2client "github.com/attestantio/go-eth2-client" + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/attestationaggregator" + "github.com/attestantio/vouch/services/attester" +) + +// createAttesterJobs creates attestation jobs for the given epoch provided accounts. +func (s *Service) createAttesterJobs(ctx context.Context, + epoch uint64, + accounts []accountmanager.ValidatingAccount, + firstRun bool) { + log.Trace().Msg("Creating attester jobs") + + idProviders := make([]eth2client.ValidatorIDProvider, len(accounts)) + for i, account := range accounts { + idProviders[i] = account.(eth2client.ValidatorIDProvider) + } + resp, err := s.attesterDutiesProvider.AttesterDuties(ctx, epoch, idProviders) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain attester duties") + return + } + + // Filter bad responses. + filteredDuties := make([]*api.AttesterDuty, 0, len(resp)) + firstSlot := epoch * s.slotsPerEpoch + lastSlot := (epoch+1)*s.slotsPerEpoch - 1 + for _, duty := range resp { + if duty.Slot < firstSlot || duty.Slot > lastSlot { + log.Warn().Uint64("epoch", epoch).Uint64("duty_slot", duty.Slot).Msg("Attester duty has invalid slot for requested epoch; ignoring") + continue + } + filteredDuties = append(filteredDuties, duty) + } + + duties, err := attester.MergeDuties(ctx, filteredDuties) + if err != nil { + log.Error().Err(err).Msg("Failed to merge attester duties") + return + } + + for _, duty := range duties { + log. + Trace(). + Uint64("slot", duty.Slot()). + Uints64("committee_indices", duty.CommitteeIndices()). + Uints64("validator_indices", duty.ValidatorCommitteeIndices()). + Msg("Received attester duty") + } + + currentSlot := s.chainTimeService.CurrentSlot() + for _, duty := range duties { + // Do not schedule attestations for past slots (or the current slot if we've just started). + if duty.Slot() < currentSlot { + log.Debug().Uint64("attestation_slot", duty.Slot()).Uint64("current_slot", currentSlot).Msg("Attestation in the past; not scheduling") + continue + } + if firstRun && duty.Slot() == currentSlot { + log.Debug().Uint64("attestation_slot", duty.Slot()).Uint64("current_slot", currentSlot).Msg("Attestation in the current slot and this is our first run; not scheduling") + continue + } + if err := s.scheduler.ScheduleJob(ctx, + fmt.Sprintf("Beacon block attestations for slot %d", duty.Slot()), + s.chainTimeService.StartOfSlot(duty.Slot()).Add(s.slotDuration/3), + s.AttestAndScheduleAggregate, + duty, + ); err != nil { + // Don't return here; we want to try to set up as many attester jobs as possible. + log.Error().Err(err).Msg("Failed to set attester job") + } + } +} + +// AttestAndScheduleAggregate attests, then schedules aggregation jobs as required. +func (s *Service) AttestAndScheduleAggregate(ctx context.Context, data interface{}) { + duty, ok := data.(*attester.Duty) + if !ok { + log.Error().Msg("Passed invalid data") + return + } + + attestations, err := s.attester.Attest(ctx, duty) + if err != nil { + log.Warn().Err(err).Msg("Failed to attest") + } + + if len(attestations) == 0 { + log.Debug().Msg("No attestations; nothing to aggregate") + return + } + + epoch := attestations[0].Data.Slot / s.slotsPerEpoch + s.subscriptionInfosMutex.Lock() + subscriptionInfoMap, exists := s.subscriptionInfos[epoch] + s.subscriptionInfosMutex.Unlock() + if !exists { + log.Warn().Msg("No subscription info for this epoch; cannot aggregate") + return + } + + for _, attestation := range attestations { + slotInfoMap, exists := subscriptionInfoMap[attestation.Data.Slot] + if !exists { + log.Debug().Uint64("attestation_slot", attestation.Data.Slot).Msg("No slot info; cannot aggregate") + continue + } + // Do not schedule aggregations for past slots. + if attestation.Data.Slot < s.chainTimeService.CurrentSlot() { + log.Debug().Uint64("aggregation_slot", attestation.Data.Slot).Uint64("current_slot", s.chainTimeService.CurrentSlot()).Msg("Aggregation in the past; not scheduling") + continue + } + info, exists := slotInfoMap[attestation.Data.Index] + if !exists { + log.Debug().Uint64("attestation_slot", attestation.Data.Slot).Uint64("committee_index", attestation.Data.Index).Msg("No committee info; cannot aggregate") + } + if info.Aggregate { + aggregatorDuty, err := attestationaggregator.NewDuty(ctx, info.ValidatorIndex, info.ValidatorPubKey, attestation, info.Signature) + if err != nil { + // Don't return here; we want to try to set up as many aggregator jobs as possible. + log.Error().Err(err).Msg("Failed to create beacon block attestation aggregation duty") + continue + } + if err := s.scheduler.ScheduleJob(ctx, + fmt.Sprintf("Beacon block attestation aggregation for slot %d committee %d", attestation.Data.Slot, attestation.Data.Index), + s.chainTimeService.StartOfSlot(attestation.Data.Slot).Add(s.slotDuration*2/3), + s.attestationAggregator.Aggregate, + aggregatorDuty, + ); err != nil { + // Don't return here; we want to try to set up as many aggregator jobs as possible. + log.Error().Err(err).Msg("Failed to schedule beacon block attestation aggregation job") + continue + } + // We are set up as an aggregator for this slot and committee. It is possible that another validator has also been + // assigned as an aggregator, but we're already carrying out the task so do not need to go any further. + return + } + } +} diff --git a/services/controller/standard/parameters.go b/services/controller/standard/parameters.go new file mode 100644 index 0000000..5596233 --- /dev/null +++ b/services/controller/standard/parameters.go @@ -0,0 +1,205 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/attestationaggregator" + "github.com/attestantio/vouch/services/attester" + "github.com/attestantio/vouch/services/beaconblockproposer" + "github.com/attestantio/vouch/services/beaconcommitteesubscriber" + "github.com/attestantio/vouch/services/chaintime" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/scheduler" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.ControllerMonitor + slotDurationProvider eth2client.SlotDurationProvider + slotsPerEpochProvider eth2client.SlotsPerEpochProvider + chainTimeService chaintime.Service + proposerDutiesProvider eth2client.ProposerDutiesProvider + attesterDutiesProvider eth2client.AttesterDutiesProvider + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + scheduler scheduler.Service + beaconChainHeadUpdatedSource eth2client.BeaconChainHeadUpdatedSource + attester attester.Service + beaconBlockProposer beaconblockproposer.Service + attestationAggregator attestationaggregator.Service + beaconCommitteeSubscriber beaconcommitteesubscriber.Service +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithMonitor sets the monitor for the module. +func WithMonitor(monitor metrics.ControllerMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithSlotDurationProvider sets the slot duration provider. +func WithSlotDurationProvider(provider eth2client.SlotDurationProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.slotDurationProvider = provider + }) +} + +// WithSlotsPerEpochProvider sets the slots per epoch provider. +func WithSlotsPerEpochProvider(provider eth2client.SlotsPerEpochProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.slotsPerEpochProvider = provider + }) +} + +// WithChainTimeService sets the chain time service. +func WithChainTimeService(service chaintime.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.chainTimeService = service + }) +} + +// WithProposerDutiesProvider sets the proposer duties provider. +func WithProposerDutiesProvider(provider eth2client.ProposerDutiesProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.proposerDutiesProvider = provider + }) +} + +// WithAttesterDutiesProvider sets the attester duties provider. +func WithAttesterDutiesProvider(provider eth2client.AttesterDutiesProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.attesterDutiesProvider = provider + }) +} + +// WithBeaconChainHeadUpdatedSource sets the source for an OnBeaconChainHeadUpdated request +func WithBeaconChainHeadUpdatedSource(source eth2client.BeaconChainHeadUpdatedSource) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconChainHeadUpdatedSource = source + }) +} + +// WithValidatingAccountsProvider sets the validating accounts provider. +func WithValidatingAccountsProvider(provider accountmanager.ValidatingAccountsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatingAccountsProvider = provider + }) +} + +// WithScheduler sets the scheduler. +func WithScheduler(scheduler scheduler.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.scheduler = scheduler + }) +} + +// WithAttester sets the attester. +func WithAttester(attester attester.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.attester = attester + }) +} + +// WithBeaconBlockProposer sets the beacon block propser. +func WithBeaconBlockProposer(proposer beaconblockproposer.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockProposer = proposer + }) +} + +// WithAttestationAggregator sets the attestation aggregator. +func WithAttestationAggregator(aggregator attestationaggregator.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.attestationAggregator = aggregator + }) +} + +// WithBeaconCommitteeSubscriber sets the beacon committee subscriber. +func WithBeaconCommitteeSubscriber(subscriber beaconcommitteesubscriber.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconCommitteeSubscriber = subscriber + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.slotDurationProvider == nil { + return nil, errors.New("no slot duration provider specified") + } + if parameters.slotsPerEpochProvider == nil { + return nil, errors.New("no slots per epoch provider specified") + } + if parameters.chainTimeService == nil { + return nil, errors.New("no chain time service specified") + } + if parameters.proposerDutiesProvider == nil { + return nil, errors.New("no proposer duties provider specified") + } + if parameters.attesterDutiesProvider == nil { + return nil, errors.New("no attester duties provider specified") + } + if parameters.beaconChainHeadUpdatedSource == nil { + return nil, errors.New("no beacon chain head updated source specified") + } + if parameters.scheduler == nil { + return nil, errors.New("no scheduler service specified") + } + if parameters.attester == nil { + return nil, errors.New("no attester specified") + } + if parameters.beaconBlockProposer == nil { + return nil, errors.New("no beacon block proposer specified") + } + if parameters.attestationAggregator == nil { + return nil, errors.New("no attestation aggregator specified") + } + if parameters.beaconCommitteeSubscriber == nil { + return nil, errors.New("no beacon committee subscriber specified") + } + + return ¶meters, nil +} diff --git a/services/controller/standard/proposer.go b/services/controller/standard/proposer.go new file mode 100644 index 0000000..ba7586f --- /dev/null +++ b/services/controller/standard/proposer.go @@ -0,0 +1,81 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "fmt" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/beaconblockproposer" +) + +// createProposerJobs creates proposal jobs for the given epoch. +func (s *Service) createProposerJobs(ctx context.Context, + epoch uint64, + accounts []accountmanager.ValidatingAccount, + firstRun bool) { + validatorIDProviders := make([]eth2client.ValidatorIDProvider, len(accounts)) + for i, account := range accounts { + validatorIDProviders[i] = account + } + + resp, err := s.proposerDutiesProvider.ProposerDuties(ctx, epoch, validatorIDProviders) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain proposer duties") + return + } + + // Filter bad responses. + duties := make([]*beaconblockproposer.Duty, 0, len(resp)) + firstSlot := epoch * s.slotsPerEpoch + lastSlot := (epoch+1)*s.slotsPerEpoch - 1 + for _, respDuty := range resp { + if respDuty.Slot < firstSlot || respDuty.Slot > lastSlot { + log.Warn().Uint64("epoch", epoch).Uint64("duty_slot", respDuty.Slot).Msg("Proposer duty has invalid slot for requested epoch; ignoring") + continue + } + duty, err := beaconblockproposer.NewDuty(ctx, respDuty.Slot, respDuty.ValidatorIndex) + if err != nil { + log.Error().Err(err).Msg("Failed to create proposer duty") + continue + } + duties = append(duties, duty) + } + + currentSlot := s.chainTimeService.CurrentSlot() + for _, duty := range duties { + // Do not schedule proposals for past slots (or the current slot if we've just started). + if duty.Slot() < currentSlot || firstRun && duty.Slot() == currentSlot { + log.Debug().Uint64("proposal_slot", duty.Slot()).Uint64("current_slot", currentSlot).Msg("Proposal in the past; not scheduling") + continue + } + go func(duty *beaconblockproposer.Duty) { + if err := s.beaconBlockProposer.Prepare(ctx, duty); err != nil { + log.Error().Uint64("proposal_slot", duty.Slot()).Err(err).Msg("Failed to prepare proposal") + return + } + if err := s.scheduler.ScheduleJob(ctx, + fmt.Sprintf("Beacon block proposal for slot %d", duty.Slot()), + s.chainTimeService.StartOfSlot(duty.Slot()), + s.beaconBlockProposer.Propose, + duty, + ); err != nil { + // Don't return here; we want to try to set up as many proposer jobs as possible. + log.Error().Err(err).Msg("Failed to set proposer job") + } + }(duty) + } +} diff --git a/services/controller/standard/service.go b/services/controller/standard/service.go new file mode 100644 index 0000000..8e36171 --- /dev/null +++ b/services/controller/standard/service.go @@ -0,0 +1,273 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "fmt" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/attestationaggregator" + "github.com/attestantio/vouch/services/attester" + "github.com/attestantio/vouch/services/beaconblockproposer" + "github.com/attestantio/vouch/services/beaconcommitteesubscriber" + "github.com/attestantio/vouch/services/chaintime" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/scheduler" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is the co-ordination system for vouch. +// It runs purely against clock events, setting up jobs for the validator's processes of block proposal, attestation +// creation and attestation aggregation. +type Service struct { + monitor metrics.ControllerMonitor + slotDuration time.Duration + slotsPerEpoch uint64 + chainTimeService chaintime.Service + proposerDutiesProvider eth2client.ProposerDutiesProvider + attesterDutiesProvider eth2client.AttesterDutiesProvider + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + scheduler scheduler.Service + attester attester.Service + beaconBlockProposer beaconblockproposer.Service + attestationAggregator attestationaggregator.Service + beaconCommitteeSubscriber beaconcommitteesubscriber.Service + activeAccounts int + // Epoch => slot => committee => subscription info + subscriptionInfos map[uint64]map[uint64]map[uint64]*beaconcommitteesubscriber.Subscription + subscriptionInfosMutex sync.Mutex +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new controller. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "controller").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + slotDuration, err := parameters.slotDurationProvider.SlotDuration(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain slot duration") + } + + slotsPerEpoch, err := parameters.slotsPerEpochProvider.SlotsPerEpoch(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain slots per epoch") + } + + s := &Service{ + monitor: parameters.monitor, + slotDuration: slotDuration, + slotsPerEpoch: slotsPerEpoch, + chainTimeService: parameters.chainTimeService, + proposerDutiesProvider: parameters.proposerDutiesProvider, + attesterDutiesProvider: parameters.attesterDutiesProvider, + validatingAccountsProvider: parameters.validatingAccountsProvider, + scheduler: parameters.scheduler, + attester: parameters.attester, + beaconBlockProposer: parameters.beaconBlockProposer, + attestationAggregator: parameters.attestationAggregator, + beaconCommitteeSubscriber: parameters.beaconCommitteeSubscriber, + subscriptionInfos: make(map[uint64]map[uint64]map[uint64]*beaconcommitteesubscriber.Subscription), + } + + log.Trace().Msg("Adding beacon chain head updated handler") + if err := parameters.beaconChainHeadUpdatedSource.AddOnBeaconChainHeadUpdatedHandler(ctx, s); err != nil { + return nil, errors.Wrap(err, "failed to add beacon chain head updated handler") + } + + // Subscriptions are usually updated one epoch in advance, but as we're + // just starting we don't have subscriptions (or subscription information) + // for this or the next epoch; fetch them now. + go func() { + log.Trace().Msg("Fetching initial validator accounts") + accounts, err := s.validatingAccountsProvider.Accounts(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain accounts for initial validators") + return + } + log.Info().Int("accounts", len(accounts)).Msg("Initial validating accounts") + if len(accounts) == 0 { + log.Debug().Msg("No active validating accounts") + return + } + currentEpoch := s.chainTimeService.CurrentEpoch() + subscriptionInfo, err := s.beaconCommitteeSubscriber.Subscribe(ctx, currentEpoch, accounts) + if err != nil { + log.Warn().Err(err).Msg("Failed to fetch initial beacon committees for current epoch") + return + } + s.subscriptionInfosMutex.Lock() + s.subscriptionInfos[currentEpoch] = subscriptionInfo + s.subscriptionInfosMutex.Unlock() + subscriptionInfo, err = s.beaconCommitteeSubscriber.Subscribe(ctx, currentEpoch+1, accounts) + if err != nil { + log.Warn().Err(err).Msg("Failed to fetch initial beacon committees for next epoch") + return + } + s.subscriptionInfosMutex.Lock() + s.subscriptionInfos[currentEpoch+1] = subscriptionInfo + s.subscriptionInfosMutex.Unlock() + }() + + if err := s.startTickers(ctx); err != nil { + return nil, errors.Wrap(err, "failed to start controller tickers") + } + + return s, nil +} + +// startTickers starts the various tickers for the controller's operations. +func (s *Service) startTickers(ctx context.Context) error { + genesisTime := s.chainTimeService.GenesisTime() + now := time.Now() + if now.Before(genesisTime) { + // Wait for genesis. + log.Info().Str("genesis", fmt.Sprintf("%v", genesisTime)).Msg("Waiting for genesis") + time.Sleep(time.Until(genesisTime)) + // Give it another half second to let the beacon node be ready. + time.Sleep(500 * time.Millisecond) + } + + // Start epoch ticker. + log.Trace().Msg("Starting epoch ticker") + if err := s.startEpochTicker(ctx); err != nil { + return errors.Wrap(err, "failed to start epoch ticker") + } + + return nil +} + +type epochTickerData struct { + mutex sync.Mutex + latestEpochRan int64 +} + +// startEpochTicker starts a ticker that ticks at the beginning of each epoch. +func (s *Service) startEpochTicker(ctx context.Context) error { + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + // Schedule for the beginning of the next epoch. + return s.chainTimeService.StartOfEpoch(s.chainTimeService.CurrentEpoch() + 1), nil + } + data := &epochTickerData{ + latestEpochRan: -1, + } + if err := s.scheduler.SchedulePeriodicJob(ctx, + "Epoch ticker", + runtimeFunc, + data, + s.epochTicker, + data, + ); err != nil { + return errors.Wrap(err, "Failed to schedule epoch ticker") + } + + // Kick off the job immediately to fetch any duties for the current epoch. + if err := s.scheduler.RunJob(ctx, "Epoch ticker"); err != nil { + return errors.Wrap(err, "Failed to run epoch ticker") + } + + return nil +} + +// epochTicker sets up the jobs for proposal, attestation and aggregation. +func (s *Service) epochTicker(ctx context.Context, data interface{}) { + // Ensure we don't run for the same epoch twice. + epochTickerData := data.(*epochTickerData) + currentEpoch := s.chainTimeService.CurrentEpoch() + log.Trace().Uint64("epoch", currentEpoch).Msg("Starting per-epoch duties") + epochTickerData.mutex.Lock() + firstRun := epochTickerData.latestEpochRan == -1 + if epochTickerData.latestEpochRan >= int64(currentEpoch) { + log.Trace().Uint64("epoch", currentEpoch).Msg("Already ran for this epoch; skipping") + epochTickerData.mutex.Unlock() + return + } + epochTickerData.latestEpochRan = int64(currentEpoch) + epochTickerData.mutex.Unlock() + s.monitor.NewEpoch() + + // Wait for half a second for the beacon node to update. + time.Sleep(500 * time.Millisecond) + + log.Trace().Msg("Updating validating accounts") + err := s.validatingAccountsProvider.(accountmanager.AccountsUpdater).UpdateAccountsState(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to update account state") + return + } + accounts, err := s.validatingAccountsProvider.Accounts(ctx) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain accounts") + return + } + if len(accounts) != s.activeAccounts { + log.Info().Int("old_accounts", s.activeAccounts).Int("accounts", len(accounts)).Msg("Change in number of validating accounts") + s.activeAccounts = len(accounts) + } + if len(accounts) == 0 { + // Expect at least one account. + log.Warn().Msg("No active validating accounts; not validating") + return + } + + // Create the jobs for our individual functions. + go s.createProposerJobs(ctx, currentEpoch, accounts, firstRun) + go s.createAttesterJobs(ctx, currentEpoch, accounts, firstRun) + go func() { + // Update beacon committee subscriptions for the next epoch. + subscriptionInfo, err := s.beaconCommitteeSubscriber.Subscribe(ctx, currentEpoch+1, accounts) + if err != nil { + log.Warn().Err(err).Msg("Failed to subscribe to beacom committees") + return + } + s.subscriptionInfosMutex.Lock() + s.subscriptionInfos[currentEpoch+1] = subscriptionInfo + s.subscriptionInfosMutex.Unlock() + }() +} + +// OnBeaconChainHeadUpdated runs attestations for a slot immediately, if the update is for the current slot. +func (s *Service) OnBeaconChainHeadUpdated(ctx context.Context, slot uint64, stateRoot []byte, bodyRoot []byte, epochTransitioni bool) { + if slot != s.chainTimeService.CurrentSlot() { + return + } + s.monitor.BlockDelay(time.Since(s.chainTimeService.StartOfSlot(slot))) + + jobName := fmt.Sprintf("Beacon block attestations for slot %d", slot) + if s.scheduler.JobExists(ctx, jobName) { + log.Trace().Uint64("slot", slot).Msg("Kicking off attestations for slot early due to receiving relevant block") + if err := s.scheduler.RunJobIfExists(ctx, jobName); err != nil { + log.Error().Str("job", jobName).Err(err).Msg("Failed to run attester job") + } + } + + // Remove old subscriptions if present. + delete(s.subscriptionInfos, s.chainTimeService.SlotToEpoch(slot)-2) +} diff --git a/services/graffitiprovider/dynamic/parameters.go b/services/graffitiprovider/dynamic/parameters.go new file mode 100644 index 0000000..586efd5 --- /dev/null +++ b/services/graffitiprovider/dynamic/parameters.go @@ -0,0 +1,80 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dynamic + +import ( + "errors" + + "github.com/rs/zerolog" + "github.com/wealdtech/go-majordomo" +) + +type parameters struct { + logLevel zerolog.Level + location string + majordomo majordomo.Service +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithLocation sets the location from which to fetch graffiti. +func WithLocation(location string) Parameter { + return parameterFunc(func(p *parameters) { + p.location = location + }) +} + +// WithMajordomo sets majordomo for the module. +func WithMajordomo(majordomo majordomo.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.majordomo = majordomo + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.majordomo == nil { + return nil, errors.New("no majordomo specified") + } + if parameters.location == "" { + return nil, errors.New("no location specified") + } + + return ¶meters, nil +} diff --git a/services/graffitiprovider/dynamic/service.go b/services/graffitiprovider/dynamic/service.go new file mode 100644 index 0000000..14f511b --- /dev/null +++ b/services/graffitiprovider/dynamic/service.go @@ -0,0 +1,99 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dynamic + +import ( + "context" + "fmt" + "math/rand" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "github.com/wealdtech/go-majordomo" +) + +// Service is a graffiti provider service. +type Service struct { + location string + majordomo majordomo.Service +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new graffiti provider service. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "graffitiprovider").Str("impl", "dynamic").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + // Random seed for line selection in multi-line graaffiti input. + rand.Seed(time.Now().UnixNano()) + + s := &Service{ + location: parameters.location, + majordomo: parameters.majordomo, + } + + return s, nil +} + +// Graffiti provides graffiti. +func (s *Service) Graffiti(ctx context.Context, slot uint64, validatorIndex uint64) ([]byte, error) { + // Replace location parameters with values. + location := strings.ReplaceAll(s.location, "{{SLOT}}", fmt.Sprintf("%d", slot)) + location = strings.ReplaceAll(location, "{{VALIDATORINDEX}}", fmt.Sprintf("%d", validatorIndex)) + log.Trace().Str("location", location).Msg("Resolved graffiti location") + + // Fetch data from location. + locationData, err := s.majordomo.Fetch(ctx, location) + if err != nil { + log.Warn().Err(err).Msg("Failed to fetch graffiti") + return nil, err + } + + // Need to remove blank lines and handle both DOS style (\r\n) and Unix style (\n) newlines. + graffitiLines := strings.Split( + strings.TrimSpace( + strings.ReplaceAll(strings.ReplaceAll(string(locationData), "\r\n", "\n"), "\n\n", "\n"), + ), + "\n") + graffitiEntries := len(graffitiLines) + if graffitiEntries == 0 { + log.Debug().Msg("No graffiti found") + return []byte{}, nil + } + + // Pick a single line. If multiple lines are available choose one at random. + // #nosec G404 + graffitiIdx := rand.Intn(graffitiEntries) + graffiti := graffitiLines[graffitiIdx] + + // Replace graffiti parameters with values. + graffiti = strings.ReplaceAll(graffiti, "{{SLOT}}", fmt.Sprintf("%d", slot)) + graffiti = strings.ReplaceAll(graffiti, "{{VALIDATORINDEX}}", fmt.Sprintf("%d", validatorIndex)) + + log.Trace().Str("graffiti", graffiti).Msg("Resolved graffiti") + return []byte(graffiti), nil +} diff --git a/services/graffitiprovider/dynamic/service_test.go b/services/graffitiprovider/dynamic/service_test.go new file mode 100644 index 0000000..23eb29d --- /dev/null +++ b/services/graffitiprovider/dynamic/service_test.go @@ -0,0 +1,310 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dynamic_test + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/attestantio/vouch/services/graffitiprovider/dynamic" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "github.com/wealdtech/go-majordomo" + directconfidant "github.com/wealdtech/go-majordomo/confidants/direct" + fileconfidant "github.com/wealdtech/go-majordomo/confidants/file" + standardmajordomo "github.com/wealdtech/go-majordomo/standard" +) + +func TestService(t *testing.T) { + ctx := context.Background() + majordomoSvc, err := standardmajordomo.New(ctx) + require.NoError(t, err) + directConfidant, err := directconfidant.New(ctx) + require.NoError(t, err) + err = majordomoSvc.RegisterConfidant(ctx, directConfidant) + require.NoError(t, err) + + tests := []struct { + name string + majordomo majordomo.Service + location string + err string + }{ + { + name: "MajordomoMissing", + location: "direct://static", + err: "problem with parameters: no majordomo specified", + }, + { + name: "LocationMissing", + majordomo: majordomoSvc, + err: "problem with parameters: no location specified", + }, + { + name: "Good", + majordomo: majordomoSvc, + location: "direct://static", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := dynamic.New(ctx, + dynamic.WithLogLevel(zerolog.Disabled), + dynamic.WithMajordomo(test.majordomo), + dynamic.WithLocation(test.location)) + + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestReplacement(t *testing.T) { + ctx := context.Background() + majordomoSvc, err := standardmajordomo.New(ctx) + require.NoError(t, err) + directConfidant, err := directconfidant.New(ctx) + require.NoError(t, err) + err = majordomoSvc.RegisterConfidant(ctx, directConfidant) + require.NoError(t, err) + + tests := []struct { + name string + location string + expectedGraffitis []string + }{ + { + name: "Static", + location: "direct:///test", + expectedGraffitis: []string{ + "test", + }, + }, + { + name: "SlotVariable", + location: "direct:///{{SLOT}}", + expectedGraffitis: []string{ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", + }, + }, + { + name: "DoubleSlotVariable", + location: "direct:///{{SLOT}} and {{SLOT}}", + expectedGraffitis: []string{ + "0 and 0", "1 and 1", "2 and 2", "3 and 3", "4 and 4", "5 and 5", "6 and 6", "7 and 7", "8 and 8", + "9 and 9", "10 and 10", "11 and 11", "12 and 12", "13 and 13", "14 and 14", "15 and 15", + }, + }, + { + name: "ValidatorIndexVariable", + location: "direct:///{{VALIDATORINDEX}}", + expectedGraffitis: []string{ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", + }, + }, + { + name: "DoubleValidatorIndexVariable", + location: "direct:///{{VALIDATORINDEX}} and {{VALIDATORINDEX}}", + expectedGraffitis: []string{ + "0 and 0", "1 and 1", "2 and 2", "3 and 3", "4 and 4", "5 and 5", "6 and 6", "7 and 7", "8 and 8", + "9 and 9", "10 and 10", "11 and 11", "12 and 12", "13 and 13", "14 and 14", "15 and 15", + }, + }, + { + name: "ValidatorIndexAndSlotVariables", + location: "direct:///{{VALIDATORINDEX}} and {{SLOT}}", + expectedGraffitis: []string{ + "0 and 0", "0 and 1", "0 and 2", "0 and 3", "0 and 4", "0 and 5", "0 and 6", "0 and 7", "0 and 8", + "0 and 9", "0 and 10", "0 and 11", "0 and 12", "0 and 13", "0 and 14", "0 and 15", + "1 and 0", "1 and 1", "1 and 2", "1 and 3", "1 and 4", "1 and 5", "1 and 6", "1 and 7", "1 and 8", + "1 and 9", "1 and 10", "1 and 11", "1 and 12", "1 and 13", "1 and 14", "1 and 15", + "2 and 0", "2 and 1", "2 and 2", "2 and 3", "2 and 4", "2 and 5", "2 and 6", "2 and 7", "2 and 8", + "2 and 9", "2 and 10", "2 and 11", "2 and 12", "2 and 13", "2 and 14", "2 and 15", + "3 and 0", "3 and 1", "3 and 2", "3 and 3", "3 and 4", "3 and 5", "3 and 6", "3 and 7", "3 and 8", + "3 and 9", "3 and 10", "3 and 11", "3 and 12", "3 and 13", "3 and 14", "3 and 15", + "4 and 0", "4 and 1", "4 and 2", "4 and 3", "4 and 4", "4 and 5", "4 and 6", "4 and 7", "4 and 8", + "4 and 9", "4 and 10", "4 and 11", "4 and 12", "4 and 13", "4 and 14", "4 and 15", + "5 and 0", "5 and 1", "5 and 2", "5 and 3", "5 and 4", "5 and 5", "5 and 6", "5 and 7", "5 and 8", + "5 and 9", "5 and 10", "5 and 11", "5 and 12", "5 and 13", "5 and 14", "5 and 15", + "6 and 0", "6 and 1", "6 and 2", "6 and 3", "6 and 4", "6 and 5", "6 and 6", "6 and 7", "6 and 8", + "6 and 9", "6 and 10", "6 and 11", "6 and 12", "6 and 13", "6 and 14", "6 and 15", + "7 and 0", "7 and 1", "7 and 2", "7 and 3", "7 and 4", "7 and 5", "7 and 6", "7 and 7", "7 and 8", + "7 and 9", "7 and 10", "7 and 11", "7 and 12", "7 and 13", "7 and 14", "7 and 15", + "8 and 0", "8 and 1", "8 and 2", "8 and 3", "8 and 4", "8 and 5", "8 and 6", "8 and 7", "8 and 8", + "8 and 9", "8 and 10", "8 and 11", "8 and 12", "8 and 13", "8 and 14", "8 and 15", + "9 and 0", "9 and 1", "9 and 2", "9 and 3", "9 and 4", "9 and 5", "9 and 6", "9 and 7", "9 and 8", + "9 and 9", "9 and 10", "9 and 11", "9 and 12", "9 and 13", "9 and 14", "9 and 15", + "10 and 0", "10 and 1", "10 and 2", "10 and 3", "10 and 4", "10 and 5", "10 and 6", "10 and 7", "10 and 8", + "10 and 9", "10 and 10", "10 and 11", "10 and 12", "10 and 13", "10 and 14", "10 and 15", + "11 and 0", "11 and 1", "11 and 2", "11 and 3", "11 and 4", "11 and 5", "11 and 6", "11 and 7", "11 and 8", + "11 and 9", "11 and 10", "11 and 11", "11 and 12", "11 and 13", "11 and 14", "11 and 15", + "12 and 0", "12 and 1", "12 and 2", "12 and 3", "12 and 4", "12 and 5", "12 and 6", "12 and 7", "12 and 8", + "12 and 9", "12 and 10", "12 and 11", "12 and 12", "12 and 13", "12 and 14", "12 and 15", + "13 and 0", "13 and 1", "13 and 2", "13 and 3", "13 and 4", "13 and 5", "13 and 6", "13 and 7", "13 and 8", + "13 and 9", "13 and 10", "13 and 11", "13 and 12", "13 and 13", "13 and 14", "13 and 15", + "14 and 0", "14 and 1", "14 and 2", "14 and 3", "14 and 4", "14 and 5", "14 and 6", "14 and 7", "14 and 8", + "14 and 9", "14 and 10", "14 and 11", "14 and 12", "14 and 13", "14 and 14", "14 and 15", + "15 and 0", "15 and 1", "15 and 2", "15 and 3", "15 and 4", "15 and 5", "15 and 6", "15 and 7", "15 and 8", + "15 and 9", "15 and 10", "15 and 11", "15 and 12", "15 and 13", "15 and 14", "15 and 15", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expectedGraffitis := make(map[string]bool) + for _, expectedGraffiti := range test.expectedGraffitis { + expectedGraffitis[expectedGraffiti] = true + } + svc, err := dynamic.New(ctx, + dynamic.WithLogLevel(zerolog.Disabled), + dynamic.WithMajordomo(majordomoSvc), + dynamic.WithLocation(test.location)) + require.NoError(t, err) + for validatorIndex := uint64(0); validatorIndex < 16; validatorIndex++ { + for slot := uint64(0); slot < 16; slot++ { + graffiti, err := svc.Graffiti(ctx, slot, validatorIndex) + require.NoError(t, err) + delete(expectedGraffitis, string(graffiti)) + } + } + require.Empty(t, expectedGraffitis) + }) + } +} + +func TestMultiline(t *testing.T) { + ctx := context.Background() + majordomoSvc, err := standardmajordomo.New(ctx) + require.NoError(t, err) + fileConfidant, err := fileconfidant.New(ctx) + require.NoError(t, err) + err = majordomoSvc.RegisterConfidant(ctx, fileConfidant) + require.NoError(t, err) + + tmpDir, err := ioutil.TempDir(os.TempDir(), "TestMultiline") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + tests := []struct { + name string + location string + content string + expectedGraffitis map[string]bool + }{ + { + name: "NoLines", + location: fmt.Sprintf("file://%s/NoLines", tmpDir), + content: "", + expectedGraffitis: map[string]bool{"": true}, + }, + { + name: "SingleLine", + location: fmt.Sprintf("file://%s/SingleLine", tmpDir), + content: "Single line\r\n", + expectedGraffitis: map[string]bool{ + "Single line": true, + }, + }, + { + name: "MultiLine", + location: fmt.Sprintf("file://%s/MultiLine", tmpDir), + content: "Line 1\r\nLine 2", + expectedGraffitis: map[string]bool{ + "Line 1": true, + "Line 2": true, + }, + }, + { + name: "Blanks", + location: fmt.Sprintf("file://%s/Blanks", tmpDir), + content: "\n\r\n\r\nThe line\r\n\n\n\r\r\n", + expectedGraffitis: map[string]bool{ + "The line": true, + }, + }, + { + name: "Template", + location: fmt.Sprintf("file://%s/Template", tmpDir), + content: "Graffiti for validator {{VALIDATORINDEX}}\nGraffiti for slot {{SLOT}}", + expectedGraffitis: map[string]bool{ + "Graffiti for validator 0": true, + "Graffiti for validator 1": true, + "Graffiti for validator 2": true, + "Graffiti for validator 3": true, + "Graffiti for validator 4": true, + "Graffiti for validator 5": true, + "Graffiti for validator 6": true, + "Graffiti for validator 7": true, + "Graffiti for validator 8": true, + "Graffiti for validator 9": true, + "Graffiti for validator 10": true, + "Graffiti for validator 11": true, + "Graffiti for validator 12": true, + "Graffiti for validator 13": true, + "Graffiti for validator 14": true, + "Graffiti for validator 15": true, + "Graffiti for slot 0": true, + "Graffiti for slot 1": true, + "Graffiti for slot 2": true, + "Graffiti for slot 3": true, + "Graffiti for slot 4": true, + "Graffiti for slot 5": true, + "Graffiti for slot 6": true, + "Graffiti for slot 7": true, + "Graffiti for slot 8": true, + "Graffiti for slot 9": true, + "Graffiti for slot 10": true, + "Graffiti for slot 11": true, + "Graffiti for slot 12": true, + "Graffiti for slot 13": true, + "Graffiti for slot 14": true, + "Graffiti for slot 15": true, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + obtainedGraffitis := make(map[string]bool) + for expectedGraffiti := range test.expectedGraffitis { + obtainedGraffitis[expectedGraffiti] = true + } + err = ioutil.WriteFile(filepath.Join(tmpDir, test.name), []byte(test.content), 0600) + require.NoError(t, err) + svc, err := dynamic.New(ctx, + dynamic.WithLogLevel(zerolog.Disabled), + dynamic.WithMajordomo(majordomoSvc), + dynamic.WithLocation(test.location)) + require.NoError(t, err) + for validatorIndex := uint64(0); validatorIndex < 16; validatorIndex++ { + for slot := uint64(0); slot < 16; slot++ { + graffiti, err := svc.Graffiti(ctx, slot, validatorIndex) + require.NoError(t, err) + require.Contains(t, test.expectedGraffitis, string(graffiti)) + delete(obtainedGraffitis, string(graffiti)) + } + } + require.Empty(t, obtainedGraffitis) + }) + } +} diff --git a/services/beaconnode/service.go b/services/graffitiprovider/service.go similarity index 63% rename from services/beaconnode/service.go rename to services/graffitiprovider/service.go index 896c1e7..b320ac8 100644 --- a/services/beaconnode/service.go +++ b/services/graffitiprovider/service.go @@ -11,12 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package beaconnode +// Package graffitiprovider provides graffiti for block proposals. +package graffitiprovider -// ConfigFetcher fetches chain configuration information. -type ConfigFetcher interface { - // FetchSecondsPerSlot fetches the number of seconds per slot for the attached beacon node. - FetchSecondsPerSlot() uint64 - // FetchSlotsPerEpoch fetches the number of slots per epoch for the attached beacon node. - FetchSlotsPerEpoch() uint64 +import ( + "context" +) + +// Service is the graffiti provider service. +type Service interface { + // Graffiti returns the graffiti for a given slot and validator. + Graffiti(ctx context.Context, slot uint64, validatorIndex uint64) ([]byte, error) } diff --git a/services/graffitiprovider/static/parameters.go b/services/graffitiprovider/static/parameters.go new file mode 100644 index 0000000..3ff945f --- /dev/null +++ b/services/graffitiprovider/static/parameters.go @@ -0,0 +1,68 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package static + +import ( + "errors" + + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + graffiti []byte +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithGraffiti sets the graffiti. +func WithGraffiti(graffiti []byte) Parameter { + return parameterFunc(func(p *parameters) { + p.graffiti = graffiti + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if len(parameters.graffiti) > 32 { + return nil, errors.New("graffiti has a mximum size of 32 bytes") + } + + return ¶meters, nil +} diff --git a/services/graffitiprovider/static/service.go b/services/graffitiprovider/static/service.go new file mode 100644 index 0000000..a18efbe --- /dev/null +++ b/services/graffitiprovider/static/service.go @@ -0,0 +1,55 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package static + +import ( + "context" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is a graffiti provider service. +type Service struct { + graffiti []byte +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new graffiti provider service. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "beaconblockproposer").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + graffiti: parameters.graffiti, + } + + return s, nil +} + +// Graffiti provides graffiti. +func (s *Service) Graffiti(ctx context.Context, slot uint64, validatorIndex uint64) ([]byte, error) { + return s.graffiti, nil +} diff --git a/services/graffitiprovider/static/service_test.go b/services/graffitiprovider/static/service_test.go new file mode 100644 index 0000000..4a3c4d0 --- /dev/null +++ b/services/graffitiprovider/static/service_test.go @@ -0,0 +1,41 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package static_test + +import ( + "context" + "testing" + + "github.com/attestantio/vouch/services/graffitiprovider/static" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + s, err := static.New(context.Background(), + static.WithLogLevel(zerolog.Disabled), + static.WithGraffiti([]byte("Test"))) + require.NoError(t, err) + res, err := s.Graffiti(context.Background(), 0, 0) + require.NoError(t, err) + require.Equal(t, []byte("Test"), res) +} + +func TestServiceNoGraffiti(t *testing.T) { + s, err := static.New(context.Background()) + require.NoError(t, err) + res, err := s.Graffiti(context.Background(), 0, 0) + require.NoError(t, err) + require.Nil(t, res) +} diff --git a/services/metrics/null/service.go b/services/metrics/null/service.go new file mode 100644 index 0000000..9f6c80f --- /dev/null +++ b/services/metrics/null/service.go @@ -0,0 +1,74 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package null is a null metrics logger. +package null + +import ( + "context" + "time" +) + +// Service is a metrics service that drops metrics. +type Service struct{} + +// New creates a new null metrics service. +func New(ctx context.Context) *Service { + return &Service{} +} + +// JobScheduled is called when a job is scheduled. +func (s *Service) JobScheduled() {} + +// JobCancelled is called when a scheduled job is cancelled. +func (s *Service) JobCancelled() {} + +// JobStartedOnTimer is called when a scheduled job is started due to meeting its time. +func (s *Service) JobStartedOnTimer() {} + +// JobStartedOnSignal is called when a scheduled job is started due to being manually signal. +func (s *Service) JobStartedOnSignal() {} + +// NewEpoch is called when vouch starts processing a new epoch. +func (s *Service) NewEpoch() {} + +// BlockDelay provides the delay between the start of a slot and vouch receiving its block. +func (s *Service) BlockDelay(delay time.Duration) {} + +// BeaconBlockProposalCompleted is called when a block proposal process has completed. +func (s *Service) BeaconBlockProposalCompleted(started time.Time, result string) {} + +// AttestationCompleted is called when a block attestation process has completed. +func (s *Service) AttestationCompleted(started time.Time, result string) {} + +// AttestationAggregationCompleted is called when an attestation aggregation process has completed. +func (s *Service) AttestationAggregationCompleted(started time.Time, result string) {} + +// AttestationAggregationCoverage measures the attestation ratio of the attestation aggregation. +func (s *Service) AttestationAggregationCoverage(frac float64) {} + +// BeaconCommitteeSubscriptionCompleted is called when an beacon committee subscription process has completed. +func (s *Service) BeaconCommitteeSubscriptionCompleted(started time.Time, result string) {} + +// BeaconCommitteeSubscribers sets the number of beacon committees to which our validators are subscribed. +func (s *Service) BeaconCommitteeSubscribers(subscribers int) {} + +// BeaconCommitteeAggregators sets the number of beacon committees for which our validators are aggregating. +func (s *Service) BeaconCommitteeAggregators(aggregators int) {} + +// Accounts sets the number of accounts in a given state. +func (s *Service) Accounts(state string, count uint64) {} + +// ClientOperation provides a generic monitor for client operations. +func (s *Service) ClientOperation(provider string, name string, succeeded bool, duration time.Duration) { +} diff --git a/services/metrics/prometheus/accountmanager.go b/services/metrics/prometheus/accountmanager.go new file mode 100644 index 0000000..6718e28 --- /dev/null +++ b/services/metrics/prometheus/accountmanager.go @@ -0,0 +1,37 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupAccountManagerMetrics() error { + s.accountManagerAccounts = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "vouch", + Subsystem: "accountmanager", + Name: "accounts_total", + Help: "The number of accounts managed by Vouch.", + }, []string{"state"}) + if err := prometheus.Register(s.accountManagerAccounts); err != nil { + return err + } + + return nil +} + +// Accounts sets the number of accounts in a given state. +func (s *Service) Accounts(state string, count uint64) { + s.accountManagerAccounts.WithLabelValues(state).Set(float64(count)) +} diff --git a/services/metrics/prometheus/attestation.go b/services/metrics/prometheus/attestation.go new file mode 100644 index 0000000..60c4ede --- /dev/null +++ b/services/metrics/prometheus/attestation.go @@ -0,0 +1,55 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupAttestationMetrics() error { + s.attestationProcessTimer = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "attestation_process", + Name: "duration_seconds", + Help: "The time vouch spends from starting the attestation process to submitting the attestation.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + }, + }) + if err := prometheus.Register(s.attestationProcessTimer); err != nil { + return err + } + + s.attestationProcessRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "attestation_process", + Name: "requests_total", + Help: "The number of attestation processes.", + }, []string{"result"}) + if err := prometheus.Register(s.attestationProcessRequests); err != nil { + return err + } + + return nil +} + +// AttestationCompleted is called when a block attestation process has completed. +func (s *Service) AttestationCompleted(started time.Time, result string) { + s.attestationProcessTimer.Observe(time.Since(started).Seconds()) + s.attestationProcessRequests.WithLabelValues(result).Inc() +} diff --git a/services/metrics/prometheus/attestationaggregation.go b/services/metrics/prometheus/attestationaggregation.go new file mode 100644 index 0000000..37d4ba6 --- /dev/null +++ b/services/metrics/prometheus/attestationaggregation.go @@ -0,0 +1,72 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupAttestationAggregationMetrics() error { + s.attestationAggregationProcessTimer = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "attestationaggregation_process", + Name: "duration_seconds", + Help: "The time vouch spends from starting the beacon block attestation aggregation process to submitting the aggregation beacon block attestation.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + }, + }) + if err := prometheus.Register(s.attestationAggregationProcessTimer); err != nil { + return err + } + + s.attestationAggregationProcessRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "attestationaggregation_process", + Name: "requests_total", + Help: "The number of beacon block attestation aggregation processes.", + }, []string{"result"}) + if err := prometheus.Register(s.attestationAggregationProcessRequests); err != nil { + return err + } + + s.attestationAggregationCoverageRatio = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "attestationaggregation", + Name: "coverage_ratio", + Help: "The ratio of included to possible attestations in the aggregate.", + Buckets: []float64{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}, + }) + if err := prometheus.Register(s.attestationAggregationCoverageRatio); err != nil { + return err + } + + return nil +} + +// AttestationAggregationCompleted is called when an attestation aggregationprocess has completed. +func (s *Service) AttestationAggregationCompleted(started time.Time, result string) { + s.attestationAggregationProcessTimer.Observe(time.Since(started).Seconds()) + s.attestationAggregationProcessRequests.WithLabelValues(result).Inc() +} + +// AttestationAggregationCoverage measures the attestation ratio of the attestation aggregation. +func (s *Service) AttestationAggregationCoverage(frac float64) { + s.attestationAggregationCoverageRatio.Observe(frac) +} diff --git a/services/metrics/prometheus/beaconblockproposal.go b/services/metrics/prometheus/beaconblockproposal.go new file mode 100644 index 0000000..e8ea216 --- /dev/null +++ b/services/metrics/prometheus/beaconblockproposal.go @@ -0,0 +1,55 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupBeaconBlockProposalMetrics() error { + s.beaconBlockProposalProcessTimer = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "beaconblockproposal_process", + Name: "duration_seconds", + Help: "The time vouch spends from starting the beacon block proposal process to submitting the beacon block.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + }, + }) + if err := prometheus.Register(s.beaconBlockProposalProcessTimer); err != nil { + return err + } + + s.beaconBlockProposalProcessRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "beaconblockproposal_process", + Name: "requests_total", + Help: "The number of beacon block proposal processes.", + }, []string{"result"}) + if err := prometheus.Register(s.beaconBlockProposalProcessRequests); err != nil { + return err + } + + return nil +} + +// BeaconBlockProposalCompleted is called when a block proposal process has completed. +func (s *Service) BeaconBlockProposalCompleted(started time.Time, result string) { + s.beaconBlockProposalProcessTimer.Observe(time.Since(started).Seconds()) + s.beaconBlockProposalProcessRequests.WithLabelValues(result).Inc() +} diff --git a/services/metrics/prometheus/beaconcommitteesubscription.go b/services/metrics/prometheus/beaconcommitteesubscription.go new file mode 100644 index 0000000..2b084d7 --- /dev/null +++ b/services/metrics/prometheus/beaconcommitteesubscription.go @@ -0,0 +1,85 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupBeaconCommitteeSubscriptionMetrics() error { + s.beaconCommitteeSubscriptionProcessTimer = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "beaconcommitteesubscription_process", + Name: "duration_seconds", + Help: "The time vouch spends from starting the beacon committee subscription process to submitting the subscription request.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + }, + }) + if err := prometheus.Register(s.beaconCommitteeSubscriptionProcessTimer); err != nil { + return err + } + + s.beaconCommitteeSubscriptionProcessRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "beaconcommitteesubscription_process", + Name: "requests_total", + Help: "The number of beacon committee subscription processes.", + }, []string{"result"}) + if err := prometheus.Register(s.beaconCommitteeSubscriptionProcessRequests); err != nil { + return err + } + + s.beaconCommitteeSubscribers = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "vouch", + Subsystem: "beaconcommitteesubscription", + Name: "subscribers_total", + Help: "The number of beacon committee subscribed.", + }) + if err := prometheus.Register(s.beaconCommitteeSubscribers); err != nil { + return err + } + + s.beaconCommitteeAggregators = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "vouch", + Subsystem: "beaconcommitteesubscription", + Name: "aggregators_total", + Help: "The number of beacon committee aggregated.", + }) + if err := prometheus.Register(s.beaconCommitteeAggregators); err != nil { + return err + } + + return nil +} + +// BeaconCommitteeSubscriptionCompleted is called when an beacon committee subscription process has completed. +func (s *Service) BeaconCommitteeSubscriptionCompleted(started time.Time, result string) { + s.beaconCommitteeSubscriptionProcessTimer.Observe(time.Since(started).Seconds()) + s.beaconCommitteeSubscriptionProcessRequests.WithLabelValues(result).Inc() +} + +// BeaconCommitteeSubscribers sets the number of beacon committees to which our validators are subscribed. +func (s *Service) BeaconCommitteeSubscribers(subscribers int) { + s.beaconCommitteeSubscribers.Set(float64(subscribers)) +} + +// BeaconCommitteeAggregators sets the number of beacon committees for which our validators are aggregating. +func (s *Service) BeaconCommitteeAggregators(aggregators int) { + s.beaconCommitteeAggregators.Set(float64(aggregators)) +} diff --git a/services/metrics/prometheus/client.go b/services/metrics/prometheus/client.go new file mode 100644 index 0000000..80abfba --- /dev/null +++ b/services/metrics/prometheus/client.go @@ -0,0 +1,58 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupClientMetrics() error { + s.clientOperationCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "client_operation", + Name: "requests_total", + }, []string{"provider", "operation", "result"}) + if err := prometheus.Register(s.clientOperationCounter); err != nil { + return err + } + s.clientOperationTimer = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "client_operation", + Name: "duration_seconds", + Help: "The time vouch spends in client operations.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3.0, + 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4.0, + }, + }, []string{"provider", "operation"}) + if err := prometheus.Register(s.clientOperationTimer); err != nil { + return err + } + + return nil +} + +// ClientOperation registers an operation. +func (s *Service) ClientOperation(provider string, operation string, succeeded bool, duration time.Duration) { + if succeeded { + s.clientOperationCounter.WithLabelValues(provider, operation, "succeeded").Add(1) + s.clientOperationTimer.WithLabelValues(provider, operation).Observe(duration.Seconds()) + } else { + s.clientOperationCounter.WithLabelValues(provider, operation, "failed").Add(1) + } +} diff --git a/services/metrics/prometheus/controller.go b/services/metrics/prometheus/controller.go new file mode 100644 index 0000000..596ca28 --- /dev/null +++ b/services/metrics/prometheus/controller.go @@ -0,0 +1,69 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupControllerMetrics() error { + controllerStartTime := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "vouch", + Name: "start_time_secs", + Help: "The timestamp at which vouch started.", + }) + if err := prometheus.Register(controllerStartTime); err != nil { + return err + } + controllerStartTime.SetToCurrentTime() + + s.epochsProcessed = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: "vouch", + Name: "epochs_processed_total", + Help: "The number of epochs vouch has processed.", + }) + if err := prometheus.Register(s.epochsProcessed); err != nil { + return err + } + + s.blockReceiptDelay = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Name: "block_receipt_delay_seconds", + Help: "The delay between the start of a slot and the time vouch receives it.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3.0, + 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4.0, + }, + }) + if err := prometheus.Register(s.blockReceiptDelay); err != nil { + return err + } + + return nil +} + +// NewEpoch is called when vouch starts processing a new epoch. +func (s *Service) NewEpoch() { + s.epochsProcessed.Inc() +} + +// BlockDelay provides the delay between the start of a slot and vouch receiving its block. +func (s *Service) BlockDelay(delay time.Duration) { + s.blockReceiptDelay.Observe(delay.Seconds()) +} diff --git a/services/metrics/prometheus/parameters.go b/services/metrics/prometheus/parameters.go new file mode 100644 index 0000000..322c65d --- /dev/null +++ b/services/metrics/prometheus/parameters.go @@ -0,0 +1,68 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "errors" + + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + address string +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithAddress sets the address. +func WithAddress(address string) Parameter { + return parameterFunc(func(p *parameters) { + p.address = address + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.address == "" { + return nil, errors.New("no address specified") + } + + return ¶meters, nil +} diff --git a/services/metrics/prometheus/scheduler.go b/services/metrics/prometheus/scheduler.go new file mode 100644 index 0000000..4177b42 --- /dev/null +++ b/services/metrics/prometheus/scheduler.go @@ -0,0 +1,72 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupSchedulerMetrics() error { + s.schedulerJobsScheduled = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "scheduler", + Name: "jobs_scheduled_total", + Help: "The total number of jobs scheduled.", + }) + if err := prometheus.Register(s.schedulerJobsScheduled); err != nil { + return err + } + + s.schedulerJobsCancelled = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "scheduler", + Name: "jobs_cancelled_total", + Help: "The total number of scheduled jobs cancelled.", + }) + if err := prometheus.Register(s.schedulerJobsCancelled); err != nil { + return err + } + + s.schedulerJobsStarted = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "scheduler", + Name: "jobs_started_total", + Help: "The total number of scheduled jobs started.", + }, []string{"trigger"}) + if err := prometheus.Register(s.schedulerJobsStarted); err != nil { + return err + } + + return nil +} + +// JobScheduled is called when a job is scheduled. +func (s *Service) JobScheduled() { + s.schedulerJobsScheduled.Inc() +} + +// JobCancelled is called when a scheduled job is cancelled. +func (s *Service) JobCancelled() { + s.schedulerJobsCancelled.Inc() +} + +// JobStartedOnTimer is called when a scheduled job is started due to meeting its time. +func (s *Service) JobStartedOnTimer() { + s.schedulerJobsStarted.WithLabelValues("timer").Inc() +} + +// JobStartedOnSignal is called when a scheduled job is started due to being manually signalled. +func (s *Service) JobStartedOnSignal() { + s.schedulerJobsStarted.WithLabelValues("signal").Inc() +} diff --git a/services/metrics/prometheus/service.go b/services/metrics/prometheus/service.go new file mode 100644 index 0000000..b362245 --- /dev/null +++ b/services/metrics/prometheus/service.go @@ -0,0 +1,108 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is a metrics service exposing metrics via prometheus. +type Service struct { + schedulerJobsScheduled prometheus.Counter + schedulerJobsCancelled prometheus.Counter + schedulerJobsStarted *prometheus.CounterVec + + epochsProcessed prometheus.Counter + blockReceiptDelay prometheus.Histogram + + beaconBlockProposalProcessTimer prometheus.Histogram + beaconBlockProposalProcessRequests *prometheus.CounterVec + + attestationProcessTimer prometheus.Histogram + attestationProcessRequests *prometheus.CounterVec + + attestationAggregationProcessTimer prometheus.Histogram + attestationAggregationProcessRequests *prometheus.CounterVec + attestationAggregationCoverageRatio prometheus.Histogram + + beaconCommitteeSubscriptionProcessTimer prometheus.Histogram + beaconCommitteeSubscriptionProcessRequests *prometheus.CounterVec + beaconCommitteeSubscribers prometheus.Gauge + beaconCommitteeAggregators prometheus.Gauge + + accountManagerAccounts *prometheus.GaugeVec + + clientOperationCounter *prometheus.CounterVec + clientOperationTimer *prometheus.HistogramVec +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new prometheus metrics service. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "metrics").Str("impl", "prometheus").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{} + + if err := s.setupSchedulerMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up scheduler metrics") + } + if err := s.setupControllerMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up controller metrics") + } + if err := s.setupBeaconBlockProposalMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up beacon block proposal metrics") + } + if err := s.setupAttestationMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up attestation metrics") + } + if err := s.setupAttestationAggregationMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up attestation aggregation metrics") + } + if err := s.setupBeaconCommitteeSubscriptionMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up beacon committee subscription metrics") + } + if err := s.setupAccountManagerMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up account manager metrics") + } + if err := s.setupClientMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up client metrics") + } + + go func() { + http.Handle("/metrics", promhttp.Handler()) + if err := http.ListenAndServe(parameters.address, nil); err != nil { + log.Warn().Str("metrics_address", parameters.address).Err(err).Msg("Failed to run metrics server") + } + }() + + return s, nil +} diff --git a/services/metrics/service.go b/services/metrics/service.go new file mode 100644 index 0000000..b342f78 --- /dev/null +++ b/services/metrics/service.go @@ -0,0 +1,83 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package metrics tracks various metrics that measure the performance of vouch. +package metrics + +import "time" + +// Service is the generic metrics service. +type Service interface{} + +// SchedulerMonitor provides methods to monitor the scheduler service. +type SchedulerMonitor interface { + // JobScheduled is called when a job is scheduled. + JobScheduled() + // JobCancelled is called when a scheduled job is cancelled. + JobCancelled() + // JobStartedOnTimer is called when a scheduled job is started due to meeting its time. + JobStartedOnTimer() + // JobStartedOnSignal is called when a scheduled job is started due to being manually signal. + JobStartedOnSignal() +} + +// ControllerMonitor provides methods to monitor the controller service. +type ControllerMonitor interface { + // NewEpoch is called when vouch starts processing a new epoch. + NewEpoch() + // BlockDelay provides the delay between the start of a slot and vouch receiving its block. + BlockDelay(delay time.Duration) +} + +// BeaconBlockProposalMonitor provides methods to monitor the block proposal process. +type BeaconBlockProposalMonitor interface { + // BeaconBlockProposalCompleted is called when a block proposal process has completed. + BeaconBlockProposalCompleted(started time.Time, result string) +} + +// AttestationMonitor provides methods to monitor the attestation process. +type AttestationMonitor interface { + // AttestationsCompleted is called when a attestation process has completed. + AttestationCompleted(started time.Time, result string) +} + +// AttestationAggregationMonitor provides methods to monitor the attestation aggregation process. +type AttestationAggregationMonitor interface { + // AttestationAggregationCompleted is called when an attestation aggregation process has completed. + AttestationAggregationCompleted(started time.Time, result string) + + // AttestationAggregationCoverage measures the attestation ratio of the attestation aggregation. + AttestationAggregationCoverage(frac float64) +} + +// BeaconCommitteeSubscriptionMonitor provides methods to monitor the outcome of beacon committee subscriptions. +type BeaconCommitteeSubscriptionMonitor interface { + // BeaconCommitteeSubscriptionCompleted is called when an beacon committee subscription process has completed. + BeaconCommitteeSubscriptionCompleted(started time.Time, result string) + // BeaconCommitteeSubscribers sets the number of beacon committees to which our validators are subscribed. + BeaconCommitteeSubscribers(subscribers int) + // BeaconCommitteeAggregators sets the number of beacon committees for which our validators are aggregating. + BeaconCommitteeAggregators(aggregators int) +} + +// AccountManagerMonitor provides methods to monitor the account manager. +type AccountManagerMonitor interface { + // Accounts sets the number of accounts in a given state. + Accounts(state string, count uint64) +} + +// ClientMonitor provides methods to monitor client connections. +type ClientMonitor interface { + // ClientOperation provides a generic monitor for client operations. + ClientOperation(provider string, name string, succeeded bool, duration time.Duration) +} diff --git a/services/scheduler/basic/parameters.go b/services/scheduler/basic/parameters.go new file mode 100644 index 0000000..ae2b7b1 --- /dev/null +++ b/services/scheduler/basic/parameters.go @@ -0,0 +1,68 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package basic + +import ( + "github.com/attestantio/vouch/services/metrics" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.SchedulerMonitor +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithMonitor sets the monitor for this module. +func WithMonitor(monitor metrics.SchedulerMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + + return ¶meters, nil +} diff --git a/services/scheduler/basic/service.go b/services/scheduler/basic/service.go new file mode 100644 index 0000000..97fb69c --- /dev/null +++ b/services/scheduler/basic/service.go @@ -0,0 +1,321 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package basic + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/scheduler" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "github.com/sasha-s/go-deadlock" +) + +// module-wide log. +var log zerolog.Logger + +// job contains control points for a job. +type job struct { + cancelCh chan struct{} + runCh chan struct{} + mutex deadlock.Mutex +} + +// Service is a controller service. +type Service struct { + monitor metrics.SchedulerMonitor + jobs map[string]*job + mutex deadlock.RWMutex +} + +// New creates a new scheduling service. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "scheduler").Str("impl", "basic").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + return &Service{ + jobs: make(map[string]*job), + monitor: parameters.monitor, + }, nil +} + +// ScheduleJob schedules a one-off job for a given time. +// This function returns two cancel funcs. If the first is triggered the job will not run. If the second is triggered the job +// runs immediately. +// Note that if the parent context is cancelled the job wil not run. +func (s *Service) ScheduleJob(ctx context.Context, name string, runtime time.Time, jobFunc scheduler.JobFunc, data interface{}) error { + if name == "" { + return scheduler.ErrNoJobName + } + if jobFunc == nil { + return scheduler.ErrNoJobFunc + } + + s.mutex.Lock() + defer s.mutex.Unlock() + + if _, exists := s.jobs[name]; exists { + return scheduler.ErrJobAlreadyExists + } + + cancelCh := make(chan struct{}) + runCh := make(chan struct{}) + s.jobs[name] = &job{ + cancelCh: cancelCh, + runCh: runCh, + } + s.monitor.JobScheduled() + + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Scheduled job") + go func() { + select { + case <-ctx.Done(): + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Parent context done; job not running") + s.mutex.Lock() + s.removeJob(ctx, name) + s.mutex.Unlock() + s.monitor.JobCancelled() + case <-cancelCh: + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Cancel triggered; job not running") + s.mutex.Lock() + s.removeJob(ctx, name) + s.mutex.Unlock() + s.monitor.JobCancelled() + case <-runCh: + s.mutex.Lock() + if s.jobExists(ctx, name) { + s.removeJob(ctx, name) + s.mutex.Unlock() + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Run triggered; job running") + s.monitor.JobStartedOnSignal() + jobFunc(ctx, data) + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Job complete") + } else { + // Job has been taken by another thread; do nothing. + s.mutex.Unlock() + } + case <-time.After(time.Until(runtime)): + s.mutex.Lock() + if s.jobExists(ctx, name) { + s.removeJob(ctx, name) + s.mutex.Unlock() + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Timer triggered; job running") + s.monitor.JobStartedOnTimer() + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Job complete") + jobFunc(ctx, data) + } else { + // Job has been taken by another thread; do nothing. + s.mutex.Unlock() + } + } + }() + + return nil +} + +// SchedulePeriodicJob scheduls a periodic job for a given time. +func (s *Service) SchedulePeriodicJob(ctx context.Context, name string, runtimeFunc scheduler.RuntimeFunc, runtimeData interface{}, jobFunc scheduler.JobFunc, jobData interface{}) error { + if name == "" { + return scheduler.ErrNoJobName + } + if runtimeFunc == nil { + return scheduler.ErrNoRuntimeFunc + } + if jobFunc == nil { + return scheduler.ErrNoJobFunc + } + + s.mutex.Lock() + defer s.mutex.Unlock() + + if _, exists := s.jobs[name]; exists { + return scheduler.ErrJobAlreadyExists + } + + cancelCh := make(chan struct{}) + runCh := make(chan struct{}) + s.jobs[name] = &job{ + cancelCh: cancelCh, + runCh: runCh, + } + s.monitor.JobScheduled() + + go func() { + for { + runtime, err := runtimeFunc(ctx, runtimeData) + if err == scheduler.ErrNoMoreInstances { + log.Trace().Str("job", name).Msg("No more instances; period job stopping") + s.mutex.Lock() + s.removeJob(ctx, name) + s.mutex.Unlock() + s.monitor.JobCancelled() + return + } + if err != nil { + log.Error().Str("job", name).Err(err).Msg("Failed to obtain runtime; periodic job stopping") + s.mutex.Lock() + s.removeJob(ctx, name) + s.mutex.Unlock() + s.monitor.JobCancelled() + return + } + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Scheduled job") + select { + case <-ctx.Done(): + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Parent context done; job not running") + s.mutex.Lock() + s.removeJob(ctx, name) + s.mutex.Unlock() + s.monitor.JobCancelled() + return + case <-cancelCh: + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Cancel triggered; job not running") + s.mutex.Lock() + s.removeJob(ctx, name) + s.mutex.Unlock() + s.monitor.JobCancelled() + return + case <-runCh: + s.mutex.Lock() + s.lockJob(ctx, name) + s.mutex.Unlock() + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Run triggered; job running") + s.monitor.JobStartedOnSignal() + jobFunc(ctx, jobData) + s.unlockJob(ctx, name) + case <-time.After(time.Until(runtime)): + s.mutex.Lock() + s.lockJob(ctx, name) + s.mutex.Unlock() + log.Trace().Str("job", name).Str("scheduled", fmt.Sprintf("%v", runtime)).Msg("Timer triggered; job running") + s.monitor.JobStartedOnTimer() + jobFunc(ctx, jobData) + s.unlockJob(ctx, name) + } + } + }() + + return nil +} + +// RunJob runs a named job immediately. +// If the job does not exist it will return an appropriate error. +func (s *Service) RunJob(ctx context.Context, name string) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + + job, exists := s.jobs[name] + if !exists { + return scheduler.ErrNoSuchJob + } + + job.runCh <- struct{}{} + return nil +} + +// JobExists returns true if a job exists. +func (s *Service) JobExists(ctx context.Context, name string) bool { + s.mutex.RLock() + defer s.mutex.RUnlock() + _, exists := s.jobs[name] + return exists +} + +// RunJobIfExists runs a job if it exists. +// This does not return an error if the job does not exist. +// If the job does not exist it will return an appropriate error. +func (s *Service) RunJobIfExists(ctx context.Context, name string) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + + if job, exists := s.jobs[name]; exists { + job.runCh <- struct{}{} + } + return nil +} + +// CancelJob removes a named job. +// If the job does not exist it will return an appropriate error. +func (s *Service) CancelJob(ctx context.Context, name string) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + + job, exists := s.jobs[name] + if !exists { + return scheduler.ErrNoSuchJob + } + + job.cancelCh <- struct{}{} + return nil +} + +// CancelJobs cancels all jobs with the given prefix. +// If the prefix matches a period job then all future instances are cancelled. +func (s *Service) CancelJobs(ctx context.Context, prefix string) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + for name, job := range s.jobs { + if strings.HasPrefix(name, prefix) { + job.cancelCh <- struct{}{} + } + } + + return nil +} + +// jobExists returns true if the job exists in the job list. +// This assumes that the service mutex is held. +func (s *Service) jobExists(ctx context.Context, name string) bool { + _, exists := s.jobs[name] + return exists +} + +// removeJob is an internal function to remove a named job. It will fail silently if the job does not exist. +// This assumes that the service mutex is held. +func (s *Service) removeJob(ctx context.Context, name string) { + delete(s.jobs, name) +} + +// lockJob locks a specific job. +// This assumes that the service mutex is held. +func (s *Service) lockJob(ctx context.Context, name string) { + job, exists := s.jobs[name] + if !exists { + return + } + job.mutex.Lock() +} + +// unlockJob unlocks a specific job. +// This assumes that the service mutex is held. +func (s *Service) unlockJob(ctx context.Context, name string) { + job, exists := s.jobs[name] + if !exists { + return + } + job.mutex.Unlock() +} diff --git a/services/scheduler/basic/service_test.go b/services/scheduler/basic/service_test.go new file mode 100644 index 0000000..15a7886 --- /dev/null +++ b/services/scheduler/basic/service_test.go @@ -0,0 +1,383 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package basic_test + +import ( + "context" + "errors" + "fmt" + "math/rand" + "sync/atomic" + "testing" + "time" + + nullmetrics "github.com/attestantio/vouch/services/metrics/null" + "github.com/attestantio/vouch/services/scheduler" + "github.com/attestantio/vouch/services/scheduler/basic" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + ctx := context.Background() + + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + assert.NotNil(t, s) +} + +func TestJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + require.NoError(t, s.ScheduleJob(ctx, "Test job", time.Now().Add(100*time.Millisecond), runFunc, nil)) + require.Equal(t, 0, run) + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 1, run) +} + +func TestJobExists(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + require.NoError(t, s.ScheduleJob(ctx, "Test job", time.Now().Add(10*time.Second), runFunc, nil)) + + require.True(t, s.JobExists(ctx, "Test job")) + require.False(t, s.JobExists(ctx, "Unknown job")) + + require.NoError(t, s.CancelJob(ctx, "Test job")) +} + +func TestCancelJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + require.NoError(t, s.ScheduleJob(ctx, "Test job", time.Now().Add(100*time.Millisecond), runFunc, nil)) + require.Equal(t, 0, run) + require.NoError(t, s.CancelJob(ctx, "Test job")) + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 0, run) +} + +func TestCancelUnknownJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + assert.EqualError(t, s.CancelJob(ctx, "Unknown job"), scheduler.ErrNoSuchJob.Error()) +} + +func TestCancelJobs(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + require.NoError(t, s.ScheduleJob(ctx, "Test job 1", time.Now().Add(100*time.Millisecond), runFunc, nil)) + require.NoError(t, s.ScheduleJob(ctx, "Test job 2", time.Now().Add(100*time.Millisecond), runFunc, nil)) + require.NoError(t, s.ScheduleJob(ctx, "No cancel job", time.Now().Add(100*time.Millisecond), runFunc, nil)) + require.Equal(t, 0, run) + require.NoError(t, s.CancelJobs(ctx, "Test job")) + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 1, run) +} + +func TestCancelParentContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + require.NoError(t, s.ScheduleJob(ctx, "Test job", time.Now().Add(100*time.Millisecond), runFunc, nil)) + require.Equal(t, 0, run) + cancel() + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 0, run) +} + +func TestRunJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + require.NoError(t, s.ScheduleJob(ctx, "Test job", time.Now().Add(time.Second), runFunc, nil)) + require.Equal(t, 0, run) + require.NoError(t, s.RunJob(ctx, "Test job")) + time.Sleep(time.Duration(100) * time.Millisecond) + assert.Equal(t, 1, run) +} + +func TestRunJobIfExists(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + require.NoError(t, s.ScheduleJob(ctx, "Test job", time.Now().Add(time.Second), runFunc, nil)) + require.Equal(t, 0, run) + require.NoError(t, s.RunJobIfExists(ctx, "Unknown job")) + require.Equal(t, 0, run) + require.NoError(t, s.RunJobIfExists(ctx, "Test job")) + time.Sleep(time.Duration(100) * time.Millisecond) + assert.Equal(t, 1, run) +} + +func TestRunUnknownJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + assert.EqualError(t, s.RunJob(ctx, "Unknown job"), scheduler.ErrNoSuchJob.Error()) +} + +func TestPeriodicJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + return time.Now().Add(100 * time.Millisecond), nil + } + + require.NoError(t, s.SchedulePeriodicJob(ctx, "Test periodic job", runtimeFunc, nil, runFunc, nil)) + require.Equal(t, 0, run) + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 1, run) + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 2, run) + require.NoError(t, s.RunJob(ctx, "Test periodic job")) + time.Sleep(time.Duration(10) * time.Millisecond) + assert.Equal(t, 3, run) +} + +func TestCancelPeriodicJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + return time.Now().Add(100 * time.Millisecond), nil + } + + require.NoError(t, s.SchedulePeriodicJob(ctx, "Test periodic job", runtimeFunc, nil, runFunc, nil)) + require.Equal(t, 0, run) + require.NoError(t, s.CancelJob(ctx, "Test periodic job")) + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 0, run) +} + +func TestCancelPeriodicParentContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + return time.Now().Add(100 * time.Millisecond), nil + } + + require.NoError(t, s.SchedulePeriodicJob(ctx, "Test job", runtimeFunc, nil, runFunc, nil)) + require.Equal(t, 0, run) + cancel() + time.Sleep(time.Duration(110) * time.Millisecond) + assert.Equal(t, 0, run) +} + +func TestLimitedPeriodicJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + if run == 3 { + return time.Now(), scheduler.ErrNoMoreInstances + } + return time.Now().Add(10 * time.Millisecond), nil + } + + require.NoError(t, s.SchedulePeriodicJob(ctx, "Test job", runtimeFunc, nil, runFunc, nil)) + require.Equal(t, 0, run) + time.Sleep(time.Duration(50) * time.Millisecond) + assert.Equal(t, 3, run) +} + +func TestBadPeriodicJob(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + if run == 3 { + return time.Now(), errors.New("Bad") + } + return time.Now().Add(10 * time.Millisecond), nil + } + + require.NoError(t, s.SchedulePeriodicJob(ctx, "Test job", runtimeFunc, nil, runFunc, nil)) + require.Equal(t, 0, run) + time.Sleep(time.Duration(50) * time.Millisecond) + assert.Equal(t, 3, run) +} + +func TestDuplicateJobName(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + return time.Now().Add(100 * time.Millisecond), nil + } + + require.NoError(t, s.ScheduleJob(ctx, "Test duplicate job", time.Now().Add(time.Second), runFunc, nil)) + require.EqualError(t, s.ScheduleJob(ctx, "Test duplicate job", time.Now().Add(time.Second), runFunc, nil), scheduler.ErrJobAlreadyExists.Error()) + + require.NoError(t, s.SchedulePeriodicJob(ctx, "Test duplicate periodic job", runtimeFunc, nil, runFunc, nil)) + require.EqualError(t, s.SchedulePeriodicJob(ctx, "Test duplicate periodic job", runtimeFunc, nil, runFunc, nil), scheduler.ErrJobAlreadyExists.Error()) +} + +func TestBadJobs(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := 0 + runFunc := func(ctx context.Context, data interface{}) { + run++ + } + + runtimeFunc := func(ctx context.Context, data interface{}) (time.Time, error) { + return time.Now().Add(100 * time.Millisecond), nil + } + + require.EqualError(t, s.ScheduleJob(ctx, "", time.Now(), runFunc, nil), scheduler.ErrNoJobName.Error()) + require.EqualError(t, s.ScheduleJob(ctx, "Test bad job", time.Now(), nil, nil), scheduler.ErrNoJobFunc.Error()) + + require.EqualError(t, s.SchedulePeriodicJob(ctx, "", runtimeFunc, nil, runFunc, nil), scheduler.ErrNoJobName.Error()) + require.EqualError(t, s.SchedulePeriodicJob(ctx, "Test bad period job", nil, nil, runFunc, nil), scheduler.ErrNoRuntimeFunc.Error()) + require.EqualError(t, s.SchedulePeriodicJob(ctx, "Test bad period job", runtimeFunc, nil, nil, nil), scheduler.ErrNoJobFunc.Error()) +} + +func TestManyJobs(t *testing.T) { + ctx := context.Background() + s, err := basic.New(ctx, basic.WithLogLevel(zerolog.Disabled), basic.WithMonitor(&nullmetrics.Service{})) + require.NoError(t, err) + require.NotNil(t, s) + + run := uint32(0) + runFunc := func(ctx context.Context, data interface{}) { + atomic.AddUint32(&run, 1) + } + + runTime := time.Now().Add(200 * time.Millisecond) + + jobs := 2048 + for i := 0; i < jobs; i++ { + require.NoError(t, s.ScheduleJob(ctx, fmt.Sprintf("Job instance %d", i), runTime, runFunc, nil)) + } + + // Kick off some jobs early. + for i := 0; i < jobs/32; i++ { + // #nosec G404 + randomJob := rand.Intn(jobs) + // Don't check for error as we could try to kick off the same job multiple times, which would cause an error. + //nolint + s.RunJob(ctx, fmt.Sprintf("Job instance %d", randomJob)) + } + + // Sleep to let the others run normally. + time.Sleep(400 * time.Millisecond) + + require.Equal(t, uint32(jobs), run) +} diff --git a/services/scheduler/service.go b/services/scheduler/service.go new file mode 100644 index 0000000..5e2be58 --- /dev/null +++ b/services/scheduler/service.go @@ -0,0 +1,76 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scheduler + +import ( + "context" + "errors" + "time" +) + +// JobFunc is the type for jobs. +type JobFunc func(context.Context, interface{}) + +// RuntimeFunc is the type of a function that generates the next runtime. +type RuntimeFunc func(context.Context, interface{}) (time.Time, error) + +// ErrNoMoreInstances is returned by the runtime generator when it has no more instances. +var ErrNoMoreInstances = errors.New("no more instances") + +// ErrNoSuchJob is returned when the scheduler is asked to act upon a job about which it has no information. +var ErrNoSuchJob = errors.New("no such job") + +// ErrJobAlreadyExists is returned when the scheduler is asked to create a job that already exists. +var ErrJobAlreadyExists = errors.New("job already exists") + +// ErrNoJobName is returned when an attempt is made to to control a job without a name. +var ErrNoJobName = errors.New("no job name") + +// ErrNoJobFunc is returned when an attempt is made to to run a nil job. +var ErrNoJobFunc = errors.New("no job function") + +// ErrNoRuntimeFunc is returned when an attempt is made to to run a periodic job without a runtime function. +var ErrNoRuntimeFunc = errors.New("no runtime function") + +// Service is the interface for schedulers. +type Service interface { + // ScheduleJob schedules a one-off job for a given time. + // This function returns two cancel funcs. If the first is triggered the job will not run. If the second is triggered the job + // runs immediately. + // Note that if the parent context is cancelled the job wil not run. + ScheduleJob(ctx context.Context, name string, runtime time.Time, job JobFunc, data interface{}) error + + // SchedulePeriodicJob schedules a job to run in a loop. + SchedulePeriodicJob(ctx context.Context, name string, runtime RuntimeFunc, runtineData interface{}, job JobFunc, jobData interface{}) error + + // CancelJob cancels a known job. + // If this is a period job then all future instances are cancelled. + CancelJob(ctx context.Context, name string) error + + // CancelJobs cancels all jobs with the given prefix. + // If the prefix matches a period job then all future instances are cancelled. + CancelJobs(ctx context.Context, prefix string) error + + // RunJob runs a known job. + // If this is a period job then the next instance will be scheduled. + RunJob(ctx context.Context, name string) error + + // JobExists returns true if a job exists. + JobExists(ctx context.Context, name string) bool + + // RunJobIfExists runs a job if it exists. + // This does not return an error if the job does not exist. + // If this is a period job then the next instance will be scheduled. + RunJobIfExists(ctx context.Context, name string) error +} diff --git a/services/signer/service.go b/services/signer/service.go deleted file mode 100644 index 95cee60..0000000 --- a/services/signer/service.go +++ /dev/null @@ -1,42 +0,0 @@ -package signer - -import "context" - -type RandaoRevealSigner interface { - // SignRanDAOReveal returns a RANDAO signature. - // This signs an epoch with the "RANDAO" domain. - SignRandaoReveal(ctx context.Context, pubKey []byte, epoch uint64) ([]byte, error) -} - -type SlotSelectionSigner interface { - // SignSlotSelection returns a slot selection signature. - // This signs a slot with the "selection proof" domain. - SignSlotSelection(ctx context.Context, pubKey []byte, epoch uint64) ([]byte, error) -} - -type BeaconBlockSigner interface { - // SignBeaconBlockProposal signs a beacon block proposal. - // TODO beaconBlockheader needs to be a struct. - SignBeaconBlockProposal(ctx context.Context, pubKey []byte, domain []byte, beaconBlockHeader []byte) ([]byte, error) -} - -type BeaconAttestationSigner interface { - // SignBeaconAttestation signs a beacon attestation. - // TODO attestation needs to be a struct. - SignBeaconAttestation(ctx context.Context, pubKey []byte, attestation []byte) ([]byte, error) -} - -type BeaconAggregateAndProofSigner interface { - // SignAggregateAndProof signs an aggregate attestation. - // TODO aggregateAndProof needs to be a struct. - SignAggregateAndProof(ctx context.Context, pubKey []byte, aggregateAndProof []byte) ([]byte, error) -} - -// Signer is a composite interface for all signer operations. -type Signer interface { - RandaoRevealSigner - SlotSelectionSigner - BeaconBlockSigner - BeaconAttestationSigner - BeaconAggregateAndProofSigner -} diff --git a/services/submitter/immediate/parameters.go b/services/submitter/immediate/parameters.go new file mode 100644 index 0000000..1ba667e --- /dev/null +++ b/services/submitter/immediate/parameters.go @@ -0,0 +1,102 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package immediate is a submitter that immediately submits requests received. +package immediate + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + beaconBlockSubmitter eth2client.BeaconBlockSubmitter + attestationSubmitter eth2client.AttestationSubmitter + beaconCommitteeSubscriptionsSubmitter eth2client.BeaconCommitteeSubscriptionsSubmitter + aggregateAttestationsSubmitter eth2client.AggregateAttestationsSubmitter +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithBeaconBlockSubmitter sets the beacon block submitter +func WithBeaconBlockSubmitter(submitter eth2client.BeaconBlockSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockSubmitter = submitter + }) +} + +// WithAttestationSubmitter sets the attestation submitter +func WithAttestationSubmitter(submitter eth2client.AttestationSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.attestationSubmitter = submitter + }) +} + +// WithBeaconCommitteeSubscriptionsSubmitter sets the attestation subnet subscriptions submitter +func WithBeaconCommitteeSubscriptionsSubmitter(submitter eth2client.BeaconCommitteeSubscriptionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconCommitteeSubscriptionsSubmitter = submitter + }) +} + +// WithAggregateAttestationsSubmitter sets the aggregate attestation submitter +func WithAggregateAttestationsSubmitter(submitter eth2client.AggregateAttestationsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.aggregateAttestationsSubmitter = submitter + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.beaconBlockSubmitter == nil { + return nil, errors.New("no beacon block submitter specified") + } + if parameters.attestationSubmitter == nil { + return nil, errors.New("no attestation submitter specified") + } + if parameters.beaconCommitteeSubscriptionsSubmitter == nil { + return nil, errors.New("no beacon committee subscriptions submitter specified") + } + if parameters.aggregateAttestationsSubmitter == nil { + return nil, errors.New("no aggregate attestations submitter specified") + } + + return ¶meters, nil +} diff --git a/services/submitter/immediate/service.go b/services/submitter/immediate/service.go new file mode 100644 index 0000000..2820bd1 --- /dev/null +++ b/services/submitter/immediate/service.go @@ -0,0 +1,160 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package immediate + +import ( + "context" + "encoding/json" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is the submitter for signed items. +type Service struct { + attestationSubmitter eth2client.AttestationSubmitter + beaconBlockSubmitter eth2client.BeaconBlockSubmitter + beaconCommitteeSubscriptionsSubmitter eth2client.BeaconCommitteeSubscriptionsSubmitter + aggregateAttestationsSubmitter eth2client.AggregateAttestationsSubmitter +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new submitter. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "submitter").Str("impl", "immediate").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + attestationSubmitter: parameters.attestationSubmitter, + beaconBlockSubmitter: parameters.beaconBlockSubmitter, + beaconCommitteeSubscriptionsSubmitter: parameters.beaconCommitteeSubscriptionsSubmitter, + aggregateAttestationsSubmitter: parameters.aggregateAttestationsSubmitter, + } + + return s, nil +} + +// SubmitBeaconBlock submits a block. +func (s *Service) SubmitBeaconBlock(ctx context.Context, block *spec.SignedBeaconBlock) error { + if block == nil { + return errors.New("no beacon block supplied") + } + + if err := s.beaconBlockSubmitter.SubmitBeaconBlock(ctx, block); err != nil { + return errors.Wrap(err, "failed to submit beacon block") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(block) + if err == nil { + e.Str("block", string(data)).Msg("Submitted beacon block") + } + } + + return nil +} + +// SubmitAttestation submits an attestation. +func (s *Service) SubmitAttestation(ctx context.Context, attestation *spec.Attestation) error { + if attestation == nil { + return errors.New("no attestation supplied") + } + + if err := s.attestationSubmitter.SubmitAttestation(ctx, attestation); err != nil { + return errors.Wrap(err, "failed to submit attestation") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(attestation) + if err == nil { + e.Str("attestation", string(data)).Msg("Submitted attestation") + } + } + + return nil +} + +// SubmitBeaconCommitteeSubscriptions submits a batch of beacon committee subscriptions. +func (s *Service) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*submitter.BeaconCommitteeSubscription) error { + if subscriptions == nil { + return errors.New("no subscriptions supplied") + } + + subs := make([]*eth2client.BeaconCommitteeSubscription, len(subscriptions)) + for i, subscription := range subscriptions { + subs[i] = ð2client.BeaconCommitteeSubscription{ + Slot: subscription.Slot, + CommitteeIndex: subscription.CommitteeIndex, + CommitteeSize: subscription.CommitteeSize, + ValidatorIndex: subscription.ValidatorIndex, + ValidatorPubKey: subscription.ValidatorPubKey, + Aggregate: subscription.Aggregate, + SlotSelectionSignature: subscription.Signature, + } + } + if err := s.beaconCommitteeSubscriptionsSubmitter.SubmitBeaconCommitteeSubscriptions(ctx, subs); err != nil { + return errors.Wrap(err, "failed to submit beacon committee subscriptions") + } + + if e := log.Trace(); e.Enabled() { + // Summary counts. + aggregating := 0 + for i := range subscriptions { + if subscriptions[i].Aggregate { + aggregating++ + } + } + + data, err := json.Marshal(subscriptions) + if err == nil { + e.Str("subscriptions", string(data)).Int("subscribing", len(subscriptions)).Int("aggregating", aggregating).Msg("Submitted subscriptions") + } + } + + return nil +} + +// SubmitAggregateAttestation submits an aggregate attestation. +func (s *Service) SubmitAggregateAttestation(ctx context.Context, aggregate *spec.SignedAggregateAndProof) error { + if aggregate == nil { + return errors.New("no aggregate attestation supplied") + } + + if err := s.aggregateAttestationsSubmitter.SubmitAggregateAttestations(ctx, []*spec.SignedAggregateAndProof{aggregate}); err != nil { + return errors.Wrap(err, "failed to submit aggregate attestation") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(aggregate) + if err == nil { + e.Str("attestation", string(data)).Msg("Submitted aggregate attestation") + } + } + + return nil +} diff --git a/services/submitter/immediate/service_test.go b/services/submitter/immediate/service_test.go new file mode 100644 index 0000000..24dd6f9 --- /dev/null +++ b/services/submitter/immediate/service_test.go @@ -0,0 +1,131 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package immediate_test + +import ( + "context" + "testing" + + "github.com/attestantio/vouch/mock" + "github.com/attestantio/vouch/services/submitter" + "github.com/attestantio/vouch/services/submitter/immediate" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + attestationSubmitter := mock.NewAttestationSubmitter() + beaconBlockSubmitter := mock.NewBeaconBlockSubmitter() + beaconCommitteeSubscriptionSubmitter := mock.NewBeaconCommitteeSubscriptionsSubmitter() + aggregateAttestationSubmitter := mock.NewAggregateAttestationsSubmitter() + + tests := []struct { + name string + params []immediate.Parameter + err string + }{ + { + name: "AttestationSubmitterMissing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + }, + err: "problem with parameters: no attestation submitter specified", + }, + { + name: "BeaconBlockSubmitterMissing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationSubmitter(attestationSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + }, + err: "problem with parameters: no beacon block submitter specified", + }, + { + name: "AttestationSubnetSubscriptionsSubmitterMissing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationSubmitter(attestationSubmitter), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + }, + err: "problem with parameters: no beacon committee subscriptions submitter specified", + }, + { + name: "AggregateAttestationSubmitterMissing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationSubmitter(attestationSubmitter), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + }, + err: "problem with parameters: no aggregate attestations submitter specified", + }, + { + name: "Good", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationSubmitter(attestationSubmitter), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := immediate.New(context.Background(), test.params...) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSubmit(t *testing.T) { + s, err := immediate.New(context.Background(), + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationSubmitter(mock.NewAttestationSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + ) + require.NoError(t, err) + + require.EqualError(t, s.SubmitBeaconBlock(context.Background(), nil), "no beacon block supplied") + require.EqualError(t, s.SubmitAttestation(context.Background(), nil), "no attestation supplied") + require.EqualError(t, s.SubmitBeaconCommitteeSubscriptions(context.Background(), nil), "no subscriptions supplied") + require.EqualError(t, s.SubmitAggregateAttestation(context.Background(), nil), "no aggregate attestation supplied") +} + +func TestInterfaces(t *testing.T) { + s, err := immediate.New(context.Background(), + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationSubmitter(mock.NewAttestationSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + ) + require.NoError(t, err) + require.Implements(t, (*submitter.BeaconBlockSubmitter)(nil), s) + require.Implements(t, (*submitter.AttestationSubmitter)(nil), s) + require.Implements(t, (*submitter.BeaconCommitteeSubscriptionsSubmitter)(nil), s) + require.Implements(t, (*submitter.AggregateAttestationSubmitter)(nil), s) +} diff --git a/services/submitter/multinode/parameters.go b/services/submitter/multinode/parameters.go new file mode 100644 index 0000000..4c3ac62 --- /dev/null +++ b/services/submitter/multinode/parameters.go @@ -0,0 +1,114 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package multinode is a strategy that obtains beacon block proposals from multiple +// nodes and selects the best one based on its attestation load. +package multinode + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + processConcurrency int64 + beaconBlockSubmitters map[string]eth2client.BeaconBlockSubmitter + attestationSubmitters map[string]eth2client.AttestationSubmitter + aggregateAttestationsSubmitters map[string]eth2client.AggregateAttestationsSubmitter + beaconCommitteeSubscriptionsSubmitters map[string]eth2client.BeaconCommitteeSubscriptionsSubmitter +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithProcessConcurrency sets the concurrency for the service. +func WithProcessConcurrency(concurrency int64) Parameter { + return parameterFunc(func(p *parameters) { + p.processConcurrency = concurrency + }) +} + +// WithBeaconBlockSubmitters sets the beacon block submitters. +func WithBeaconBlockSubmitters(submitters map[string]eth2client.BeaconBlockSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockSubmitters = submitters + }) +} + +// WithAttestationSubmitters sets the attestation submitters. +func WithAttestationSubmitters(submitters map[string]eth2client.AttestationSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.attestationSubmitters = submitters + }) +} + +// WithAggregateAttestationsSubmitters sets the aggregate attestation submitters. +func WithAggregateAttestationsSubmitters(submitters map[string]eth2client.AggregateAttestationsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.aggregateAttestationsSubmitters = submitters + }) +} + +// WithBeaconCommitteeSubscriptionsSubmitters sets the attestation submitters. +func WithBeaconCommitteeSubscriptionsSubmitters(submitters map[string]eth2client.BeaconCommitteeSubscriptionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconCommitteeSubscriptionsSubmitters = submitters + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.processConcurrency == 0 { + return nil, errors.New("no process concurrency specified") + } + if parameters.beaconBlockSubmitters == nil { + return nil, errors.New("no beacon block submitters specified") + } + if parameters.attestationSubmitters == nil { + return nil, errors.New("no attestation submitters specified") + } + if parameters.aggregateAttestationsSubmitters == nil { + return nil, errors.New("no aggregate attestations submitters specified") + } + if parameters.beaconCommitteeSubscriptionsSubmitters == nil { + return nil, errors.New("no beacon committee subscription submitters specified") + } + + return ¶meters, nil +} diff --git a/services/submitter/multinode/service.go b/services/submitter/multinode/service.go new file mode 100644 index 0000000..b35ac14 --- /dev/null +++ b/services/submitter/multinode/service.go @@ -0,0 +1,59 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is the provider for beacon block proposals. +type Service struct { + processConcurrency int64 + beaconBlockSubmitters map[string]eth2client.BeaconBlockSubmitter + attestationSubmitters map[string]eth2client.AttestationSubmitter + aggregateAttestationsSubmitters map[string]eth2client.AggregateAttestationsSubmitter + beaconCommitteeSubscriptionSubmitters map[string]eth2client.BeaconCommitteeSubscriptionsSubmitter +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new beacon block propsal strategy. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("strategy", "submitter").Str("impl", "multinode").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + processConcurrency: parameters.processConcurrency, + beaconBlockSubmitters: parameters.beaconBlockSubmitters, + attestationSubmitters: parameters.attestationSubmitters, + aggregateAttestationsSubmitters: parameters.aggregateAttestationsSubmitters, + beaconCommitteeSubscriptionSubmitters: parameters.beaconCommitteeSubscriptionsSubmitters, + } + + return s, nil +} diff --git a/services/submitter/multinode/submitaggregateattestation.go b/services/submitter/multinode/submitaggregateattestation.go new file mode 100644 index 0000000..c946552 --- /dev/null +++ b/services/submitter/multinode/submitaggregateattestation.go @@ -0,0 +1,66 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + "encoding/json" + "sync" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" +) + +// SubmitAggregateAttestation submits an aggregate attestation. +func (s *Service) SubmitAggregateAttestation(ctx context.Context, aggregate *spec.SignedAggregateAndProof) error { + if aggregate == nil { + return errors.New("no aggregate attestation supplied") + } + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, submitter := range s.aggregateAttestationsSubmitters { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + name string, + submitter eth2client.AggregateAttestationsSubmitter, + ) { + defer wg.Done() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + if err := submitter.SubmitAggregateAttestations(ctx, []*spec.SignedAggregateAndProof{aggregate}); err != nil { + log.Warn().Str("submitter", name).Uint64("slot", aggregate.Message.Aggregate.Data.Slot).Err(err).Msg("Failed to submit aggregate attestation") + return + } + }(ctx, sem, &wg, name, submitter) + } + wg.Wait() + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(aggregate) + if err == nil { + e.Str("attestation", string(data)).Msg("Submitted aggregate attestation") + } + } + + return nil +} diff --git a/services/submitter/multinode/submitattestation.go b/services/submitter/multinode/submitattestation.go new file mode 100644 index 0000000..ccb2470 --- /dev/null +++ b/services/submitter/multinode/submitattestation.go @@ -0,0 +1,66 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + "encoding/json" + "sync" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" +) + +// SubmitAttestation submits an attestation. +func (s *Service) SubmitAttestation(ctx context.Context, attestation *spec.Attestation) error { + if attestation == nil { + return errors.New("no attestation supplied") + } + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, submitter := range s.attestationSubmitters { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + name string, + submitter eth2client.AttestationSubmitter, + ) { + defer wg.Done() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + if err := submitter.SubmitAttestation(ctx, attestation); err != nil { + log.Warn().Str("submitter", name).Uint64("slot", attestation.Data.Slot).Err(err).Msg("Failed to submit attestation") + return + } + }(ctx, sem, &wg, name, submitter) + } + wg.Wait() + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(attestation) + if err == nil { + e.Str("attestation", string(data)).Msg("Submitted attestation") + } + } + + return nil +} diff --git a/services/submitter/multinode/submitbeaconblock.go b/services/submitter/multinode/submitbeaconblock.go new file mode 100644 index 0000000..772fa63 --- /dev/null +++ b/services/submitter/multinode/submitbeaconblock.go @@ -0,0 +1,66 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + "encoding/json" + "sync" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" +) + +// SubmitBeaconBlock submits a beacon block. +func (s *Service) SubmitBeaconBlock(ctx context.Context, block *spec.SignedBeaconBlock) error { + if block == nil { + return errors.New("no beacon block supplied") + } + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, submitter := range s.beaconBlockSubmitters { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + name string, + submitter eth2client.BeaconBlockSubmitter, + ) { + defer wg.Done() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + if err := submitter.SubmitBeaconBlock(ctx, block); err != nil { + log.Warn().Str("submitter", name).Uint64("slot", block.Message.Slot).Err(err).Msg("Failed to submit beacon block") + return + } + }(ctx, sem, &wg, name, submitter) + } + wg.Wait() + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(block) + if err == nil { + e.Str("block", string(data)).Msg("Submitted beacon block") + } + } + + return nil +} diff --git a/services/submitter/multinode/submitbeaconcommitteesubscriptions.go b/services/submitter/multinode/submitbeaconcommitteesubscriptions.go new file mode 100644 index 0000000..fed823c --- /dev/null +++ b/services/submitter/multinode/submitbeaconcommitteesubscriptions.go @@ -0,0 +1,87 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + "encoding/json" + "sync" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" +) + +// SubmitBeaconCommitteeSubscriptions submits a batch of beacon committee subscriptions. +func (s *Service) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*submitter.BeaconCommitteeSubscription) error { + if subscriptions == nil { + return errors.New("no subscriptions supplied") + } + + subs := make([]*eth2client.BeaconCommitteeSubscription, len(subscriptions)) + for i, subscription := range subscriptions { + subs[i] = ð2client.BeaconCommitteeSubscription{ + Slot: subscription.Slot, + CommitteeIndex: subscription.CommitteeIndex, + CommitteeSize: subscription.CommitteeSize, + ValidatorIndex: subscription.ValidatorIndex, + ValidatorPubKey: subscription.ValidatorPubKey, + Aggregate: subscription.Aggregate, + SlotSelectionSignature: subscription.Signature, + } + } + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, submitter := range s.beaconCommitteeSubscriptionSubmitters { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + name string, + submitter eth2client.BeaconCommitteeSubscriptionsSubmitter, + ) { + defer wg.Done() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + if err := submitter.SubmitBeaconCommitteeSubscriptions(ctx, subs); err != nil { + log.Warn().Str("submitter", name).Err(err).Msg("Failed to submit beacon committee subscription") + return + } + }(ctx, sem, &wg, name, submitter) + } + wg.Wait() + + if e := log.Trace(); e.Enabled() { + // Summary counts. + aggregating := 0 + for i := range subscriptions { + if subscriptions[i].Aggregate { + aggregating++ + } + } + + data, err := json.Marshal(subscriptions) + if err == nil { + e.Str("subscriptions", string(data)).Int("subscribing", len(subscriptions)).Int("aggregating", aggregating).Msg("Submitted subscriptions") + } + } + + return nil +} diff --git a/services/submitter/null/parameters.go b/services/submitter/null/parameters.go new file mode 100644 index 0000000..76a09c6 --- /dev/null +++ b/services/submitter/null/parameters.go @@ -0,0 +1,55 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package null is a submitter that does not submit requests. +package null + +import ( + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + return ¶meters, nil +} diff --git a/services/submitter/null/service.go b/services/submitter/null/service.go new file mode 100644 index 0000000..345b8a2 --- /dev/null +++ b/services/submitter/null/service.go @@ -0,0 +1,121 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package null + +import ( + "context" + "encoding/json" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is the submitter for signed items. +type Service struct{} + +// module-wide log. +var log zerolog.Logger + +// New creates a new submitter. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "submitter").Str("impl", "null").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{} + + return s, nil +} + +// SubmitBeaconBlock submits a block. +func (s *Service) SubmitBeaconBlock(ctx context.Context, block *spec.SignedBeaconBlock) error { + if block == nil { + return errors.New("no beacon block supplied") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(block) + if err == nil { + e.Str("block", string(data)).Msg("Not submitting beacon block") + } + } + + return nil +} + +// SubmitAttestation submits a beacon block attestation. +func (s *Service) SubmitAttestation(ctx context.Context, attestation *spec.Attestation) error { + if attestation == nil { + return errors.New("no attestation supplied") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(attestation) + if err == nil { + e.Str("attestation", string(data)).Msg("Not submitting attestation") + } + } + + return nil +} + +// SubmitBeaconCommitteeSubscriptions submits a batch of beacon committee subscriptions. +func (s *Service) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*submitter.BeaconCommitteeSubscription) error { + if subscriptions == nil { + return errors.New("no subscriptions supplied") + } + + if e := log.Trace(); e.Enabled() { + // Summary counts. + aggregating := 0 + for i := range subscriptions { + if subscriptions[i].Aggregate { + aggregating++ + } + } + + data, err := json.Marshal(subscriptions) + if err == nil { + e.Str("subscriptions", string(data)).Int("subscribing", len(subscriptions)).Int("aggregating", aggregating).Msg("Not submitting subscriptions") + } + } + + return nil +} + +// SubmitAggregateAttestation submits an aggregate attestation. +func (s *Service) SubmitAggregateAttestation(ctx context.Context, aggregate *spec.SignedAggregateAndProof) error { + if aggregate == nil { + return errors.New("no aggregate attestation supplied") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(aggregate) + if err == nil { + e.Str("attestation", string(data)).Msg("Not submitting aggregate attestation") + } + } + + return nil +} diff --git a/services/submitter/null/service_test.go b/services/submitter/null/service_test.go new file mode 100644 index 0000000..2fee69b --- /dev/null +++ b/services/submitter/null/service_test.go @@ -0,0 +1,68 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package null_test + +import ( + "context" + "testing" + + "github.com/attestantio/vouch/services/submitter" + "github.com/attestantio/vouch/services/submitter/null" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + tests := []struct { + name string + params []null.Parameter + }{ + { + name: "Good", + params: []null.Parameter{ + null.WithLogLevel(zerolog.Disabled), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := null.New(context.Background(), test.params...) + require.NoError(t, err) + }) + } +} + +func TestSubmit(t *testing.T) { + s, err := null.New(context.Background(), + null.WithLogLevel(zerolog.Disabled), + ) + require.NoError(t, err) + + require.EqualError(t, s.SubmitBeaconBlock(context.Background(), nil), "no beacon block supplied") + require.EqualError(t, s.SubmitAttestation(context.Background(), nil), "no attestation supplied") + require.EqualError(t, s.SubmitBeaconCommitteeSubscriptions(context.Background(), nil), "no subscriptions supplied") + require.EqualError(t, s.SubmitAggregateAttestation(context.Background(), nil), "no aggregate attestation supplied") +} + +func TestInterfaces(t *testing.T) { + s, err := null.New(context.Background(), + null.WithLogLevel(zerolog.Disabled), + ) + require.NoError(t, err) + require.Implements(t, (*submitter.BeaconBlockSubmitter)(nil), s) + require.Implements(t, (*submitter.AttestationSubmitter)(nil), s) + require.Implements(t, (*submitter.BeaconCommitteeSubscriptionsSubmitter)(nil), s) + require.Implements(t, (*submitter.AggregateAttestationSubmitter)(nil), s) +} diff --git a/services/submitter/service.go b/services/submitter/service.go new file mode 100644 index 0000000..7a2675c --- /dev/null +++ b/services/submitter/service.go @@ -0,0 +1,58 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package submitter + +import ( + "context" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// Service is the submitter service. +type Service interface{} + +// AttestationSubmitter is the interface for a submitter of attestations. +type AttestationSubmitter interface { + // SubmitAttestation submits an attestation. + SubmitAttestation(ctx context.Context, block *spec.Attestation) error +} + +// BeaconBlockSubmitter is the interface for a submitter of beacon blocks. +type BeaconBlockSubmitter interface { + // SubmitBeaconBlock submits a block. + SubmitBeaconBlock(ctx context.Context, block *spec.SignedBeaconBlock) error +} + +// BeaconCommitteeSubscription is a subscription for a particular beacon committee at a given time. +type BeaconCommitteeSubscription struct { + Slot uint64 + CommitteeIndex uint64 + CommitteeSize uint64 + ValidatorIndex uint64 + ValidatorPubKey []byte + Aggregate bool + Signature []byte +} + +// BeaconCommitteeSubscriptionsSubmitter is the interface for a submitter of beacon committee subscriptions. +type BeaconCommitteeSubscriptionsSubmitter interface { + // SubmitBeaconCommitteeSubscription submits a batch of beacon committee subscriptions. + SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*BeaconCommitteeSubscription) error +} + +// AggregateAttestationSubmitter is the interface for a submitter of aggregate attestations. +type AggregateAttestationSubmitter interface { + // SubmitAggregateAttestation submits an aggregate attestation. + SubmitAggregateAttestation(ctx context.Context, aggregateAttestation *spec.SignedAggregateAndProof) error +} diff --git a/strategies/beaconblockproposal/best/parameters.go b/strategies/beaconblockproposal/best/parameters.go new file mode 100644 index 0000000..65f68eb --- /dev/null +++ b/strategies/beaconblockproposal/best/parameters.go @@ -0,0 +1,104 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package best is a strategy that obtains beacon block proposals from multiple +// nodes and selects the best one based on its attestation load. +package best + +import ( + "context" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/metrics" + nullmetrics "github.com/attestantio/vouch/services/metrics/null" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + clientMonitor metrics.ClientMonitor + processConcurrency int64 + beaconBlockProposalProviders map[string]eth2client.BeaconBlockProposalProvider + timeout time.Duration +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithClientMonitor sets the client monitor for the service. +func WithClientMonitor(monitor metrics.ClientMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.clientMonitor = monitor + }) +} + +// WithProcessConcurrency sets the concurrency for the service. +func WithProcessConcurrency(concurrency int64) Parameter { + return parameterFunc(func(p *parameters) { + p.processConcurrency = concurrency + }) +} + +// WithBeaconBlockProposalProviders sets the beacon block proposal providers. +func WithBeaconBlockProposalProviders(providers map[string]eth2client.BeaconBlockProposalProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockProposalProviders = providers + }) +} + +// WithTimeout sets the timeout for beacon block proposal requests. +func WithTimeout(timeout time.Duration) Parameter { + return parameterFunc(func(p *parameters) { + p.timeout = timeout + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + timeout: 2 * time.Second, + clientMonitor: nullmetrics.New(context.Background()), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.processConcurrency == 0 { + return nil, errors.New("no process concurrency specified") + } + if parameters.beaconBlockProposalProviders == nil { + return nil, errors.New("no beacon block proposal providers specified") + } + + return ¶meters, nil +} diff --git a/strategies/beaconblockproposal/best/service.go b/strategies/beaconblockproposal/best/service.go new file mode 100644 index 0000000..3804221 --- /dev/null +++ b/strategies/beaconblockproposal/best/service.go @@ -0,0 +1,148 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package best + +import ( + "context" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/metrics" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + "golang.org/x/sync/semaphore" +) + +// Service is the provider for beacon block proposals. +type Service struct { + clientMonitor metrics.ClientMonitor + processConcurrency int64 + beaconBlockProposalProviders map[string]eth2client.BeaconBlockProposalProvider + timeout time.Duration +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new beacon block propsal strategy. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("strategy", "beaconblockproposal").Str("impl", "best").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + processConcurrency: parameters.processConcurrency, + beaconBlockProposalProviders: parameters.beaconBlockProposalProviders, + timeout: parameters.timeout, + clientMonitor: parameters.clientMonitor, + } + + return s, nil +} + +// BeaconBlockProposal provies the best beacon block proposal from a number of beacon nodes. +func (s *Service) BeaconBlockProposal(ctx context.Context, slot uint64, randaoReveal []byte, graffiti []byte) (*spec.BeaconBlock, error) { + var mu sync.Mutex + bestScore := float64(0) + var bestProposal *spec.BeaconBlock + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, provider := range s.beaconBlockProposalProviders { + wg.Add(1) + go func(ctx context.Context, sem *semaphore.Weighted, wg *sync.WaitGroup, name string, provider eth2client.BeaconBlockProposalProvider, mu *sync.Mutex) { + defer wg.Done() + + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + log := log.With().Str("provider", name).Uint64("slot", slot).Logger() + + opCtx, cancel := context.WithTimeout(ctx, s.timeout) + started := time.Now() + proposal, err := provider.BeaconBlockProposal(opCtx, slot, randaoReveal, graffiti) + s.clientMonitor.ClientOperation(name, "beacon block proposal", err == nil, time.Since(started)) + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain beacon block proposal") + cancel() + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained beacon block proposal") + cancel() + + mu.Lock() + score := scoreBeaconBlockProposal(ctx, name, slot, proposal) + if score > bestScore || bestProposal == nil { + bestScore = score + bestProposal = proposal + } + mu.Unlock() + }(ctx, sem, &wg, name, provider, &mu) + } + wg.Wait() + + return bestProposal, nil +} + +// scoreBeaconBlockPropsal generates a score for a beacon block. +// The score is relative to the reward expected by proposing the block. +func scoreBeaconBlockProposal(ctx context.Context, name string, slot uint64, blockProposal *spec.BeaconBlock) float64 { + immediateAttestationScore := float64(0) + attestationScore := float64(0) + + // Add attestation scores. + for _, attestation := range blockProposal.Body.Attestations { + inclusionDistance := float64(slot - attestation.Data.Slot) + attestationScore += float64(attestation.AggregationBits.Count()) / inclusionDistance + if inclusionDistance == 1 { + immediateAttestationScore += float64(attestation.AggregationBits.Count()) / inclusionDistance + } + } + + // Add slashing scores. + // Slashing reward will be at most MAX_EFFECTIVE_BALANCE/WHISTLEBLOWER_REWARD_QUOTIENT, + // which is 0.0625 Ether. + // Individual attestation reward at 16K validators will be around 90,000 GWei, or .00009 Ether. + // So we state that a single slashing event has the same weight as about 700 attestations. + slashingWeight := float64(700) + + // Add proposer slashing scores. + proposerSlashingScore := float64(len(blockProposal.Body.ProposerSlashings)) * slashingWeight + + // Add attester slashing scores. + attesterSlashingScore := float64(len(blockProposal.Body.AttesterSlashings)) * slashingWeight + + log.Trace(). + Uint64("slot", slot). + Str("provider", name). + Float64("immediate_attestations", immediateAttestationScore). + Float64("attestations", attestationScore). + Float64("proposer_slashings", proposerSlashingScore). + Float64("attester_slashings", attesterSlashingScore). + Float64("total", attestationScore+proposerSlashingScore+attesterSlashingScore). + Msg("Scored block") + + return attestationScore + proposerSlashingScore + attestationScore +} diff --git a/strategies/beaconblockproposal/first/parameters.go b/strategies/beaconblockproposal/first/parameters.go new file mode 100644 index 0000000..6e1d378 --- /dev/null +++ b/strategies/beaconblockproposal/first/parameters.go @@ -0,0 +1,93 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package best is a strategy that obtains beacon block proposals from multiple +// nodes and selects the best one based on its attestation load. +package best + +import ( + "context" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/metrics" + nullmetrics "github.com/attestantio/vouch/services/metrics/null" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + clientMonitor metrics.ClientMonitor + beaconBlockProposalProviders map[string]eth2client.BeaconBlockProposalProvider + timeout time.Duration +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithClientMonitor sets the client monitor for the service. +func WithClientMonitor(monitor metrics.ClientMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.clientMonitor = monitor + }) +} + +// WithBeaconBlockProposalProviders sets the beacon block proposal providers. +func WithBeaconBlockProposalProviders(providers map[string]eth2client.BeaconBlockProposalProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockProposalProviders = providers + }) +} + +// WithTimeout sets the timeout for beacon block proposal requests. +func WithTimeout(timeout time.Duration) Parameter { + return parameterFunc(func(p *parameters) { + p.timeout = timeout + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + timeout: 2 * time.Second, + clientMonitor: nullmetrics.New(context.Background()), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.beaconBlockProposalProviders == nil { + return nil, errors.New("no beacon block proposal providers specified") + } + + return ¶meters, nil +} diff --git a/strategies/beaconblockproposal/first/service.go b/strategies/beaconblockproposal/first/service.go new file mode 100644 index 0000000..cf27d1d --- /dev/null +++ b/strategies/beaconblockproposal/first/service.go @@ -0,0 +1,98 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package best + +import ( + "context" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/metrics" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is the provider for beacon block proposals. +type Service struct { + clientMonitor metrics.ClientMonitor + beaconBlockProposalProviders map[string]eth2client.BeaconBlockProposalProvider + timeout time.Duration +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new beacon block propsal strategy. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("strategy", "beaconblockproposal").Str("impl", "best").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + beaconBlockProposalProviders: parameters.beaconBlockProposalProviders, + timeout: parameters.timeout, + clientMonitor: parameters.clientMonitor, + } + + return s, nil +} + +// BeaconBlockProposal provies the best beacon block proposal from a number of beacon nodes. +func (s *Service) BeaconBlockProposal(ctx context.Context, slot uint64, randaoReveal []byte, graffiti []byte) (*spec.BeaconBlock, error) { + // We create a cancelable context with a timeout. As soon as the first provider has responded we + // cancel the context to cancel the other requests. + ctx, cancel := context.WithTimeout(ctx, s.timeout) + + proposalCh := make(chan *spec.BeaconBlock, 1) + for name, provider := range s.beaconBlockProposalProviders { + go func(ctx context.Context, name string, provider eth2client.BeaconBlockProposalProvider, ch chan *spec.BeaconBlock) { + log := log.With().Str("provider", name).Uint64("slot", slot).Logger() + + started := time.Now() + proposal, err := provider.BeaconBlockProposal(ctx, slot, randaoReveal, graffiti) + s.clientMonitor.ClientOperation(name, "beacon block proposal", err == nil, time.Since(started)) + cancel() + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain beacon block proposal") + return + } + if proposal == nil { + log.Warn().Err(err).Msg("Returned empty beacon block proposal") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained beacon block proposal") + + ch <- proposal + }(ctx, name, provider, proposalCh) + } + + select { + case <-ctx.Done(): + cancel() + log.Warn().Msg("Failed to obtain beacon block proposal before timeout") + return nil, errors.New("failed to obtain beacon block proposal before timeout") + case proposal := <-proposalCh: + cancel() + return proposal, nil + } +}