Initial release

This commit is contained in:
Jim McDonald 2020-09-28 07:46:00 +01:00
parent 804389ddef
commit bee97962e7
No known key found for this signature in database
GPG Key ID: 89CEB61B2AD2A5E7
94 changed files with 12225 additions and 178 deletions

23
.dockerignore Normal file
View File

@ -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

23
.github/workflows/golangci-lint.yml vendored Normal file
View File

@ -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

115
.github/workflows/release.yml vendored Normal file
View File

@ -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

19
Dockerfile Normal file
View File

@ -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"]

201
LICENSE Normal file
View File

@ -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.

View File

@ -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.

51
clients.go Normal file
View File

@ -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
}

82
docs/accountmanager.md Normal file
View File

@ -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.

77
docs/configuration.md Normal file
View File

@ -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_<MODULE>_LOG_LEVEL` or the configuration option `<module>.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`.

38
docs/getting_started.md Normal file
View File

@ -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.

36
docs/graffiti.md Normal file
View File

@ -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.

48
docs/majordomo.md Normal file
View File

@ -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
```

View File

@ -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.

51
go.mod
View File

@ -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

709
go.sum
View File

@ -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=

38
loggers/jaeger.go Normal file
View File

@ -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...)
}

71
logging.go Normal file
View File

@ -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()
}
}

624
main.go
View File

@ -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
}

1022
media/architecture.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 64 KiB

216
mock/accountmanager.go Normal file
View File

@ -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
}

125
mock/eth2client.go Normal file
View File

@ -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
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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,
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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,
}
}
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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 := &ethpb.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")
}

View File

@ -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)
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -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
}
}
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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(&parameters)
}
}
if parameters.majordomo == nil {
return nil, errors.New("no majordomo specified")
}
if parameters.location == "" {
return nil, errors.New("no location specified")
}
return &parameters, nil
}

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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)
}

View File

@ -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(&parameters)
}
}
if len(parameters.graffiti) > 32 {
return nil, errors.New("graffiti has a mximum size of 32 bytes")
}
return &parameters, nil
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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) {
}

View File

@ -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))
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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())
}

View File

@ -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(&parameters)
}
}
if parameters.address == "" {
return nil, errors.New("no address specified")
}
return &parameters, nil
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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(&parameters)
}
}
if parameters.monitor == nil {
return nil, errors.New("no monitor specified")
}
return &parameters, nil
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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] = &eth2client.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
}

View File

@ -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)
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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] = &eth2client.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
}

View File

@ -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(&parameters)
}
}
return &parameters, nil
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -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
}

View File

@ -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(&parameters)
}
}
if parameters.beaconBlockProposalProviders == nil {
return nil, errors.New("no beacon block proposal providers specified")
}
return &parameters, nil
}

View File

@ -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
}
}