mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac18cbab3e | ||
|
|
2f1c89d0a6 | ||
|
|
a3ad4181d3 | ||
|
|
f8ac23e8d7 | ||
|
|
b6815d1a2a | ||
|
|
a79b813bd0 | ||
|
|
ad971145f0 | ||
|
|
602948921c | ||
|
|
607e969a30 | ||
|
|
79f1ae9930 | ||
|
|
a98f681f98 | ||
|
|
e0e1f697d3 | ||
|
|
1b70a66120 | ||
|
|
94eba96a6e | ||
|
|
f052d8e307 | ||
|
|
df45686828 | ||
|
|
84d228877a | ||
|
|
b2b26742b0 | ||
|
|
9dc630c809 | ||
|
|
452430db56 | ||
|
|
b0d676a734 | ||
|
|
ff73470085 | ||
|
|
a41349999f | ||
|
|
004f4bc41a | ||
|
|
64c8e1a051 | ||
|
|
d95d48f6b2 | ||
|
|
3e702f0c51 | ||
|
|
2e36fcc3ce | ||
|
|
aa0cda306b | ||
|
|
aa79f83f35 | ||
|
|
8de7e75c77 | ||
|
|
4a1b419c0e | ||
|
|
b6a08d5073 | ||
|
|
65d2ab5d53 | ||
|
|
34b03f9d53 | ||
|
|
dca513b8c9 | ||
|
|
446941be92 | ||
|
|
b76cdb01d1 | ||
|
|
ce5b250ef0 | ||
|
|
2c4ccf62af | ||
|
|
c7ad5194e6 | ||
|
|
ddb866131b | ||
|
|
49fb03aa3a | ||
|
|
1ed3a51117 | ||
|
|
4d5660ccbb | ||
|
|
7596d271ad | ||
|
|
943f9350f3 | ||
|
|
07863846e6 | ||
|
|
cc59ab618d | ||
|
|
9794949e8a | ||
|
|
5c741d2b27 | ||
|
|
52c76deb5e | ||
|
|
c986118f16 | ||
|
|
df6694e3b7 | ||
|
|
a55ad238e6 | ||
|
|
be21db030e | ||
|
|
16488c8a40 | ||
|
|
a7489aa675 | ||
|
|
b1647d2f3d | ||
|
|
c7f3275dfa | ||
|
|
7aeba43338 | ||
|
|
688db9ef8c | ||
|
|
173883da3e | ||
|
|
6077e04619 | ||
|
|
95c57363a2 | ||
|
|
9b96b4e34f | ||
|
|
40ba1987cd | ||
|
|
9c08c0a1a4 | ||
|
|
b2360fa2f6 | ||
|
|
e7cc6ce18b | ||
|
|
a83e206c89 | ||
|
|
947dbdaef6 | ||
|
|
ac87f51047 | ||
|
|
b30db1b6c7 | ||
|
|
405b2d66de | ||
|
|
757a5e1492 | ||
|
|
0b7a24df6e | ||
|
|
e042be75ce | ||
|
|
eaf7e34baf | ||
|
|
3b086dd588 | ||
|
|
cbd8cbbf38 | ||
|
|
7391dbe6fb | ||
|
|
3dd1bab526 | ||
|
|
93e632972a | ||
|
|
5a385c3c23 | ||
|
|
d701cd032a | ||
|
|
224059ba8e | ||
|
|
1a5234e39f | ||
|
|
3cbc27f53d | ||
|
|
a80a1707cf | ||
|
|
290ceb3f0d | ||
|
|
136e2fe9ba | ||
|
|
4b6ea09555 | ||
|
|
508e2eafcb | ||
|
|
6fc581edc7 | ||
|
|
2f1f2e5da0 | ||
|
|
4600f2a0d4 | ||
|
|
58bc417f52 | ||
|
|
65af8f3cde | ||
|
|
7e1aa10f60 | ||
|
|
623f3c89ad | ||
|
|
628e3113b2 | ||
|
|
aa27a0c1f4 | ||
|
|
0a90ae9e97 | ||
|
|
d10b7f2739 | ||
|
|
5f4be0415f | ||
|
|
0f1c6f09bd | ||
|
|
6118f9cab8 | ||
|
|
829dbd3bf2 | ||
|
|
f0ad10463e | ||
|
|
3d0dab0b95 | ||
|
|
5abfabc355 | ||
|
|
e84b600d5d | ||
|
|
e64a46f126 | ||
|
|
0746fa3048 | ||
|
|
94eb3fbca7 | ||
|
|
48d63398d4 | ||
|
|
4e1f47e187 | ||
|
|
85c7c7fc55 | ||
|
|
05406e8d81 | ||
|
|
70d451cea5 | ||
|
|
c270d7a2f7 | ||
|
|
115d037948 | ||
|
|
b34a633e53 | ||
|
|
d1b989a711 | ||
|
|
8078359eab | ||
|
|
7de8dc7424 | ||
|
|
987dbd26c6 | ||
|
|
1f0aeac9b4 | ||
|
|
37cab43242 | ||
|
|
6f94e599ac | ||
|
|
86bc3d5418 | ||
|
|
8cc45f0b4b | ||
|
|
77df1bba43 | ||
|
|
1d4e52cb49 | ||
|
|
c1e124fbd3 | ||
|
|
91bc6ec86d | ||
|
|
cd55abc9bf | ||
|
|
44478e147e | ||
|
|
61b73b0f61 | ||
|
|
5ce7491d73 | ||
|
|
b5ecf56382 | ||
|
|
0acb57df48 | ||
|
|
325baf9f8e | ||
|
|
1c2385e413 | ||
|
|
c06a709463 | ||
|
|
735cc3ae4b | ||
|
|
2da00b3930 | ||
|
|
f5db041ad8 | ||
|
|
1d94b90013 | ||
|
|
ff6a95d6a9 | ||
|
|
1b7e867251 | ||
|
|
e7918125b9 | ||
|
|
6dd96cf916 | ||
|
|
0b2ef25cc4 | ||
|
|
834e3310c5 | ||
|
|
fc9c4cfe0c | ||
|
|
f2bb5e0d51 | ||
|
|
f5fc8b363d | ||
|
|
a31509f7d6 | ||
|
|
4b451ec2fa | ||
|
|
a8fee14b89 | ||
|
|
9cfb1b5637 | ||
|
|
df1724f763 | ||
|
|
f1a586ca56 | ||
|
|
b9cb926662 | ||
|
|
220cc21356 | ||
|
|
5bc7c1a26c | ||
|
|
4c3237cd0d | ||
|
|
7215e04a69 | ||
|
|
71e9c0471b | ||
|
|
1a6f402fb8 | ||
|
|
b9297c2506 | ||
|
|
1032789706 | ||
|
|
51e289b72b | ||
|
|
a75a350cef | ||
|
|
aa34e61a80 | ||
|
|
3f33f04be2 | ||
|
|
5733b5b638 | ||
|
|
e1ce81c81d | ||
|
|
bd67ba0307 | ||
|
|
12bb5a7ab8 | ||
|
|
d57dbbf104 | ||
|
|
6482b4add6 | ||
|
|
b672e83470 | ||
|
|
710e891844 | ||
|
|
9e3bf521a0 | ||
|
|
3ca899b832 | ||
|
|
d221c0544c | ||
|
|
eed07a39a3 | ||
|
|
5a5edacd11 | ||
|
|
c7f4cb0ca5 | ||
|
|
fbc2171053 | ||
|
|
57946f1552 | ||
|
|
b487bb042c | ||
|
|
0c8029c950 | ||
|
|
b3bbeea3fb | ||
|
|
09105549b1 | ||
|
|
364c764de2 | ||
|
|
3f1fe30959 | ||
|
|
aae360ccb8 | ||
|
|
32a3e4649d | ||
|
|
dd3bd121fe | ||
|
|
edf84f47ba | ||
|
|
320c5ee9ab | ||
|
|
36a1674549 | ||
|
|
693b2a6961 | ||
|
|
e5481f9074 | ||
|
|
c89712699c | ||
|
|
7130855b73 | ||
|
|
d505966a42 | ||
|
|
732c07238a | ||
|
|
b843d0077e | ||
|
|
f11cf0cbf5 | ||
|
|
ea43e71e60 | ||
|
|
71b01ee2aa | ||
|
|
de349f5691 | ||
|
|
b1d2e94854 | ||
|
|
2293079861 | ||
|
|
69867ba21c | ||
|
|
f72af8247a | ||
|
|
9ca4406a80 | ||
|
|
1ad82adf80 | ||
|
|
11eb440df2 | ||
|
|
e2192f6992 |
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
ethdo
|
||||
|
||||
# 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
23
.github/workflows/golangci-lint.yml
vendored
Normal 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@v2
|
||||
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: --timeout=10m
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
156
.github/workflows/release.yml
vendored
Normal file
156
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
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.16
|
||||
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 "GO111MODULE=on" >> $GITHUB_ENV
|
||||
# Release tag comes from the github reference.
|
||||
RELEASE_TAG=$(echo ${GITHUB_REF} | sed -e 's!.*/!!')
|
||||
echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV
|
||||
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 "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV
|
||||
echo "::set-output name=RELEASE_VERSION::${RELEASE_VERSION}"
|
||||
|
||||
- name: Build
|
||||
run: go build -v -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" .
|
||||
|
||||
- name: Test
|
||||
run: go test -v .
|
||||
|
||||
- name: Fetch xgo
|
||||
run: |
|
||||
go install github.com/wealdtech/xgo@latest
|
||||
|
||||
- name: Cross-compile linux
|
||||
run: |
|
||||
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" --targets="linux/amd64" github.com/wealdtech/ethdo
|
||||
|
||||
- name: Cross-compile windows
|
||||
run: |
|
||||
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION} -s -w -extldflags -static" --targets="windows/amd64" github.com/wealdtech/ethdo
|
||||
|
||||
- name: Create windows release files
|
||||
run: |
|
||||
mv ethdo-windows-4.0-amd64.exe ethdo.exe
|
||||
zip --junk-paths ethdo-${RELEASE_VERSION}-windows-exe.zip ethdo.exe
|
||||
sha256sum ethdo-${RELEASE_VERSION}-windows-exe.zip >ethdo-${RELEASE_VERSION}-windows.sha256
|
||||
|
||||
- name: Create linux AMD64 tgz file
|
||||
run: |
|
||||
mv ethdo-linux-amd64 ethdo
|
||||
tar zcf ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz ethdo
|
||||
sha256sum ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz >ethdo-${RELEASE_VERSION}-linux-amd64.sha256
|
||||
|
||||
# - name: Create linux ARM64 tgz file
|
||||
# run: |
|
||||
# mv ethdo-linux-arm64 ethdo
|
||||
# tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
|
||||
# sha256sum ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
|
||||
|
||||
- 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: true
|
||||
prerelease: false
|
||||
|
||||
- name: Upload windows checksum file
|
||||
id: upload-release-asset-windows-checksum
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-windows.sha256
|
||||
asset_name: ethdo-${{ env.RELEASE_VERSION }}-windows.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
- 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: ./ethdo-${{ env.RELEASE_VERSION }}-windows-exe.zip
|
||||
asset_name: ethdo-${{ env.RELEASE_VERSION }}-windows-exe.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload linux AMD64 checksum file
|
||||
id: upload-release-asset-linux-amd64-checksum
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.sha256
|
||||
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
- 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: ./ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
|
||||
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
|
||||
# - name: Upload linux ARM64 checksum file
|
||||
# id: upload-release-asset-linux-arm64-checksum
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
|
||||
# asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
|
||||
# asset_content_type: text/plain
|
||||
|
||||
# - 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: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
|
||||
# asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
|
||||
# asset_content_type: application/gzip
|
||||
83
CHANGELOG.md
Normal file
83
CHANGELOG.md
Normal file
@@ -0,0 +1,83 @@
|
||||
1.14.0:
|
||||
- add "chain verify signedcontributionandproof"
|
||||
- show both block and body root in "block info"
|
||||
- add exit / withdrawable epoch to "validator info"
|
||||
|
||||
1.13.0:
|
||||
- rework and provide additional information to "chain status" output
|
||||
|
||||
1.12.0:
|
||||
- add "synccommittee members"
|
||||
|
||||
1.11.0
|
||||
- add Altair information to "block info"
|
||||
- add more information to "chain info"
|
||||
|
||||
1.10.2
|
||||
- use local shamir code (copied from github.com/hashicorp/vault)
|
||||
|
||||
1.10.0
|
||||
- add "wallet sharedexport" and "wallet sharedimport"
|
||||
|
||||
1.9.1
|
||||
- Avoid crash when required interfaces for chain status command are not supported
|
||||
- Avoid crash with latest version of herumi/go-bls
|
||||
|
||||
1.9.0
|
||||
- allow use of Ethereum 1 address as withdrawal credentials
|
||||
|
||||
1.8.1
|
||||
- fix issue where 'attester duties' and 'attester inclusion' could crash
|
||||
|
||||
1.8.0
|
||||
- add "chain time"
|
||||
- add "validator keycheck"
|
||||
|
||||
1.7.5:
|
||||
- add "slot time"
|
||||
- add "attester duties"
|
||||
- add "node events"
|
||||
- add activation epoch to "validator info"
|
||||
|
||||
1.7.3:
|
||||
- fix issue where base directory was ignored for wallet creation
|
||||
- new "validator duties" command to display known duties for a given validator
|
||||
- update go-eth2-client to display correct validator status from prysm
|
||||
|
||||
1.7.2:
|
||||
- new "account derive" command to derive keys directly from a mnemonic and derivation path
|
||||
- add more output to "deposit verify" to explain operation
|
||||
|
||||
1.7.1:
|
||||
- fix "store not set" issue
|
||||
|
||||
1.7.0:
|
||||
- "validator depositdata" now defaults to mainnet, does not silently fetch fork version from chain
|
||||
- update deposit data output to version 3, to allow for better deposit checking
|
||||
- use go-eth2-client for beacon node communications
|
||||
- deprecated "--basedir" in favor of "--base-dir"
|
||||
- deprecated "--storepassphrase" in favor of "--store-passphrase"
|
||||
- deprecated "--walletpassphrsae" in favor of "--wallet-passphrsae"
|
||||
- renamed "--exportpassphrase" and "--importpassphrase" flags to "--passphrase"
|
||||
- reworked internal structure of account-related commands
|
||||
- reject weak passphrases by default
|
||||
|
||||
1.6.1:
|
||||
- "attester inclusion" defaults to previous epoch
|
||||
- output array for launchpad deposit data JSON in all situations
|
||||
|
||||
1.6.0:
|
||||
- update BLS HKDF function to match spec 04
|
||||
- add --launchpad option to "validator depositdata" to output data in launchpad format
|
||||
|
||||
1.5.9:
|
||||
- fix issue where wallet mnemonics were not normalised to NFKD
|
||||
- "block info" supports fetching the gensis block (--slot=0)
|
||||
- "attester inclusion" command finds the inclusion slot for a validator's attestation
|
||||
- "account info" with verbose option now displays participants for distributed accounts
|
||||
- fix issue where distributed account generation without a passphrase was not allowed
|
||||
|
||||
1.5.8:
|
||||
- allow raw deposit transactions to be supplied to "deposit verify"
|
||||
- move functionality of "account withdrawalcredentials" to be part of "account info"
|
||||
- add genesis validators root to "chain info"
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM golang:1.16-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/ethdo /app
|
||||
|
||||
ENTRYPOINT ["/app/ethdo"]
|
||||
85
README.md
85
README.md
@@ -5,11 +5,12 @@
|
||||
|
||||
A command-line tool for managing common tasks in Ethereum 2.
|
||||
|
||||
** Please note that this library uses standards that are not yet final, and as such may result in changes that alter public and private keys. Do not use this library for production use just yet **
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Install](#install)
|
||||
- [Binaries](#binaries)
|
||||
- [Docker](#docker)
|
||||
- [Source](#source)
|
||||
- [Usage](#usage)
|
||||
- [Maintainers](#maintainers)
|
||||
- [Contribute](#contribute)
|
||||
@@ -17,27 +18,77 @@ A command-line tool for managing common tasks in Ethereum 2.
|
||||
|
||||
## Install
|
||||
|
||||
### Binaries
|
||||
|
||||
Binaries for the latest version of `ethdo` can be obtained from [the releases page](https://github.com/wealdtech/ethdo/releases).
|
||||
|
||||
### Docker
|
||||
|
||||
You can obtain the latest version of `ethdo` using docker with:
|
||||
|
||||
```
|
||||
docker pull wealdtech/ethdo
|
||||
```
|
||||
|
||||
### Source
|
||||
`ethdo` is a standard Go program which can be installed with:
|
||||
|
||||
```sh
|
||||
GO111MODULE=on go get github.com/wealdtech/ethdo
|
||||
```
|
||||
|
||||
Note that `ethdo` requires at least version 1.13 of go to operate. The version of go can be found with `go version`.
|
||||
|
||||
If this does not work please see the [troubleshooting](https://github.com/wealdtech/ethdo/blob/master/docs/troubleshooting.md) page.
|
||||
|
||||
The docker image can be build locally with:
|
||||
|
||||
```sh
|
||||
docker build -t ethdo .
|
||||
```
|
||||
|
||||
You can run `ethdo` using docker after that. Example:
|
||||
|
||||
```sh
|
||||
docker run -it ethdo --help
|
||||
```
|
||||
|
||||
Note that that many `ethdo` commands connect to the beacon node to obtain information. If the beacon node is running directly on the server this requires the `--network=host` command, for example:
|
||||
|
||||
```sh
|
||||
docker run --network=host ethdo chain status
|
||||
```
|
||||
|
||||
Alternatively, if the beacon node is running in a separate docker container a shared network can be created with `docker network create eth2` and accessed by adding `--network=eth2` added to both the beacon node and `ethdo` containers.
|
||||
|
||||
## Usage
|
||||
|
||||
ethdo contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
|
||||
|
||||
### Wallets and accounts
|
||||
|
||||
ethdo uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types.
|
||||
ethdo uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types. When on the filesystem the locations of the created wallets and accounts are:
|
||||
|
||||
- for Linux: $HOME/.config/ethereum2/wallets
|
||||
- for OSX: $HOME/Library/Application Support/ethereum2/wallets
|
||||
- for Windows: %APPDATA%\ethereum2\wallets
|
||||
|
||||
If using the filesystem store, the additional parameter `basedir` can be supplied to change this location.
|
||||
|
||||
> If using docker as above you can make this directory accessible to docker to make wallets and accounts persistent. For example, for linux you could use the following command to list your wallets on Linux:
|
||||
>
|
||||
> ```
|
||||
> docker run -v $HOME/.config/ethereum2/wallets:/data ethdo --basedir=/data wallet list
|
||||
> ```
|
||||
>
|
||||
> This will allow you to use `ethdo` with or without docker, with the same location for wallets and accounts.
|
||||
|
||||
All ethdo comands take the following parameters:
|
||||
|
||||
- `store`: the name of the storage system for wallets. This can be one of "filesystem" or "s3", and defaults to "filesystem"
|
||||
- `store`: the name of the storage system for wallets. This can be one of "filesystem" (for local storage of the wallet) or "s3" (for remote storage of the wallet on [Amazon's S3](https://aws.amazon.com/s3/) storage system), and defaults to "filesystem"
|
||||
- `storepassphrase`: the passphrase for the store. If this is empty the store is unencrypted
|
||||
- `walletpassphrase`: the passphrase for the wallet. This is required for some wallet-centric operations such as creating new accounts
|
||||
- `accountpassphrase`: the passphrase for the account. This is required for some account-centric operations such as signing data
|
||||
- `passphrase`: the passphrase for the account. This is required for some account-centric operations such as signing data
|
||||
|
||||
Accounts are specified in the standard "<wallet>/<account>" format, for example the account "savings" in the wallet "primary" would be referenced as "primary/savings".
|
||||
|
||||
@@ -72,14 +123,38 @@ If set, the `--debug` argument will output additional information about the oper
|
||||
|
||||
Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command.
|
||||
|
||||
## Passphrase strength
|
||||
|
||||
`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue.
|
||||
|
||||
If a weak passphrase is required, `ethdo` can be supplied with the `--allow-weak-passphrases` option which will force it to accept any passphrase, even if it is considered weak.
|
||||
|
||||
## Rules for account passphrases
|
||||
|
||||
Account passphrases are used in various places in `ethdo`. Where they are used, the following rules apply:
|
||||
|
||||
- commands that require passphrases to operate, for example unlocking an account, can be supplied with multiple passphrases. If they are, then each passphrase is tried until one succeeds or they all fail
|
||||
- commands that require passphrases to create, for example creating an account, must be supplied with a single passphrase. If more than one passphrase is supplied the command will fail
|
||||
|
||||
In addition, the following rules apply to passphrases supplied on the command line:
|
||||
|
||||
- passphrases **must not** start with `0x`
|
||||
- passphrases **must not** contain the comma (,) character
|
||||
|
||||
# Commands
|
||||
|
||||
Command information, along with sample outputs and optional arguments, is available in [the usage section](https://github.com/wealdtech/ethdo/blob/master/docs/usage.md).
|
||||
|
||||
# HOWTO
|
||||
|
||||
There is a [HOWTO](https://github.com/wealdtech/ethdo/blob/master/docs/howto.md) that covers details about how to carry out various common tasks. There is also a specific document that provides details of how to carry out [common conversions](docs/conversions.md) from mnemonic, to account, to deposit data, for launchpad-related configurations.
|
||||
|
||||
## Maintainers
|
||||
|
||||
Jim McDonald: [@mcdee](https://github.com/mcdee).
|
||||
|
||||
Special thanks to [@SuburbanDad](https://github.com/SuburbanDad) for updating xgo to allow for cross-compilation of `ethdo` releaes.
|
||||
|
||||
## Contribute
|
||||
|
||||
Contributions welcome. Please check out [the issues](https://github.com/wealdtech/ethdo/issues).
|
||||
|
||||
96
cmd/account/create/input.go
Normal file
96
cmd/account/create/input.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
timeout time.Duration
|
||||
// For all accounts.
|
||||
wallet e2wtypes.Wallet
|
||||
accountName string
|
||||
passphrase string
|
||||
walletPassphrase string
|
||||
// For distributed accounts.
|
||||
participants uint32
|
||||
signingThreshold uint32
|
||||
// For pathed accounts.
|
||||
path string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
var err error
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
|
||||
// Account name.
|
||||
if viper.GetString("account") == "" {
|
||||
return nil, errors.New("account is required")
|
||||
}
|
||||
_, data.accountName, err = e2wallet.WalletAndAccountNames(viper.GetString("account"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account name")
|
||||
}
|
||||
if data.accountName == "" {
|
||||
return nil, errors.New("account name is required")
|
||||
}
|
||||
|
||||
// Wallet.
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
data.wallet, err = util.WalletFromInput(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain wallet")
|
||||
}
|
||||
|
||||
// Passphrase.
|
||||
data.passphrase, err = util.GetOptionalPassphrase()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain passphrase")
|
||||
}
|
||||
|
||||
// Wallet passphrase.
|
||||
data.walletPassphrase = util.GetWalletPassphrase()
|
||||
|
||||
// Participants.
|
||||
if viper.GetInt32("participants") == 0 {
|
||||
return nil, errors.New("participants must be at least one")
|
||||
}
|
||||
data.participants = viper.GetUint32("participants")
|
||||
|
||||
// Signing threshold.
|
||||
if viper.GetInt32("signing-threshold") == 0 {
|
||||
return nil, errors.New("signing threshold must be at least one")
|
||||
}
|
||||
data.signingThreshold = viper.GetUint32("signing-threshold")
|
||||
|
||||
// Path.
|
||||
data.path = viper.GetString("path")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
161
cmd/account/create/input_internal_test.go
Normal file
161
cmd/account/create/input_internal_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "WalletUnknown",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Unknown/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain wallet: wallet not found",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "account is required",
|
||||
},
|
||||
{
|
||||
name: "AccountWalletOnly",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"account": "Test wallet/",
|
||||
},
|
||||
err: "account name is required",
|
||||
},
|
||||
{
|
||||
name: "AccountMalformed",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "//",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain account name: invalid account format",
|
||||
},
|
||||
{
|
||||
name: "MultiplePassphrases",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": []string{"ce%NohGhah4ye5ra", "other"},
|
||||
"participants": 3,
|
||||
"signing-threshold": 2,
|
||||
},
|
||||
err: "failed to obtain passphrase: multiple passphrases supplied",
|
||||
},
|
||||
{
|
||||
name: "ParticipantsZero",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"participants": 0,
|
||||
"signing-threshold": 2,
|
||||
},
|
||||
err: "participants must be at least one",
|
||||
},
|
||||
{
|
||||
name: "SigningThresholdZero",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"participants": 3,
|
||||
"signing-threshold": 0,
|
||||
},
|
||||
err: "signing threshold must be at least one",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"participants": 3,
|
||||
"signing-threshold": 2,
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
accountName: "Test account",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
participants: 3,
|
||||
signingThreshold: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
// Cannot compare accounts directly, so need to check each element individually.
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
require.Equal(t, test.res.accountName, res.accountName)
|
||||
require.Equal(t, test.res.passphrase, res.passphrase)
|
||||
require.Equal(t, test.res.participants, res.participants)
|
||||
require.Equal(t, test.res.signingThreshold, res.signingThreshold)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
45
cmd/account/create/output.go
Normal file
45
cmd/account/create/output.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
account e2wtypes.Account
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
if data.account == nil {
|
||||
return "", errors.New("no account")
|
||||
}
|
||||
|
||||
if pubKeyProvider, ok := data.account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
|
||||
return fmt.Sprintf("%#x", pubKeyProvider.CompositePublicKey().Marshal()), nil
|
||||
}
|
||||
|
||||
if pubKeyProvider, ok := data.account.(e2wtypes.AccountPublicKeyProvider); ok {
|
||||
return fmt.Sprintf("%#x", pubKeyProvider.PublicKey().Marshal()), nil
|
||||
}
|
||||
|
||||
return "", errors.New("no public key available")
|
||||
}
|
||||
113
cmd/account/create/output_internal_test.go
Normal file
113
cmd/account/create/output_internal_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
distributed "github.com/wealdtech/go-eth2-wallet-distributed"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func hexToBytes(input string) []byte {
|
||||
res, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test", scratch.New(), keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
distributedWallet, err := distributed.CreateWallet(context.Background(), "Test distributed", scratch.New(), keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, distributedWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
distributed0, err := distributedWallet.(e2wtypes.WalletDistributedAccountImporter).ImportDistributedAccount(context.Background(),
|
||||
"Distributed 0",
|
||||
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
2,
|
||||
[][]byte{
|
||||
hexToBytes("0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268"),
|
||||
hexToBytes("0xaec922bd7a9b7b1dc21993133b586b0c3041c1e2e04b513e862227b9d7aecaf9444222f7e78282a449622ffc6278915d"),
|
||||
},
|
||||
map[uint64]string{
|
||||
1: "localhost-1:12345",
|
||||
2: "localhost-2:12345",
|
||||
3: "localhost-3:12345",
|
||||
},
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "AccountNil",
|
||||
dataOut: &dataOut{},
|
||||
err: "no account",
|
||||
},
|
||||
{
|
||||
name: "Account",
|
||||
dataOut: &dataOut{
|
||||
account: interop0,
|
||||
},
|
||||
res: "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
{
|
||||
name: "DistributedAccount",
|
||||
dataOut: &dataOut{
|
||||
account: distributed0,
|
||||
},
|
||||
res: "0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
147
cmd/account/create/process.go
Normal file
147
cmd/account/create/process.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if data.passphrase != "" && !util.AcceptablePassphrase(data.passphrase) {
|
||||
return nil, errors.New("supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag")
|
||||
}
|
||||
locker, isLocker := data.wallet.(e2wtypes.WalletLocker)
|
||||
if isLocker {
|
||||
if err := locker.Unlock(ctx, []byte(data.walletPassphrase)); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unlock wallet")
|
||||
}
|
||||
defer func() {
|
||||
if err := locker.Lock(ctx); err != nil {
|
||||
util.Log.Trace().Err(err).Msg("Failed to lock wallet")
|
||||
}
|
||||
}()
|
||||
}
|
||||
if data.participants == 0 {
|
||||
return nil, errors.New("participants is required")
|
||||
}
|
||||
|
||||
// Create style of account based on input.
|
||||
switch {
|
||||
case data.participants > 1:
|
||||
return processDistributed(ctx, data)
|
||||
case data.path != "":
|
||||
return processPathed(ctx, data)
|
||||
default:
|
||||
return processStandard(ctx, data)
|
||||
}
|
||||
}
|
||||
|
||||
func processStandard(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if data.passphrase == "" {
|
||||
return nil, errors.New("passphrase is required")
|
||||
}
|
||||
|
||||
results := &dataOut{}
|
||||
|
||||
creator, isCreator := data.wallet.(e2wtypes.WalletAccountCreator)
|
||||
if !isCreator {
|
||||
return nil, errors.New("wallet does not support account creation")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
account, err := creator.CreateAccount(ctx, data.accountName, []byte(data.passphrase))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account")
|
||||
}
|
||||
results.account = account
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func processPathed(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if data.passphrase == "" {
|
||||
return nil, errors.New("passphrase is required")
|
||||
}
|
||||
match, err := regexp.Match("^m/[0-9]+/[0-9]+(/[0-9+])+", []byte(data.path))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to match path to regular expression")
|
||||
}
|
||||
if !match {
|
||||
return nil, errors.New("path does not match expected format m/…")
|
||||
}
|
||||
|
||||
results := &dataOut{}
|
||||
|
||||
creator, isCreator := data.wallet.(e2wtypes.WalletPathedAccountCreator)
|
||||
if !isCreator {
|
||||
return nil, errors.New("wallet does not support account creation with an explicit path")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
account, err := creator.CreatePathedAccount(ctx, data.path, data.accountName, []byte(data.passphrase))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account")
|
||||
}
|
||||
results.account = account
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func processDistributed(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if data.signingThreshold == 0 {
|
||||
return nil, errors.New("signing threshold required")
|
||||
}
|
||||
if data.signingThreshold <= data.participants/2 {
|
||||
return nil, errors.New("signing threshold must be more than half the number of participants")
|
||||
}
|
||||
if data.signingThreshold > data.participants {
|
||||
return nil, errors.New("signing threshold cannot be higher than the number of participants")
|
||||
}
|
||||
|
||||
results := &dataOut{}
|
||||
|
||||
creator, isCreator := data.wallet.(e2wtypes.WalletDistributedAccountCreator)
|
||||
if !isCreator {
|
||||
return nil, errors.New("wallet does not support distributed account creation")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
account, err := creator.CreateDistributedAccount(ctx,
|
||||
data.accountName,
|
||||
data.participants,
|
||||
data.signingThreshold,
|
||||
[]byte(data.passphrase))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account")
|
||||
}
|
||||
results.account = account
|
||||
return results, nil
|
||||
}
|
||||
315
cmd/account/create/process_internal_test.go
Normal file
315
cmd/account/create/process_internal_test.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/attestantio/dirk/testing/daemon"
|
||||
"github.com/attestantio/dirk/testing/resources"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
dirk "github.com/wealdtech/go-eth2-wallet-dirk"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
hd "github.com/wealdtech/go-eth2-wallet-hd/v2"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
testNDWallet, err := nd.CreateWallet(context.Background(),
|
||||
"Test",
|
||||
scratch.New(),
|
||||
keystorev4.New(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
testHDWallet, err := hd.CreateWallet(context.Background(),
|
||||
"Test",
|
||||
[]byte("pass"),
|
||||
scratch.New(),
|
||||
keystorev4.New(),
|
||||
[]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// #nosec G404
|
||||
port1 := uint32(12000 + rand.Intn(4000))
|
||||
// #nosec G404
|
||||
port2 := uint32(12000 + rand.Intn(4000))
|
||||
// #nosec G404
|
||||
port3 := uint32(12000 + rand.Intn(4000))
|
||||
peers := map[uint64]string{
|
||||
1: fmt.Sprintf("signer-test01:%d", port1),
|
||||
2: fmt.Sprintf("signer-test02:%d", port2),
|
||||
3: fmt.Sprintf("signer-test03:%d", port3),
|
||||
}
|
||||
_, path, err := daemon.New(context.Background(), "", 1, port1, peers)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(path)
|
||||
_, path, err = daemon.New(context.Background(), "", 2, port2, peers)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(path)
|
||||
_, path, err = daemon.New(context.Background(), "", 3, port3, peers)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(path)
|
||||
endpoints := []*dirk.Endpoint{
|
||||
dirk.NewEndpoint("signer-test01", port1),
|
||||
dirk.NewEndpoint("signer-test02", port2),
|
||||
dirk.NewEndpoint("signer-test03", port3),
|
||||
}
|
||||
credentials, err := credentialsFromCerts(context.Background(), resources.ClientTest01Crt, resources.ClientTest01Key, resources.CACrt)
|
||||
require.NoError(t, err)
|
||||
testDistributedWallet, err := dirk.OpenWallet(context.Background(), "Wallet 3", credentials, endpoints)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "WalletPassphraseIncorrect",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "bad",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "failed to unlock wallet: incorrect passphrase",
|
||||
},
|
||||
{
|
||||
name: "PassphraseMissing",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "passphrase is required",
|
||||
},
|
||||
{
|
||||
name: "PassphraseWeak",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "poor",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PathMalformed",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "n/12381/3600/1/2/3",
|
||||
},
|
||||
err: "path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "PathPassphraseMissing",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "m/12381/3600/1/2/3",
|
||||
},
|
||||
err: "passphrase is required",
|
||||
},
|
||||
{
|
||||
name: "PathNotSupported",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testNDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "m/12381/3600/1/2/3",
|
||||
},
|
||||
err: "wallet does not support account creation with an explicit path",
|
||||
},
|
||||
{
|
||||
name: "GoodWithPath",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testHDWallet,
|
||||
accountName: "Pathed",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 1,
|
||||
signingThreshold: 1,
|
||||
path: "m/12381/3600/1/2/3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DistributedSigningThresholdZero",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 0,
|
||||
},
|
||||
err: "signing threshold required",
|
||||
},
|
||||
{
|
||||
name: "DistributedSigningThresholdNotHalf",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 1,
|
||||
},
|
||||
err: "signing threshold must be more than half the number of participants",
|
||||
},
|
||||
{
|
||||
name: "DistributedSigningThresholdTooHigh",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 4,
|
||||
},
|
||||
err: "signing threshold cannot be higher than the number of participants",
|
||||
},
|
||||
{
|
||||
name: "DistributedNotSupported",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testNDWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 2,
|
||||
},
|
||||
err: "wallet does not support distributed account creation",
|
||||
},
|
||||
{
|
||||
name: "DistributedGood",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testDistributedWallet,
|
||||
accountName: "Remote",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
participants: 3,
|
||||
signingThreshold: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.dataIn.accountName, res.account.Name())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilData(t *testing.T) {
|
||||
_, err := processStandard(context.Background(), nil)
|
||||
require.EqualError(t, err, "no data")
|
||||
_, err = processPathed(context.Background(), nil)
|
||||
require.EqualError(t, err, "no data")
|
||||
_, err = processDistributed(context.Background(), nil)
|
||||
require.EqualError(t, err, "no data")
|
||||
}
|
||||
|
||||
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.New("failed to add CA certificate")
|
||||
}
|
||||
tlsCfg.RootCAs = cp
|
||||
}
|
||||
|
||||
return credentials.NewTLS(tlsCfg), nil
|
||||
}
|
||||
50
cmd/account/create/run.go
Normal file
50
cmd/account/create/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountcreate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the account create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if !viper.GetBool("verbose") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
58
cmd/account/derive/input.go
Normal file
58
cmd/account/derive/input.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
quiet bool
|
||||
// Derivation information.
|
||||
mnemonic string
|
||||
path string
|
||||
// Output options.
|
||||
showPrivateKey bool
|
||||
showWithdrawalCredentials bool
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
// Quiet.
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
|
||||
// Mnemonic.
|
||||
if viper.GetString("mnemonic") == "" {
|
||||
return nil, errors.New("mnemonic is required")
|
||||
}
|
||||
data.mnemonic = viper.GetString("mnemonic")
|
||||
|
||||
// Path.
|
||||
if viper.GetString("path") == "" {
|
||||
return nil, errors.New("path is required")
|
||||
}
|
||||
data.path = viper.GetString("path")
|
||||
|
||||
// Show private key.
|
||||
data.showPrivateKey = viper.GetBool("show-private-key")
|
||||
|
||||
// Show withdrawal credentials.
|
||||
data.showWithdrawalCredentials = viper.GetBool("show-withdrawal-credentials")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
78
cmd/account/derive/input_internal_test.go
Normal file
78
cmd/account/derive/input_internal_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "MnemonicMissing",
|
||||
vars: map[string]interface{}{
|
||||
"path": "m/12381/3600/0/0",
|
||||
},
|
||||
err: "mnemonic is required",
|
||||
},
|
||||
{
|
||||
name: "PathMissing",
|
||||
vars: map[string]interface{}{
|
||||
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
},
|
||||
err: "path is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
"path": "m/12381/3600/0/0",
|
||||
},
|
||||
res: &dataIn{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
// Cannot compare accounts directly, so need to check each element individually.
|
||||
require.Equal(t, test.res.mnemonic, res.mnemonic)
|
||||
require.Equal(t, test.res.path, res.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
53
cmd/account/derive/output.go
Normal file
53
cmd/account/derive/output.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
showPrivateKey bool
|
||||
showWithdrawalCredentials bool
|
||||
key *e2types.BLSPrivateKey
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
if data.key == nil {
|
||||
return "", errors.New("no key")
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
if data.showPrivateKey {
|
||||
builder.WriteString(fmt.Sprintf("Private key: %#x\n", data.key.Marshal()))
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("Public key: %#x", data.key.PublicKey().Marshal()))
|
||||
if data.showWithdrawalCredentials {
|
||||
withdrawalCredentials := util.SHA256(data.key.PublicKey().Marshal())
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
builder.WriteString(fmt.Sprintf("\nWithdrawal credentials: %#x", withdrawalCredentials))
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
101
cmd/account/derive/output_internal_test.go
Normal file
101
cmd/account/derive/output_internal_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func blsPrivateKey(input string) *e2types.BLSPrivateKey {
|
||||
data, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
key, err := e2types.BLSPrivateKeyFromBytes(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
needs []string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "KeyMissing",
|
||||
dataOut: &dataOut{},
|
||||
err: "no key",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
},
|
||||
needs: []string{"Public key"},
|
||||
},
|
||||
{
|
||||
name: "PrivatKey",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showPrivateKey: true,
|
||||
},
|
||||
needs: []string{"Public key", "Private key"},
|
||||
},
|
||||
{
|
||||
name: "WithdrawalCredentials",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Public key", "Withdrawal credentials"},
|
||||
},
|
||||
{
|
||||
name: "All",
|
||||
dataOut: &dataOut{
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showPrivateKey: true,
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Public key", "Private key", "Withdrawal credentials"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
for _, need := range test.needs {
|
||||
require.Contains(t, res, need)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
72
cmd/account/derive/process.go
Normal file
72
cmd/account/derive/process.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// pathRegex is the regular expression that matches an HD path.
|
||||
var pathRegex = regexp.MustCompile("^m/[0-9]+/[0-9]+(/[0-9+])+")
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
// If there are more than 24 words we treat the additional characters as the passphrase.
|
||||
mnemonicParts := strings.Split(data.mnemonic, " ")
|
||||
mnemonicPassphrase := ""
|
||||
if len(mnemonicParts) > 24 {
|
||||
data.mnemonic = strings.Join(mnemonicParts[:24], " ")
|
||||
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
|
||||
}
|
||||
// Normalise the input.
|
||||
data.mnemonic = string(norm.NFKD.Bytes([]byte(data.mnemonic)))
|
||||
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
|
||||
|
||||
if !bip39.IsMnemonicValid(data.mnemonic) {
|
||||
return nil, errors.New("mnemonic is invalid")
|
||||
}
|
||||
|
||||
// Create seed from mnemonic and passphrase.
|
||||
seed := bip39.NewSeed(data.mnemonic, mnemonicPassphrase)
|
||||
|
||||
// Ensure the path is valid.
|
||||
match := pathRegex.Match([]byte(data.path))
|
||||
if !match {
|
||||
return nil, errors.New("path does not match expected format m/…")
|
||||
}
|
||||
|
||||
// Derive private key from seed and path.
|
||||
key, err := util.PrivateKeyFromSeedAndPath(seed, data.path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate key")
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
showPrivateKey: data.showPrivateKey,
|
||||
showWithdrawalCredentials: data.showWithdrawalCredentials,
|
||||
key: key,
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
97
cmd/account/derive/process_internal_test.go
Normal file
97
cmd/account/derive/process_internal_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
privKey []byte
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "MnemonicMissing",
|
||||
dataIn: &dataIn{
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
},
|
||||
{
|
||||
name: "MnemonicInvalid",
|
||||
dataIn: &dataIn{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
},
|
||||
{
|
||||
name: "PathMissing",
|
||||
dataIn: &dataIn{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
},
|
||||
err: "path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "PathInvalid",
|
||||
dataIn: &dataIn{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "n/12381/3600/0/0",
|
||||
},
|
||||
err: "path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataIn: &dataIn{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
privKey: testutil.HexToBytes("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
},
|
||||
{
|
||||
name: "Extended",
|
||||
dataIn: &dataIn{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art extended",
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
privKey: testutil.HexToBytes("0x58c8b280ae035de0452797b52fb62555f27f78541ea2f04b23e7bb0fcd0fc2d6"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.privKey, res.key.Marshal())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
49
cmd/account/derive/run.go
Normal file
49
cmd/account/derive/run.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Run runs the account create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if dataIn.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
87
cmd/account/import/input.go
Normal file
87
cmd/account/import/input.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
timeout time.Duration
|
||||
wallet e2wtypes.Wallet
|
||||
key []byte
|
||||
accountName string
|
||||
passphrase string
|
||||
walletPassphrase string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
var err error
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
|
||||
// Account name.
|
||||
if viper.GetString("account") == "" {
|
||||
return nil, errors.New("account is required")
|
||||
}
|
||||
_, data.accountName, err = e2wallet.WalletAndAccountNames(viper.GetString("account"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account name")
|
||||
}
|
||||
if data.accountName == "" {
|
||||
return nil, errors.New("account name is required")
|
||||
}
|
||||
|
||||
// Wallet.
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
data.wallet, err = util.WalletFromInput(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain wallet")
|
||||
}
|
||||
|
||||
// Passphrase.
|
||||
data.passphrase, err = util.GetOptionalPassphrase()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain passphrase")
|
||||
}
|
||||
|
||||
// Wallet passphrase.
|
||||
data.walletPassphrase = util.GetWalletPassphrase()
|
||||
|
||||
// Key.
|
||||
if viper.GetString("key") == "" {
|
||||
return nil, errors.New("key is required")
|
||||
}
|
||||
data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "key is malformed")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
152
cmd/account/import/input_internal_test.go
Normal file
152
cmd/account/import/input_internal_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "WalletUnknown",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Unknown/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain wallet: wallet not found",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "account is required",
|
||||
},
|
||||
{
|
||||
name: "AccountWalletOnly",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"account": "Test wallet/",
|
||||
},
|
||||
err: "account name is required",
|
||||
},
|
||||
{
|
||||
name: "AccountMalformed",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "//",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain account name: invalid account format",
|
||||
},
|
||||
{
|
||||
name: "MultiplePassphrases",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": []string{"ce%NohGhah4ye5ra", "other"},
|
||||
},
|
||||
err: "failed to obtain passphrase: multiple passphrases supplied",
|
||||
},
|
||||
{
|
||||
name: "KeyMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "key is required",
|
||||
},
|
||||
{
|
||||
name: "KeyMalformed",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"key": "invalid",
|
||||
},
|
||||
err: "key is malformed: encoding/hex: invalid byte: U+0069 'i'",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
accountName: "Test account",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
// Cannot compare accounts directly, so need to check each element individually.
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
require.Equal(t, test.res.accountName, res.accountName)
|
||||
require.Equal(t, test.res.passphrase, res.passphrase)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
45
cmd/account/import/output.go
Normal file
45
cmd/account/import/output.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
account e2wtypes.Account
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
if data.account == nil {
|
||||
return "", errors.New("no account")
|
||||
}
|
||||
|
||||
if pubKeyProvider, ok := data.account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
|
||||
return fmt.Sprintf("%#x", pubKeyProvider.CompositePublicKey().Marshal()), nil
|
||||
}
|
||||
|
||||
if pubKeyProvider, ok := data.account.(e2wtypes.AccountPublicKeyProvider); ok {
|
||||
return fmt.Sprintf("%#x", pubKeyProvider.PublicKey().Marshal()), nil
|
||||
}
|
||||
|
||||
return "", errors.New("no public key available")
|
||||
}
|
||||
113
cmd/account/import/output_internal_test.go
Normal file
113
cmd/account/import/output_internal_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
distributed "github.com/wealdtech/go-eth2-wallet-distributed"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func hexToBytes(input string) []byte {
|
||||
res, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test", scratch.New(), keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
distributedWallet, err := distributed.CreateWallet(context.Background(), "Test distributed", scratch.New(), keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, distributedWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
distributed0, err := distributedWallet.(e2wtypes.WalletDistributedAccountImporter).ImportDistributedAccount(context.Background(),
|
||||
"Distributed 0",
|
||||
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
2,
|
||||
[][]byte{
|
||||
hexToBytes("0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268"),
|
||||
hexToBytes("0xaec922bd7a9b7b1dc21993133b586b0c3041c1e2e04b513e862227b9d7aecaf9444222f7e78282a449622ffc6278915d"),
|
||||
},
|
||||
map[uint64]string{
|
||||
1: "localhost-1:12345",
|
||||
2: "localhost-2:12345",
|
||||
3: "localhost-3:12345",
|
||||
},
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "AccountNil",
|
||||
dataOut: &dataOut{},
|
||||
err: "no account",
|
||||
},
|
||||
{
|
||||
name: "Account",
|
||||
dataOut: &dataOut{
|
||||
account: interop0,
|
||||
},
|
||||
res: "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
{
|
||||
name: "DistributedAccount",
|
||||
dataOut: &dataOut{
|
||||
account: distributed0,
|
||||
},
|
||||
res: "0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/account/import/process.go
Normal file
55
cmd/account/import/process.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if data.passphrase == "" {
|
||||
return nil, errors.New("passphrase is required")
|
||||
}
|
||||
if !util.AcceptablePassphrase(data.passphrase) {
|
||||
return nil, errors.New("supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag")
|
||||
}
|
||||
locker, isLocker := data.wallet.(e2wtypes.WalletLocker)
|
||||
if isLocker {
|
||||
if err := locker.Unlock(ctx, []byte(data.walletPassphrase)); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unlock wallet")
|
||||
}
|
||||
defer func() {
|
||||
if err := locker.Lock(ctx); err != nil {
|
||||
util.Log.Trace().Err(err).Msg("Failed to lock wallet")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
results := &dataOut{}
|
||||
|
||||
account, err := data.wallet.(e2wtypes.WalletAccountImporter).ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to import account")
|
||||
}
|
||||
results.account = account
|
||||
|
||||
return results, nil
|
||||
}
|
||||
95
cmd/account/import/process_internal_test.go
Normal file
95
cmd/account/import/process_internal_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
testNDWallet, err := nd.CreateWallet(context.Background(),
|
||||
"Test",
|
||||
scratch.New(),
|
||||
keystorev4.New(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "PassphraseMissing",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testNDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "",
|
||||
walletPassphrase: "pass",
|
||||
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
},
|
||||
err: "passphrase is required",
|
||||
},
|
||||
{
|
||||
name: "PassphraseWeak",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testNDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "poor",
|
||||
walletPassphrase: "pass",
|
||||
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
},
|
||||
err: "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
wallet: testNDWallet,
|
||||
accountName: "Good",
|
||||
passphrase: "ce%NohGhah4ye5ra",
|
||||
walletPassphrase: "pass",
|
||||
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.dataIn.accountName, res.account.Name())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/account/import/run.go
Normal file
50
cmd/account/import/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the account import data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if !viper.GetBool("verbose") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
51
cmd/account/key/input.go
Normal file
51
cmd/account/key/input.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
timeout time.Duration
|
||||
account e2wtypes.Account
|
||||
passphrases []string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
var err error
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
|
||||
// Account.
|
||||
_, data.account, err = util.WalletAndAccountFromInput(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain acount")
|
||||
}
|
||||
|
||||
// Passphrases.
|
||||
data.passphrases = util.GetPassphrases()
|
||||
|
||||
return data, nil
|
||||
}
|
||||
128
cmd/account/key/input_internal_test.go
Normal file
128
cmd/account/key/input_internal_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{
|
||||
"account": "Test wallet/Interop 0",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "WalletUnknown",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Unknown/Interop 0",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain acount: failed to open wallet for account: wallet not found",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain acount: failed to open wallet for account: invalid account format",
|
||||
},
|
||||
{
|
||||
name: "AccountWalletOnly",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"account": "Test wallet/",
|
||||
},
|
||||
err: "failed to obtain acount: no account name",
|
||||
},
|
||||
{
|
||||
name: "AccountMalformed",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "//",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "failed to obtain acount: failed to open wallet for account: invalid account format",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Interop 0",
|
||||
"passphrase": []string{"ce%NohGhah4ye5ra", "pass"},
|
||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
passphrases: []string{"ce%NohGhah4ye5ra", "pass"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
// Cannot compare accounts directly, so need to check each element individually.
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
require.Equal(t, test.res.passphrases, res.passphrases)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
36
cmd/account/key/output.go
Normal file
36
cmd/account/key/output.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
if len(data.key) == 0 {
|
||||
return "", errors.New("no account")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%#x", data.key), nil
|
||||
}
|
||||
69
cmd/account/key/output_internal_test.go
Normal file
69
cmd/account/key/output_internal_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func hexToBytes(input string) []byte {
|
||||
res, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "AccountNil",
|
||||
dataOut: &dataOut{},
|
||||
err: "no account",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataOut: &dataOut{
|
||||
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
},
|
||||
res: "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
70
cmd/account/key/process.go
Normal file
70
cmd/account/key/process.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
if len(data.passphrases) == 0 {
|
||||
return nil, errors.New("passphrase is required")
|
||||
}
|
||||
|
||||
results := &dataOut{}
|
||||
|
||||
privateKeyProvider, isPrivateKeyProvider := data.account.(e2wtypes.AccountPrivateKeyProvider)
|
||||
if !isPrivateKeyProvider {
|
||||
return nil, errors.New("account does not provide its private key")
|
||||
}
|
||||
|
||||
if locker, isLocker := data.account.(e2wtypes.AccountLocker); isLocker {
|
||||
unlocked, err := locker.IsUnlocked(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find out if account is locked")
|
||||
}
|
||||
if !unlocked {
|
||||
for _, passphrase := range data.passphrases {
|
||||
err = locker.Unlock(ctx, []byte(passphrase))
|
||||
if err == nil {
|
||||
unlocked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !unlocked {
|
||||
return nil, errors.New("failed to unlock account")
|
||||
}
|
||||
// Because we unlocked the accout we should re-lock it when we're done.
|
||||
defer func() {
|
||||
if err := locker.Lock(ctx); err != nil {
|
||||
util.Log.Trace().Err(err).Msg("Failed to lock account")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
key, err := privateKeyProvider.PrivateKey(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain private key")
|
||||
}
|
||||
results.key = key.Marshal()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
87
cmd/account/key/process_internal_test.go
Normal file
87
cmd/account/key/process_internal_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
testNDWallet, err := nd.CreateWallet(context.Background(),
|
||||
"Test",
|
||||
scratch.New(),
|
||||
keystorev4.New(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testNDWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
interop0, err := testNDWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "PassphrasesMissing",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
account: interop0,
|
||||
},
|
||||
err: "passphrase is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
account: interop0,
|
||||
passphrases: []string{"ce%NohGhah4ye5ra", "pass"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
require.NotNil(t, res.key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/account/key/run.go
Normal file
50
cmd/account/key/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 accountkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the account import data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019 Weald Technology Trading
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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
|
||||
@@ -15,9 +15,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
accountcreate "github.com/wealdtech/ethdo/cmd/account/create"
|
||||
)
|
||||
|
||||
var accountCreateCmd = &cobra.Command{
|
||||
@@ -28,35 +29,37 @@ var accountCreateCmd = &cobra.Command{
|
||||
ethdo account create --account="primary/operations" --passphrase="my secret"
|
||||
|
||||
In quiet mode this will return 0 if the account is created successfully, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(!remote, "account create not available with remote wallets")
|
||||
assert(rootAccount != "", "--account is required")
|
||||
assert(rootAccountPassphrase != "", "--passphrase is required")
|
||||
|
||||
w, err := walletFromPath(rootAccount)
|
||||
errCheck(err, "Failed to access wallet")
|
||||
|
||||
if w.Type() == "hierarchical deterministic" {
|
||||
assert(rootWalletPassphrase != "", "--walletpassphrase is required to create new accounts with hierarchical deterministic wallets")
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := accountcreate.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = accountFromPath(rootAccount)
|
||||
assert(err != nil, "Account already exists")
|
||||
|
||||
err = w.Unlock([]byte(rootWalletPassphrase))
|
||||
errCheck(err, "Failed to unlock wallet")
|
||||
|
||||
_, accountName, err := walletAndAccountNamesFromPath(rootAccount)
|
||||
errCheck(err, "Failed to obtain accout name")
|
||||
|
||||
account, err := w.CreateAccount(accountName, []byte(rootAccountPassphrase))
|
||||
errCheck(err, "Failed to create account")
|
||||
|
||||
outputIf(verbose, fmt.Sprintf("0x%048x", account.PublicKey().Marshal()))
|
||||
os.Exit(_exitSuccess)
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
accountCmd.AddCommand(accountCreateCmd)
|
||||
accountFlags(accountCreateCmd)
|
||||
accountCreateCmd.Flags().Uint32("participants", 1, "Number of participants (1 for non-distributed accounts, >1 for distributed accounts)")
|
||||
accountCreateCmd.Flags().Uint32("signing-threshold", 1, "Signing threshold (1 for non-distributed accounts)")
|
||||
accountCreateCmd.Flags().String("path", "", "path of account (for hierarchical deterministic accounts)")
|
||||
}
|
||||
|
||||
func accountCreateBindings() {
|
||||
if err := viper.BindPFlag("participants", accountCreateCmd.Flags().Lookup("participants")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("path", accountCreateCmd.Flags().Lookup("path")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
69
cmd/accountderive.go
Normal file
69
cmd/accountderive.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
accountderive "github.com/wealdtech/ethdo/cmd/account/derive"
|
||||
)
|
||||
|
||||
var accountDeriveCmd = &cobra.Command{
|
||||
Use: "derive",
|
||||
Short: "Derive an account",
|
||||
Long: `Derive an account from a mnemonic and path. For example:
|
||||
|
||||
ethdo account derive --mnemonic="..." --path="m/12381/3600/0/0"
|
||||
|
||||
In quiet mode this will return 0 if the inputs can derive an account account, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := accountderive.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
accountCmd.AddCommand(accountDeriveCmd)
|
||||
accountFlags(accountDeriveCmd)
|
||||
accountDeriveCmd.Flags().String("mnemonic", "", "mnemonic from which to derive the HD seed")
|
||||
accountDeriveCmd.Flags().String("path", "", "path from which to derive the account")
|
||||
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
|
||||
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
|
||||
}
|
||||
|
||||
func accountDeriveBindings() {
|
||||
if err := viper.BindPFlag("mnemonic", accountDeriveCmd.Flags().Lookup("mnemonic")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("path", accountDeriveCmd.Flags().Lookup("path")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("show-withdrawal-credentials", accountDeriveCmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/go-bytesutil"
|
||||
types "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
"github.com/spf13/viper"
|
||||
accountimport "github.com/wealdtech/ethdo/cmd/account/import"
|
||||
)
|
||||
|
||||
var accountImportKey string
|
||||
|
||||
var accountImportCmd = &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "Import an account",
|
||||
@@ -32,40 +29,29 @@ var accountImportCmd = &cobra.Command{
|
||||
ethdo account import --account="primary/testing" --key="0x..." --passphrase="my secret"
|
||||
|
||||
In quiet mode this will return 0 if the account is imported successfully, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(!remote, "account import not available with remote wallets")
|
||||
assert(rootAccount != "", "--account is required")
|
||||
assert(rootAccountPassphrase != "", "--passphrase is required")
|
||||
assert(accountImportKey != "", "--key is required")
|
||||
|
||||
key, err := bytesutil.FromHexString(accountImportKey)
|
||||
errCheck(err, "Invalid key")
|
||||
|
||||
w, err := walletFromPath(rootAccount)
|
||||
errCheck(err, "Failed to access wallet")
|
||||
|
||||
_, ok := w.(types.WalletAccountImporter)
|
||||
assert(ok, fmt.Sprintf("wallets of type %q do not allow importing accounts", w.Type()))
|
||||
|
||||
_, err = accountFromPath(rootAccount)
|
||||
assert(err != nil, "Account already exists")
|
||||
|
||||
err = w.Unlock([]byte(rootWalletPassphrase))
|
||||
errCheck(err, "Failed to unlock wallet")
|
||||
|
||||
_, accountName, err := walletAndAccountNamesFromPath(rootAccount)
|
||||
errCheck(err, "Failed to obtain accout name")
|
||||
|
||||
account, err := w.(types.WalletAccountImporter).ImportAccount(accountName, key, []byte(rootAccountPassphrase))
|
||||
errCheck(err, "Failed to create account")
|
||||
|
||||
outputIf(verbose, fmt.Sprintf("0x%048x", account.PublicKey().Marshal()))
|
||||
os.Exit(_exitSuccess)
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := accountimport.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
accountCmd.AddCommand(accountImportCmd)
|
||||
accountFlags(accountImportCmd)
|
||||
accountImportCmd.Flags().StringVar(&accountImportKey, "key", "", "Private key of the account to import (0x...)")
|
||||
accountImportCmd.Flags().String("key", "", "Private key of the account to import (0x...)")
|
||||
}
|
||||
|
||||
func accountImportBindings() {
|
||||
if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
pb "github.com/wealdtech/eth2-signer-api/pb/v1"
|
||||
"github.com/spf13/viper"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var accountInfoCmd = &cobra.Command{
|
||||
@@ -31,27 +34,48 @@ var accountInfoCmd = &cobra.Command{
|
||||
|
||||
In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(rootAccount != "", "--account is required")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
|
||||
if remote {
|
||||
listerClient := pb.NewListerClient(remoteGRPCConn)
|
||||
listAccountsReq := &pb.ListAccountsRequest{
|
||||
Paths: []string{
|
||||
rootAccount,
|
||||
},
|
||||
assert(viper.GetString("account") != "", "--account is required")
|
||||
wallet, account, err := walletAndAccountFromInput(ctx)
|
||||
errCheck(err, "Failed to obtain account")
|
||||
|
||||
// Disallow wildcards (for now)
|
||||
assert(fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) == viper.GetString("account"), "Mismatched account name")
|
||||
|
||||
if quiet {
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID()))
|
||||
var withdrawalPubKey e2types.PublicKey
|
||||
if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
|
||||
fmt.Printf("Public key: %#x\n", pubKeyProvider.PublicKey().Marshal())
|
||||
// May be overwritten later, but grab it for now.
|
||||
withdrawalPubKey = pubKeyProvider.PublicKey()
|
||||
}
|
||||
if distributedAccount, ok := account.(e2wtypes.DistributedAccount); ok {
|
||||
fmt.Printf("Composite public key: %#x\n", distributedAccount.CompositePublicKey().Marshal())
|
||||
fmt.Printf("Signing threshold: %d/%d\n", distributedAccount.SigningThreshold(), len(distributedAccount.Participants()))
|
||||
if verbose {
|
||||
fmt.Printf("Participants:\n")
|
||||
for k, v := range distributedAccount.Participants() {
|
||||
fmt.Printf(" %d: %s\n", k, v)
|
||||
}
|
||||
}
|
||||
resp, err := listerClient.ListAccounts(context.Background(), listAccountsReq)
|
||||
errCheck(err, "Failed to access account")
|
||||
assert(resp.State == pb.ResponseState_SUCCEEDED, "No such account")
|
||||
assert(len(resp.Accounts) == 1, "No such account")
|
||||
fmt.Printf("Public key: %#048x\n", resp.Accounts[0].PublicKey)
|
||||
|
||||
} else {
|
||||
account, err := accountFromPath(rootAccount)
|
||||
errCheck(err, "Failed to access wallet")
|
||||
outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID()))
|
||||
outputIf(!quiet, fmt.Sprintf("Public key: %#048x", account.PublicKey().Marshal()))
|
||||
outputIf(verbose && account.Path() != "", fmt.Sprintf("Path: %s", account.Path()))
|
||||
withdrawalPubKey = distributedAccount.CompositePublicKey()
|
||||
}
|
||||
if verbose {
|
||||
withdrawalCredentials := util.SHA256(withdrawalPubKey.Marshal())
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
fmt.Printf("Withdrawal credentials: %#x\n", withdrawalCredentials)
|
||||
}
|
||||
if pathProvider, ok := account.(e2wtypes.AccountPathProvider); ok {
|
||||
if pathProvider.Path() != "" {
|
||||
fmt.Printf("Path: %s\n", pathProvider.Path())
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
|
||||
@@ -15,10 +15,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
types "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
"github.com/spf13/viper"
|
||||
accountkey "github.com/wealdtech/ethdo/cmd/account/key"
|
||||
)
|
||||
|
||||
// accountKeyCmd represents the account key command
|
||||
@@ -30,26 +30,18 @@ var accountKeyCmd = &cobra.Command{
|
||||
ethdo account key --account="Personal wallet/Operations" --passphrase="my account passphrase"
|
||||
|
||||
In quiet mode this will return 0 if the key can be obtained, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(!remote, "account keys not available with remote wallets")
|
||||
assert(rootAccount != "", "--account is required")
|
||||
|
||||
account, err := accountFromPath(rootAccount)
|
||||
errCheck(err, "Failed to access account")
|
||||
|
||||
_, ok := account.(types.AccountPrivateKeyProvider)
|
||||
assert(ok, fmt.Sprintf("account %q does not provide its private key", rootAccount))
|
||||
|
||||
assert(rootAccountPassphrase != "", "--passphrase is required")
|
||||
err = account.Unlock([]byte(rootAccountPassphrase))
|
||||
errCheck(err, "Failed to unlock account to obtain private key")
|
||||
defer account.Lock()
|
||||
privateKey, err := account.(types.AccountPrivateKeyProvider).PrivateKey()
|
||||
errCheck(err, "Failed to obtain private key")
|
||||
account.Lock()
|
||||
|
||||
outputIf(!quiet, fmt.Sprintf("%#064x", privateKey.Marshal()))
|
||||
os.Exit(_exitSuccess)
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := accountkey.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
pb "github.com/wealdtech/eth2-signer-api/pb/v1"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var accountLockCmd = &cobra.Command{
|
||||
@@ -31,26 +30,19 @@ var accountLockCmd = &cobra.Command{
|
||||
|
||||
In quiet mode this will return 0 if the account is locked, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(remote, "account lock only works with remote wallets")
|
||||
assert(rootAccount != "", "--account is required")
|
||||
|
||||
client := pb.NewAccountManagerClient(remoteGRPCConn)
|
||||
lockReq := &pb.LockAccountRequest{
|
||||
Account: rootAccount,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
resp, err := client.Lock(ctx, lockReq)
|
||||
errCheck(err, "Failed in attempt to lock account")
|
||||
switch resp.State {
|
||||
case pb.ResponseState_DENIED:
|
||||
die("Lock request denied")
|
||||
case pb.ResponseState_FAILED:
|
||||
die("Lock request failed")
|
||||
case pb.ResponseState_SUCCEEDED:
|
||||
outputIf(!quiet, "Lock request succeeded")
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
assert(viper.GetString("account") != "", "--account is required")
|
||||
|
||||
_, account, err := walletAndAccountFromInput(ctx)
|
||||
errCheck(err, "Failed to obtain account")
|
||||
|
||||
locker, isLocker := account.(e2wtypes.AccountLocker)
|
||||
assert(isLocker, "Account does not support locking")
|
||||
|
||||
err = locker.Lock(ctx)
|
||||
errCheck(err, "Failed to lock account")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
pb "github.com/wealdtech/eth2-signer-api/pb/v1"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var accountUnlockCmd = &cobra.Command{
|
||||
@@ -31,27 +31,31 @@ var accountUnlockCmd = &cobra.Command{
|
||||
|
||||
In quiet mode this will return 0 if the account is unlocked, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(remote, "account unlock only works with remote wallets")
|
||||
assert(rootAccount != "", "--account is required")
|
||||
|
||||
client := pb.NewAccountManagerClient(remoteGRPCConn)
|
||||
unlockReq := &pb.UnlockAccountRequest{
|
||||
Account: rootAccount,
|
||||
Passphrase: []byte(rootAccountPassphrase),
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
resp, err := client.Unlock(ctx, unlockReq)
|
||||
errCheck(err, "Failed in attempt to unlock account")
|
||||
switch resp.State {
|
||||
case pb.ResponseState_DENIED:
|
||||
die("Unlock request denied")
|
||||
case pb.ResponseState_FAILED:
|
||||
die("Unlock request failed")
|
||||
case pb.ResponseState_SUCCEEDED:
|
||||
outputIf(!quiet, "Unlock request succeeded")
|
||||
os.Exit(_exitSuccess)
|
||||
|
||||
assert(viper.GetString("account") != "", "--account is required")
|
||||
|
||||
_, account, err := walletAndAccountFromInput(ctx)
|
||||
errCheck(err, "Failed to obtain account")
|
||||
|
||||
locker, isLocker := account.(e2wtypes.AccountLocker)
|
||||
assert(isLocker, "Account does not support unlocking")
|
||||
|
||||
unlocked := false
|
||||
for _, passphrase := range getPassphrases() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
err := locker.Unlock(ctx, []byte(passphrase))
|
||||
cancel()
|
||||
if err == nil {
|
||||
// Success.
|
||||
unlocked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert(unlocked, "Failed to unlock account")
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
29
cmd/attestation.go
Normal file
29
cmd/attestation.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// attestationCmd represents the attestation command
|
||||
var attestationCmd = &cobra.Command{
|
||||
Use: "attestation",
|
||||
Short: "Obtain information about an Ethereum 2 attestation",
|
||||
Long: "Obtain information about an Ethereum 2 attestation",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(attestationCmd)
|
||||
}
|
||||
32
cmd/attester.go
Normal file
32
cmd/attester.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// attesterCmd represents the attester command
|
||||
var attesterCmd = &cobra.Command{
|
||||
Use: "attester",
|
||||
Short: "Obtain information about Ethereum 2 attesters",
|
||||
Long: "Obtain information about Ethereum 2 attesters",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(attesterCmd)
|
||||
}
|
||||
|
||||
func attesterFlags(cmd *cobra.Command) {
|
||||
}
|
||||
87
cmd/attester/duties/input.go
Normal file
87
cmd/attester/duties/input.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
// Chain information.
|
||||
slotsPerEpoch uint64
|
||||
// Operation.
|
||||
account string
|
||||
pubKey string
|
||||
eth2Client eth2client.Service
|
||||
epoch spec.Epoch
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.json = viper.GetBool("json")
|
||||
|
||||
// Account or pubkey.
|
||||
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
|
||||
return nil, errors.New("account or pubkey is required")
|
||||
}
|
||||
data.account = viper.GetString("account")
|
||||
data.pubKey = viper.GetString("pubkey")
|
||||
|
||||
// Ethereum 2 client.
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
// Epoch
|
||||
epoch := viper.GetInt64("epoch")
|
||||
if epoch == -1 {
|
||||
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
|
||||
}
|
||||
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis data")
|
||||
}
|
||||
epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch))
|
||||
}
|
||||
data.epoch = spec.Epoch(epoch)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
96
cmd/attester/duties/input_internal_test.go
Normal file
96
cmd/attester/duties/input_internal_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "account or pubkey is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/attester/duties/output.go
Normal file
55
cmd/attester/duties/output.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
json bool
|
||||
duty *api.AttesterDuty
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
if data.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if data.duty == nil {
|
||||
return "No duties found", nil
|
||||
}
|
||||
|
||||
if data.json {
|
||||
bytes, err := json.Marshal(data.duty)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshalJSON")
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Validator attesting in slot %d committee %d", data.duty.Slot, data.duty.CommitteeIndex), nil
|
||||
}
|
||||
85
cmd/attester/duties/output_internal_test.go
Normal file
85
cmd/attester/duties/output_internal_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
)
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
dataOut: &dataOut{},
|
||||
res: "No duties found",
|
||||
},
|
||||
{
|
||||
name: "Present",
|
||||
dataOut: &dataOut{
|
||||
duty: &api.AttesterDuty{
|
||||
PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
Slot: 1,
|
||||
ValidatorIndex: 2,
|
||||
CommitteeIndex: 3,
|
||||
CommitteeLength: 4,
|
||||
CommitteesAtSlot: 5,
|
||||
ValidatorCommitteeIndex: 6,
|
||||
},
|
||||
},
|
||||
res: "Validator attesting in slot 1 committee 3",
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
dataOut: &dataOut{
|
||||
json: true,
|
||||
duty: &api.AttesterDuty{
|
||||
PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
Slot: 1,
|
||||
ValidatorIndex: 2,
|
||||
CommitteeIndex: 3,
|
||||
CommitteeLength: 4,
|
||||
CommitteesAtSlot: 5,
|
||||
ValidatorCommitteeIndex: 6,
|
||||
},
|
||||
},
|
||||
res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
102
cmd/attester/duties/process.go
Normal file
102
cmd/attester/duties/process.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
if data.account != "" {
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account")
|
||||
}
|
||||
} else {
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch validator
|
||||
pubKeys := make([]spec.BLSPubKey, 1)
|
||||
pubKey, err := util.BestPublicKey(account)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain public key for account")
|
||||
}
|
||||
copy(pubKeys[0][:], pubKey.Marshal())
|
||||
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to obtain validator information")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return nil, errors.New("validator is not known")
|
||||
}
|
||||
var validator *api.Validator
|
||||
for _, v := range validators {
|
||||
validator = v
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
debug: data.debug,
|
||||
quiet: data.quiet,
|
||||
verbose: data.verbose,
|
||||
}
|
||||
|
||||
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain duty for validator")
|
||||
}
|
||||
|
||||
results.duty = duty
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
|
||||
if len(duties) == 0 {
|
||||
return nil, errors.New("validator does not have duty for that epoch")
|
||||
}
|
||||
|
||||
return duties[0], nil
|
||||
}
|
||||
66
cmd/attester/duties/process_internal_test.go
Normal file
66
cmd/attester/duties/process_internal_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/auto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
eth2Client, err := auto.New(context.Background(),
|
||||
auto.WithLogLevel(zerolog.Disabled),
|
||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Client",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
slotsPerEpoch: 32,
|
||||
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
|
||||
epoch: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/attester/duties/run.go
Normal file
50
cmd/attester/duties/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 attesterduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
89
cmd/attester/inclusion/input.go
Normal file
89
cmd/attester/inclusion/input.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
// Chain information.
|
||||
slotsPerEpoch uint64
|
||||
// Operation.
|
||||
eth2Client eth2client.Service
|
||||
epoch spec.Epoch
|
||||
account string
|
||||
pubKey string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
|
||||
// Account or pubkey.
|
||||
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
|
||||
return nil, errors.New("account or pubkey is required")
|
||||
}
|
||||
data.account = viper.GetString("account")
|
||||
data.pubKey = viper.GetString("pubkey")
|
||||
|
||||
// Ethereum 2 client.
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
|
||||
}
|
||||
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
// Epoch.
|
||||
epoch := viper.GetInt64("epoch")
|
||||
if epoch == -1 {
|
||||
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis data")
|
||||
}
|
||||
epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch))
|
||||
if epoch > 0 {
|
||||
epoch--
|
||||
}
|
||||
}
|
||||
data.epoch = spec.Epoch(epoch)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
96
cmd/attester/inclusion/input_internal_test.go
Normal file
96
cmd/attester/inclusion/input_internal_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "AccountMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "account or pubkey is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
46
cmd/attester/inclusion/output.go
Normal file
46
cmd/attester/inclusion/output.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
slot spec.Slot
|
||||
attestationIndex uint64
|
||||
inclusionDelay spec.Slot
|
||||
found bool
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
if !data.quiet {
|
||||
if data.found {
|
||||
return fmt.Sprintf("Attestation included in block %d, attestation %d (inclusion delay %d)", data.slot, data.attestationIndex, data.inclusionDelay), nil
|
||||
}
|
||||
return "Attestation not found", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
62
cmd/attester/inclusion/output_internal_test.go
Normal file
62
cmd/attester/inclusion/output_internal_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
dataOut: &dataOut{},
|
||||
res: "Attestation not found",
|
||||
},
|
||||
{
|
||||
name: "Found",
|
||||
dataOut: &dataOut{
|
||||
found: true,
|
||||
slot: 123,
|
||||
attestationIndex: 456,
|
||||
inclusionDelay: 7,
|
||||
},
|
||||
res: "Attestation included in block 123, attestation 456 (inclusion delay 7)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
136
cmd/attester/inclusion/process.go
Normal file
136
cmd/attester/inclusion/process.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
if data.account != "" {
|
||||
ctx, cancel := context.WithTimeout(ctx, data.timeout)
|
||||
defer cancel()
|
||||
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account")
|
||||
}
|
||||
} else {
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch validator
|
||||
pubKeys := make([]phase0.BLSPubKey, 1)
|
||||
pubKey, err := util.BestPublicKey(account)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain public key for account")
|
||||
}
|
||||
copy(pubKeys[0][:], pubKey.Marshal())
|
||||
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to obtain validator information")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return nil, errors.New("validator is not known")
|
||||
}
|
||||
var validator *api.Validator
|
||||
for _, v := range validators {
|
||||
validator = v
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
debug: data.debug,
|
||||
quiet: data.quiet,
|
||||
verbose: data.verbose,
|
||||
}
|
||||
|
||||
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain duty for validator")
|
||||
}
|
||||
|
||||
startSlot := duty.Slot + 1
|
||||
endSlot := startSlot + 32
|
||||
for slot := startSlot; slot < endSlot; slot++ {
|
||||
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block")
|
||||
}
|
||||
if signedBlock == nil {
|
||||
continue
|
||||
}
|
||||
blockSlot, err := signedBlock.Slot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block slot")
|
||||
}
|
||||
if blockSlot != slot {
|
||||
continue
|
||||
}
|
||||
if data.debug {
|
||||
fmt.Printf("Fetched block for slot %d\n", slot)
|
||||
}
|
||||
attestations, err := signedBlock.Attestations()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain block attestations")
|
||||
}
|
||||
for i, attestation := range attestations {
|
||||
if attestation.Data.Slot == duty.Slot &&
|
||||
attestation.Data.Index == duty.CommitteeIndex &&
|
||||
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
results.slot = slot
|
||||
results.attestationIndex = uint64(i)
|
||||
results.inclusionDelay = slot - duty.Slot
|
||||
results.found = true
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
|
||||
// Find the attesting slot for the given epoch.
|
||||
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
|
||||
if len(duties) == 0 {
|
||||
return nil, errors.New("validator does not have duty for that epoch")
|
||||
}
|
||||
|
||||
return duties[0], nil
|
||||
}
|
||||
66
cmd/attester/inclusion/process_internal_test.go
Normal file
66
cmd/attester/inclusion/process_internal_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/auto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
eth2Client, err := auto.New(context.Background(),
|
||||
auto.WithLogLevel(zerolog.Disabled),
|
||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Client",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
slotsPerEpoch: 32,
|
||||
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
|
||||
epoch: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/attester/inclusion/run.go
Normal file
50
cmd/attester/inclusion/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 attesterinclusion
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
65
cmd/attesterduties.go
Normal file
65
cmd/attesterduties.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
attesterduties "github.com/wealdtech/ethdo/cmd/attester/duties"
|
||||
)
|
||||
|
||||
var attesterDutiesCmd = &cobra.Command{
|
||||
Use: "duties",
|
||||
Short: "Obtain information about duties of an attester",
|
||||
Long: `Obtain information about dutes of an attester. For example:
|
||||
|
||||
ethdo attester duties --account=Validators/00001 --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if a duty from the attester is found, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := attesterduties.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
attesterCmd.AddCommand(attesterDutiesCmd)
|
||||
attesterFlags(attesterDutiesCmd)
|
||||
attesterDutiesCmd.Flags().Int64("epoch", -1, "the last complete epoch")
|
||||
attesterDutiesCmd.Flags().String("pubkey", "", "the public key of the attester")
|
||||
attesterDutiesCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
|
||||
}
|
||||
|
||||
func attesterDutiesBindings() {
|
||||
if err := viper.BindPFlag("epoch", attesterDutiesCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("pubkey", attesterDutiesCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", attesterDutiesCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
61
cmd/attesterinclusion.go
Normal file
61
cmd/attesterinclusion.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
attesterinclusion "github.com/wealdtech/ethdo/cmd/attester/inclusion"
|
||||
)
|
||||
|
||||
var attesterInclusionCmd = &cobra.Command{
|
||||
Use: "inclusion",
|
||||
Short: "Obtain information about attester inclusion",
|
||||
Long: `Obtain information about attester inclusion. For example:
|
||||
|
||||
ethdo attester inclusion --account=Validators/00001 --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if an attestation from the attester is found on the block of the given epoch, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := attesterinclusion.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
attesterCmd.AddCommand(attesterInclusionCmd)
|
||||
attesterFlags(attesterInclusionCmd)
|
||||
attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch")
|
||||
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
|
||||
}
|
||||
|
||||
func attesterInclusionBindings() {
|
||||
if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
68
cmd/block/info/input.go
Normal file
68
cmd/block/info/input.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 blockinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
// Operation.
|
||||
eth2Client eth2client.Service
|
||||
jsonOutput bool
|
||||
// Chain information.
|
||||
blockID string
|
||||
stream bool
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.jsonOutput = viper.GetBool("json")
|
||||
|
||||
data.stream = viper.GetBool("stream")
|
||||
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
if viper.GetString("blockid") == "" {
|
||||
data.blockID = "head"
|
||||
} else {
|
||||
// Specific slot.
|
||||
data.blockID = viper.GetString("blockid")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
126
cmd/block/info/input_internal_test.go
Normal file
126
cmd/block/info/input_internal_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 blockinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
},
|
||||
{
|
||||
name: "ConnectionBad",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": "localhost:1",
|
||||
"blockid": "justified",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
blockID: "justified",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
},
|
||||
{
|
||||
name: "BlockIDNil",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
blockID: "head",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BlockIDSpecific",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"blockid": "justified",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
blockID: "justified",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
require.Equal(t, test.res.blockID, res.blockID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
493
cmd/block/info/output.go
Normal file
493
cmd/block/info/output.go
Normal file
@@ -0,0 +1,493 @@
|
||||
// Copyright © 2019, 2020, 2021 Weald Technology Trading
|
||||
// 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 blockinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-bitfield"
|
||||
"github.com/wealdtech/go-string2eth"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
debug bool
|
||||
verbose bool
|
||||
eth2Client eth2client.Service
|
||||
genesisTime time.Time
|
||||
slotDuration time.Duration
|
||||
slotsPerEpoch uint64
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func outputBlockGeneral(ctx context.Context,
|
||||
verbose bool,
|
||||
slot phase0.Slot,
|
||||
blockRoot phase0.Root,
|
||||
bodyRoot phase0.Root,
|
||||
parentRoot phase0.Root,
|
||||
stateRoot phase0.Root,
|
||||
graffiti []byte,
|
||||
genesisTime time.Time,
|
||||
slotDuration time.Duration,
|
||||
slotsPerEpoch uint64,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Slot: %d\n", slot))
|
||||
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
|
||||
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
|
||||
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
|
||||
if verbose {
|
||||
res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot))
|
||||
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
|
||||
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
|
||||
}
|
||||
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
|
||||
if utf8.Valid(graffiti) {
|
||||
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
|
||||
} else {
|
||||
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti))
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockETH1Data(ctx context.Context, eth1Data *phase0.ETH1Data) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount))
|
||||
res.WriteString(fmt.Sprintf("Ethereum 1 deposit root: %#x\n", eth1Data.DepositRoot))
|
||||
res.WriteString(fmt.Sprintf("Ethereum 1 block hash: %#x\n", eth1Data.BlockHash))
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*phase0.Attestation) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
validatorCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations)))
|
||||
if verbose {
|
||||
beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider)
|
||||
if isProvider {
|
||||
for i, att := range attestations {
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
|
||||
// Fetch committees for this epoch if not already obtained.
|
||||
committees, exists := validatorCommittees[att.Data.Slot]
|
||||
if !exists {
|
||||
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain beacon committees")
|
||||
}
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
|
||||
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
}
|
||||
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
|
||||
}
|
||||
committees = validatorCommittees[att.Data.Slot]
|
||||
}
|
||||
|
||||
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
|
||||
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
|
||||
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
|
||||
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
|
||||
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
|
||||
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
|
||||
res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch))
|
||||
res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root))
|
||||
res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch))
|
||||
res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*phase0.AttesterSlashing) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings)))
|
||||
if verbose {
|
||||
for i, slashing := range attesterSlashings {
|
||||
// Say what was slashed.
|
||||
att1 := slashing.Attestation1
|
||||
att2 := slashing.Attestation2
|
||||
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
|
||||
if len(slashedIndices) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
res.WriteString(fmt.Sprintln(" Slashed validators:"))
|
||||
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", slashedIndices)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain beacon committees")
|
||||
}
|
||||
for k, v := range validators {
|
||||
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
|
||||
}
|
||||
|
||||
// Say what caused the slashing.
|
||||
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
|
||||
res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch))
|
||||
if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) {
|
||||
res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root))
|
||||
res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root))
|
||||
}
|
||||
if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) {
|
||||
res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot))
|
||||
res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot))
|
||||
}
|
||||
} else if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
|
||||
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
|
||||
res.WriteString(" Surround voted:\n")
|
||||
res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch))
|
||||
res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
// Deposits.
|
||||
res.WriteString(fmt.Sprintf("Deposits: %d\n", len(deposits)))
|
||||
if verbose {
|
||||
for i, deposit := range deposits {
|
||||
data := deposit.Data
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
res.WriteString(fmt.Sprintf(" Public key: %#x\n", data.PublicKey))
|
||||
res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(data.Amount), true)))
|
||||
res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", data.WithdrawalCredentials))
|
||||
res.WriteString(fmt.Sprintf(" Signature: %#x\n", data.Signature))
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Service, verbose bool, voluntaryExits []*phase0.SignedVoluntaryExit) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Voluntary exits: %d\n", len(voluntaryExits)))
|
||||
if verbose {
|
||||
for i, voluntaryExit := range voluntaryExits {
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
|
||||
if err != nil {
|
||||
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
|
||||
} else {
|
||||
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[0].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
|
||||
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service, verbose bool, syncAggregate *altair.SyncAggregate, epoch phase0.Epoch) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString("Sync aggregate: ")
|
||||
res.WriteString(fmt.Sprintf("%d/%d\n", syncAggregate.SyncCommitteeBits.Count(), syncAggregate.SyncCommitteeBits.Len()))
|
||||
if verbose {
|
||||
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
|
||||
if isProvider {
|
||||
config, err := specProvider.Spec(ctx)
|
||||
if err == nil {
|
||||
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
res.WriteString(" Contributions: ")
|
||||
res.WriteString(bitvectorToString(syncAggregate.SyncCommitteeBits))
|
||||
res.WriteString("\n")
|
||||
|
||||
syncCommitteesProvider, isProvider := eth2Client.(eth2client.SyncCommitteesProvider)
|
||||
if isProvider {
|
||||
syncCommittee, err := syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", uint64(epoch)*slotsPerEpoch))
|
||||
if err != nil {
|
||||
res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err))
|
||||
} else {
|
||||
res.WriteString(" Contributing validators:")
|
||||
for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ {
|
||||
if syncAggregate.SyncCommitteeBits.BitAt(i) {
|
||||
res.WriteString(fmt.Sprintf(" %d", syncCommittee.Validators[i]))
|
||||
}
|
||||
}
|
||||
res.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *altair.SignedBeaconBlock) (string, error) {
|
||||
if signedBlock == nil {
|
||||
return "", errors.New("no block supplied")
|
||||
}
|
||||
|
||||
body := signedBlock.Message.Body
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
// General info.
|
||||
blockRoot, err := signedBlock.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain block root")
|
||||
}
|
||||
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to generate body root")
|
||||
}
|
||||
|
||||
tmp, err := outputBlockGeneral(ctx,
|
||||
data.verbose,
|
||||
signedBlock.Message.Slot,
|
||||
blockRoot,
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti,
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Eth1 data.
|
||||
if data.verbose {
|
||||
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
}
|
||||
|
||||
// Sync aggregate.
|
||||
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Attestations.
|
||||
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Attester slashings.
|
||||
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
|
||||
// Add verbose proposer slashings.
|
||||
|
||||
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Voluntary exits.
|
||||
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phase0.SignedBeaconBlock) (string, error) {
|
||||
if signedBlock == nil {
|
||||
return "", errors.New("no block supplied")
|
||||
}
|
||||
|
||||
body := signedBlock.Message.Body
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
// General info.
|
||||
blockRoot, err := signedBlock.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain block root")
|
||||
}
|
||||
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to generate block root")
|
||||
}
|
||||
tmp, err := outputBlockGeneral(ctx,
|
||||
data.verbose,
|
||||
signedBlock.Message.Slot,
|
||||
blockRoot,
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti,
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Eth1 data.
|
||||
if data.verbose {
|
||||
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
}
|
||||
|
||||
// Attestations.
|
||||
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Attester slashings.
|
||||
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
|
||||
// Add verbose proposer slashings.
|
||||
|
||||
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
// Voluntary exits.
|
||||
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
// intersection returns a list of items common between the two sets.
|
||||
func intersection(set1 []uint64, set2 []uint64) []phase0.ValidatorIndex {
|
||||
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
|
||||
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
|
||||
res := make([]phase0.ValidatorIndex, 0)
|
||||
|
||||
set1Pos := 0
|
||||
set2Pos := 0
|
||||
for set1Pos < len(set1) && set2Pos < len(set2) {
|
||||
switch {
|
||||
case set1[set1Pos] < set2[set2Pos]:
|
||||
set1Pos++
|
||||
case set2[set2Pos] < set1[set1Pos]:
|
||||
set2Pos++
|
||||
default:
|
||||
res = append(res, phase0.ValidatorIndex(set1[set1Pos]))
|
||||
set1Pos++
|
||||
set2Pos++
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func bitlistToString(input bitfield.Bitlist) string {
|
||||
bits := int(input.Len())
|
||||
|
||||
res := ""
|
||||
for i := 0; i < bits; i++ {
|
||||
if input.BitAt(uint64(i)) {
|
||||
res = fmt.Sprintf("%s✓", res)
|
||||
} else {
|
||||
res = fmt.Sprintf("%s✕", res)
|
||||
}
|
||||
if i%8 == 7 {
|
||||
res = fmt.Sprintf("%s ", res)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(res)
|
||||
}
|
||||
|
||||
func bitvectorToString(input bitfield.Bitvector512) string {
|
||||
bits := int(input.Len())
|
||||
|
||||
res := strings.Builder{}
|
||||
for i := 0; i < bits; i++ {
|
||||
if input.BitAt(uint64(i)) {
|
||||
res.WriteString("✓")
|
||||
} else {
|
||||
res.WriteString("✕")
|
||||
}
|
||||
if i%8 == 7 && i != bits-1 {
|
||||
res.WriteString(" ")
|
||||
}
|
||||
}
|
||||
return res.String()
|
||||
}
|
||||
|
||||
func attestingIndices(input bitfield.Bitlist, indices []phase0.ValidatorIndex) string {
|
||||
bits := int(input.Len())
|
||||
res := ""
|
||||
for i := 0; i < bits; i++ {
|
||||
if input.BitAt(uint64(i)) {
|
||||
res = fmt.Sprintf("%s%d ", res, indices[i])
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(res)
|
||||
}
|
||||
177
cmd/block/info/output_internal_test.go
Normal file
177
cmd/block/info/output_internal_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 blockinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
)
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataOut: &dataOut{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// func TestOutputBlockText(t *testing.T) {
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// dataOut *dataOut
|
||||
// signedBeaconBlock *spec.SignedBeaconBlock
|
||||
// err string
|
||||
// }{
|
||||
// {
|
||||
// name: "Nil",
|
||||
// err: "no data",
|
||||
// },
|
||||
// {
|
||||
// name: "Good",
|
||||
// dataOut: &dataOut{},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// for _, test := range tests {
|
||||
// t.Run(test.name, func(t *testing.T) {
|
||||
// res := outputBlockText(context.Background(), test.dataOut, test.signedBeaconBlock)
|
||||
// if test.err != "" {
|
||||
// require.EqualError(t, err, test.err)
|
||||
// } else {
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, test.res, res)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestOutputBlockDeposits(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
verbose bool
|
||||
deposits []*spec.Deposit
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
res: "Deposits: 0\n",
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
res: "Deposits: 0\n",
|
||||
},
|
||||
{
|
||||
name: "Single",
|
||||
deposits: []*spec.Deposit{
|
||||
{
|
||||
Data: &spec.DepositData{
|
||||
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
|
||||
WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
|
||||
Amount: spec.Gwei(32000000000),
|
||||
Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
res: "Deposits: 1\n",
|
||||
},
|
||||
{
|
||||
name: "SingleVerbose",
|
||||
deposits: []*spec.Deposit{
|
||||
{
|
||||
Data: &spec.DepositData{
|
||||
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
|
||||
WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
|
||||
Amount: spec.Gwei(32000000000),
|
||||
Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
verbose: true,
|
||||
res: "Deposits: 1\n 0:\n Public key: 0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c\n Amount: 32 Ether\n Withdrawal credentials: 0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b\n Signature: 0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := outputBlockDeposits(context.Background(), test.verbose, test.deposits)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputBlockETH1Data(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
verbose bool
|
||||
eth1Data *spec.ETH1Data
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Good",
|
||||
eth1Data: &spec.ETH1Data{
|
||||
DepositRoot: testutil.HexToRoot("0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83"),
|
||||
DepositCount: 109936,
|
||||
BlockHash: testutil.HexToBytes("0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4"),
|
||||
},
|
||||
res: "Ethereum 1 deposit count: 109936\nEthereum 1 deposit root: 0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83\nEthereum 1 block hash: 0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := outputBlockETH1Data(context.Background(), test.eth1Data)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
155
cmd/block/info/process.go
Normal file
155
cmd/block/info/process.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 blockinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var jsonOutput bool
|
||||
var results *dataOut
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
results = &dataOut{
|
||||
debug: data.debug,
|
||||
verbose: data.verbose,
|
||||
eth2Client: data.eth2Client,
|
||||
}
|
||||
|
||||
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
|
||||
}
|
||||
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
|
||||
}
|
||||
results.genesisTime = genesis.GenesisTime
|
||||
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
|
||||
|
||||
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon block")
|
||||
}
|
||||
if signedBlock == nil {
|
||||
return nil, errors.New("empty beacon block")
|
||||
}
|
||||
switch signedBlock.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(ctx, data.jsonOutput, signedBlock.Altair); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unknown block version")
|
||||
}
|
||||
|
||||
if data.stream {
|
||||
jsonOutput = data.jsonOutput
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to start block stream")
|
||||
}
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
return &dataOut{}, nil
|
||||
}
|
||||
|
||||
func headEventHandler(event *api.Event) {
|
||||
// Only interested in head events.
|
||||
if event.Topic != "head" {
|
||||
return
|
||||
}
|
||||
|
||||
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
|
||||
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to obtain block: %v\n", err)
|
||||
return
|
||||
}
|
||||
if signedBlock == nil {
|
||||
fmt.Println("Empty beacon block")
|
||||
return
|
||||
}
|
||||
switch signedBlock.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
return
|
||||
}
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(context.Background(), jsonOutput, signedBlock.Altair); err != nil {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
default:
|
||||
data, err := outputPhase0BlockText(ctx, results, signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair.SignedBeaconBlock) error {
|
||||
switch {
|
||||
case jsonOutput:
|
||||
data, err := json.Marshal(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
default:
|
||||
data, err := outputAltairBlockText(ctx, results, signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
64
cmd/block/info/process_internal_test.go
Normal file
64
cmd/block/info/process_internal_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 blockinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/auto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
eth2Client, err := auto.New(context.Background(),
|
||||
auto.WithLogLevel(zerolog.Disabled),
|
||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Client",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
},
|
||||
err: "failed to output block: failed to generate text: no block supplied",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/block/info/run.go
Normal file
50
cmd/block/info/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 blockinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
227
cmd/blockinfo.go
227
cmd/blockinfo.go
@@ -14,21 +14,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/ethdo/grpc"
|
||||
string2eth "github.com/wealdtech/go-string2eth"
|
||||
"github.com/spf13/viper"
|
||||
blockinfo "github.com/wealdtech/ethdo/cmd/block/info"
|
||||
)
|
||||
|
||||
var blockInfoSlot int64
|
||||
|
||||
var blockInfoCmd = &cobra.Command{
|
||||
Use: "info",
|
||||
Short: "Obtain information about a block",
|
||||
@@ -37,204 +29,37 @@ var blockInfoCmd = &cobra.Command{
|
||||
ethdo block info --slot=12345
|
||||
|
||||
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := connect()
|
||||
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain block")
|
||||
|
||||
assert(blockInfoSlot != 0, "--slot is required")
|
||||
|
||||
var slot uint64
|
||||
if blockInfoSlot < 0 {
|
||||
// TODO latest block.
|
||||
} else {
|
||||
slot = uint64(blockInfoSlot)
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := blockinfo.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assert(slot > 0, "slot must be greater than 0")
|
||||
|
||||
signedBlock, err := grpc.FetchBlock(eth2GRPCConn, slot)
|
||||
errCheck(err, "Failed to obtain block")
|
||||
if signedBlock == nil {
|
||||
outputIf(!quiet, "No block at that slot")
|
||||
os.Exit(_exitFailure)
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
block := signedBlock.Block
|
||||
body := block.Body
|
||||
|
||||
// General info.
|
||||
outputIf(verbose, fmt.Sprintf("Parent root: %#x", block.ParentRoot))
|
||||
outputIf(verbose, fmt.Sprintf("State root: %#x", block.StateRoot))
|
||||
if len(body.Graffiti) > 0 && hex.EncodeToString(body.Graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
|
||||
if utf8.Valid(body.Graffiti) {
|
||||
fmt.Printf("Graffiti: %s\n", string(body.Graffiti))
|
||||
} else {
|
||||
fmt.Printf("Graffiti: %#x\n", body.Graffiti)
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
|
||||
// Eth1 data.
|
||||
eth1Data := body.Eth1Data
|
||||
outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit count: %d", eth1Data.DepositCount))
|
||||
outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit root: %#x", eth1Data.DepositRoot))
|
||||
outputIf(verbose, fmt.Sprintf("Ethereum 1 block hash: %#x", eth1Data.BlockHash))
|
||||
|
||||
// Attestations.
|
||||
fmt.Printf("Attestations: %d\n", len(body.Attestations))
|
||||
if verbose {
|
||||
for i, att := range body.Attestations {
|
||||
fmt.Printf("\t%d:\n", i)
|
||||
|
||||
fmt.Printf("\t\tCommittee index: %d\n", att.Data.CommitteeIndex)
|
||||
fmt.Printf("\t\tAttesters: %d\n", countSetBits(att.AggregationBits))
|
||||
fmt.Printf("\t\tAggregation bits: %s\n", bitsToString(att.AggregationBits))
|
||||
fmt.Printf("\t\tSlot: %d\n", att.Data.Slot)
|
||||
fmt.Printf("\t\tBeacon block root: %#x\n", att.Data.BeaconBlockRoot)
|
||||
fmt.Printf("\t\tSource epoch: %d\n", att.Data.Source.Epoch)
|
||||
fmt.Printf("\t\tSource root: %#x\n", att.Data.Source.Root)
|
||||
fmt.Printf("\t\tTarget epoch: %d\n", att.Data.Target.Epoch)
|
||||
fmt.Printf("\t\tTarget root: %#x\n", att.Data.Target.Root)
|
||||
}
|
||||
}
|
||||
|
||||
// Attester slashings.
|
||||
fmt.Printf("Attester slashings: %d\n", len(body.AttesterSlashings))
|
||||
if verbose {
|
||||
for i, slashing := range body.AttesterSlashings {
|
||||
fmt.Printf("\t%d:\n", i)
|
||||
|
||||
// Say what was slashed.
|
||||
att1 := slashing.Attestation_1
|
||||
att2 := slashing.Attestation_2
|
||||
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
|
||||
if len(slashedIndices) == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Println("\t\tSlashed validators:")
|
||||
for _, slashedIndex := range slashedIndices {
|
||||
validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, slashedIndex)
|
||||
errCheck(err, "Failed to obtain validator information")
|
||||
fmt.Printf("\t\t\t%#x (%d)\n", validator.PublicKey, slashedIndex)
|
||||
}
|
||||
|
||||
// Say what caused the slashing.
|
||||
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
|
||||
fmt.Printf("\t\tDouble voted for same target epoch (%d):\n", att1.Data.Target.Epoch)
|
||||
if !bytes.Equal(att1.Data.Target.Root, att2.Data.Target.Root) {
|
||||
fmt.Printf("\t\t\tAttestation 1 target epoch root: %#x\n", att1.Data.Target.Root)
|
||||
fmt.Printf("\t\t\tAttestation 2target epoch root: %#x\n", att2.Data.Target.Root)
|
||||
}
|
||||
if !bytes.Equal(att1.Data.BeaconBlockRoot, att2.Data.BeaconBlockRoot) {
|
||||
fmt.Printf("\t\t\tAttestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot)
|
||||
fmt.Printf("\t\t\tAttestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot)
|
||||
}
|
||||
} else {
|
||||
if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
|
||||
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
|
||||
fmt.Printf("\t\tSurround voted:\n")
|
||||
fmt.Printf("\t\t\tAttestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch)
|
||||
fmt.Printf("\t\t\tAttestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Proposer slashings once proposer slashings exist.
|
||||
|
||||
// Deposits.
|
||||
fmt.Printf("Deposits: %d\n", len(body.Deposits))
|
||||
if verbose {
|
||||
for i, deposit := range body.Deposits {
|
||||
data := deposit.Data
|
||||
fmt.Printf("\t%d:\n", i)
|
||||
fmt.Printf("\t\tPublic key: %#x\n", data.PublicKey)
|
||||
fmt.Printf("\t\tAmount: %s\n", string2eth.GWeiToString(data.Amount, true))
|
||||
fmt.Printf("\t\tWithdrawal credentials: %#x\n", data.WithdrawalCredentials)
|
||||
fmt.Printf("\t\tSignature: %#x\n", data.Signature)
|
||||
}
|
||||
}
|
||||
|
||||
// Voluntary exits.
|
||||
fmt.Printf("Voluntary exits: %d\n", len(body.VoluntaryExits))
|
||||
if verbose {
|
||||
for i, voluntaryExit := range body.VoluntaryExits {
|
||||
fmt.Printf("\t%d:\n", i)
|
||||
validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, voluntaryExit.Exit.ValidatorIndex)
|
||||
errCheck(err, "Failed to obtain validator information")
|
||||
fmt.Printf("\t\tValidator: %#x (%d)\n", validator.PublicKey, voluntaryExit.Exit.ValidatorIndex)
|
||||
fmt.Printf("\t\tEpoch: %d\n", voluntaryExit.Exit.Epoch)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// intersection returns a list of items common between the two sets.
|
||||
func intersection(set1 []uint64, set2 []uint64) []uint64 {
|
||||
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
|
||||
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
|
||||
res := make([]uint64, 0)
|
||||
if len(set1) < len(set2) {
|
||||
set1, set2 = set2, set1
|
||||
}
|
||||
|
||||
set2Pos := 0
|
||||
set2LastIndex := len(set2) - 1
|
||||
for set1Pos := range set1 {
|
||||
for set1[set1Pos] == set2[set2Pos] {
|
||||
res = append(res, set1[set1Pos])
|
||||
if set2Pos == set2LastIndex {
|
||||
break
|
||||
}
|
||||
set2Pos++
|
||||
}
|
||||
for set1[set1Pos] > set2[set2Pos] {
|
||||
if set2Pos == set2LastIndex {
|
||||
break
|
||||
}
|
||||
set2Pos++
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// countSetBits counts the number of bits that are set in the given byte array.
|
||||
func countSetBits(input []byte) int {
|
||||
total := 0
|
||||
for _, x := range input {
|
||||
item := uint8(x)
|
||||
for item > 0 {
|
||||
if item&0x01 == 1 {
|
||||
total++
|
||||
}
|
||||
item >>= 1
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func bitsToString(input []byte) string {
|
||||
elements := make([]string, len(input))
|
||||
for i, x := range input {
|
||||
item := uint8(x)
|
||||
mask := uint8(0x80)
|
||||
element := ""
|
||||
for mask > 0 {
|
||||
if item&mask != 0 {
|
||||
element = fmt.Sprintf("%s✓", element)
|
||||
} else {
|
||||
element = fmt.Sprintf("%s✕", element)
|
||||
}
|
||||
mask >>= 1
|
||||
}
|
||||
elements[i] = element
|
||||
}
|
||||
return strings.Join(elements, " ")
|
||||
}
|
||||
|
||||
func init() {
|
||||
blockCmd.AddCommand(blockInfoCmd)
|
||||
blockFlags(blockInfoCmd)
|
||||
blockInfoCmd.Flags().Int64Var(&blockInfoSlot, "slot", -1, "the default slot")
|
||||
|
||||
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
|
||||
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
|
||||
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func blockInfoBindings() {
|
||||
if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
81
cmd/chain/time/input.go
Normal file
81
cmd/chain/time/input.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
// Input
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
timestamp string
|
||||
slot string
|
||||
epoch string
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.json = viper.GetBool("json")
|
||||
|
||||
haveInput := false
|
||||
if viper.GetString("timestamp") != "" {
|
||||
data.timestamp = viper.GetString("timestamp")
|
||||
haveInput = true
|
||||
}
|
||||
if viper.GetString("slot") != "" {
|
||||
if haveInput {
|
||||
return nil, errors.New("only one of timestamp, slot and epoch allowed")
|
||||
}
|
||||
data.slot = viper.GetString("slot")
|
||||
haveInput = true
|
||||
}
|
||||
if viper.GetString("epoch") != "" {
|
||||
if haveInput {
|
||||
return nil, errors.New("only one of timestamp, slot and epoch allowed")
|
||||
}
|
||||
data.epoch = viper.GetString("epoch")
|
||||
haveInput = true
|
||||
}
|
||||
if !haveInput {
|
||||
return nil, errors.New("one of timestamp, slot or epoch required")
|
||||
}
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
data.connection = viper.GetString("connection")
|
||||
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
97
cmd/chain/time/input_internal_test.go
Normal file
97
cmd/chain/time/input_internal_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"slot": "1",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "IDMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "one of timestamp, slot or epoch required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
66
cmd/chain/time/output.go
Normal file
66
cmd/chain/time/output.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
debug bool
|
||||
quiet bool
|
||||
verbose bool
|
||||
|
||||
epoch spec.Epoch
|
||||
epochStart time.Time
|
||||
epochEnd time.Time
|
||||
slot spec.Slot
|
||||
slotStart time.Time
|
||||
slotEnd time.Time
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
}
|
||||
|
||||
if data.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.epoch))
|
||||
builder.WriteString("\n Epoch start ")
|
||||
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString("\n Epoch end ")
|
||||
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
|
||||
|
||||
builder.WriteString("\nSlot ")
|
||||
builder.WriteString(fmt.Sprintf("%d", data.slot))
|
||||
builder.WriteString("\n Slot start ")
|
||||
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString("\n Slot end ")
|
||||
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
|
||||
builder.WriteString("\n")
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
85
cmd/chain/time/output_internal_test.go
Normal file
85
cmd/chain/time/output_internal_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 (
|
||||
// "context"
|
||||
// "testing"
|
||||
//
|
||||
// api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
// "github.com/stretchr/testify/require"
|
||||
// "github.com/wealdtech/ethdo/testutil"
|
||||
// )
|
||||
//
|
||||
// func TestOutput(t *testing.T) {
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// dataOut *dataOut
|
||||
// res string
|
||||
// err string
|
||||
// }{
|
||||
// {
|
||||
// name: "Nil",
|
||||
// err: "no data",
|
||||
// },
|
||||
// {
|
||||
// name: "Empty",
|
||||
// dataOut: &dataOut{},
|
||||
// res: "No duties found",
|
||||
// },
|
||||
// {
|
||||
// name: "Present",
|
||||
// dataOut: &dataOut{
|
||||
// duty: &api.AttesterDuty{
|
||||
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
// Slot: 1,
|
||||
// ValidatorIndex: 2,
|
||||
// CommitteeIndex: 3,
|
||||
// CommitteeLength: 4,
|
||||
// CommitteesAtSlot: 5,
|
||||
// ValidatorCommitteeIndex: 6,
|
||||
// },
|
||||
// },
|
||||
// res: "Validator attesting in slot 1 committee 3",
|
||||
// },
|
||||
// {
|
||||
// name: "JSON",
|
||||
// dataOut: &dataOut{
|
||||
// json: true,
|
||||
// duty: &api.AttesterDuty{
|
||||
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
|
||||
// Slot: 1,
|
||||
// ValidatorIndex: 2,
|
||||
// CommitteeIndex: 3,
|
||||
// CommitteeLength: 4,
|
||||
// CommitteesAtSlot: 5,
|
||||
// ValidatorCommitteeIndex: 6,
|
||||
// },
|
||||
// },
|
||||
// res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// for _, test := range tests {
|
||||
// t.Run(test.name, func(t *testing.T) {
|
||||
// res, err := output(context.Background(), test.dataOut)
|
||||
// if test.err != "" {
|
||||
// require.EqualError(t, err, test.err)
|
||||
// } else {
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, test.res, res)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
89
cmd/chain/time/process.go
Normal file
89
cmd/chain/time/process.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
|
||||
}
|
||||
|
||||
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
|
||||
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
|
||||
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis data")
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
debug: data.debug,
|
||||
quiet: data.quiet,
|
||||
verbose: data.verbose,
|
||||
}
|
||||
|
||||
// Calculate the slot given the input.
|
||||
switch {
|
||||
case data.slot != "":
|
||||
slot, err := strconv.ParseUint(data.slot, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse slot")
|
||||
}
|
||||
results.slot = spec.Slot(slot)
|
||||
case data.epoch != "":
|
||||
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
results.slot = spec.Slot(epoch * slotsPerEpoch)
|
||||
case data.timestamp != "":
|
||||
timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse timestamp")
|
||||
}
|
||||
secs := timestamp.Sub(genesis.GenesisTime)
|
||||
if secs < 0 {
|
||||
return nil, errors.New("timestamp prior to genesis")
|
||||
}
|
||||
results.slot = spec.Slot(secs / slotDuration)
|
||||
}
|
||||
|
||||
// Fill in the info given the slot.
|
||||
results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration)
|
||||
results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration)
|
||||
results.epoch = spec.Epoch(uint64(results.slot) / slotsPerEpoch)
|
||||
results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration)
|
||||
results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
103
cmd/chain/time/process_internal_test.go
Normal file
103
cmd/chain/time/process_internal_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
expected *dataOut
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "Slot",
|
||||
dataIn: &dataIn{
|
||||
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
timeout: 10 * time.Second,
|
||||
allowInsecureConnections: true,
|
||||
slot: "1",
|
||||
},
|
||||
expected: &dataOut{
|
||||
epochStart: time.Unix(1606824023, 0),
|
||||
epochEnd: time.Unix(1606824407, 0),
|
||||
slot: 1,
|
||||
slotStart: time.Unix(1606824035, 0),
|
||||
slotEnd: time.Unix(1606824047, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Epoch",
|
||||
dataIn: &dataIn{
|
||||
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
timeout: 10 * time.Second,
|
||||
allowInsecureConnections: true,
|
||||
epoch: "2",
|
||||
},
|
||||
expected: &dataOut{
|
||||
epoch: 2,
|
||||
epochStart: time.Unix(1606824791, 0),
|
||||
epochEnd: time.Unix(1606825175, 0),
|
||||
slot: 64,
|
||||
slotStart: time.Unix(1606824791, 0),
|
||||
slotEnd: time.Unix(1606824803, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Timestamp",
|
||||
dataIn: &dataIn{
|
||||
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
timeout: 10 * time.Second,
|
||||
allowInsecureConnections: true,
|
||||
timestamp: "2021-01-01T00:00:00",
|
||||
},
|
||||
expected: &dataOut{
|
||||
epoch: 6862,
|
||||
epochStart: time.Unix(1609459031, 0),
|
||||
epochEnd: time.Unix(1609459415, 0),
|
||||
slot: 219598,
|
||||
slotStart: time.Unix(1609459199, 0),
|
||||
slotEnd: time.Unix(1609459211, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/chain/time/run.go
Normal file
50
cmd/chain/time/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
86
cmd/chain/verify/signedcontributionandproof/command.go
Normal file
86
cmd/chain/verify/signedcontributionandproof/command.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// 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 chainverifysignedcontributionandproof
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Input.
|
||||
data string
|
||||
item *altair.SignedContributionAndProof
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
|
||||
// Data.
|
||||
spec map[string]interface{}
|
||||
validator *api.Validator
|
||||
syncCommittee *api.SyncCommittee
|
||||
|
||||
// Output.
|
||||
itemStructureValid bool
|
||||
validatorKnown bool
|
||||
validatorInSyncCommittee bool
|
||||
validatorIsAggregator bool
|
||||
contributionSignatureValidFormat bool
|
||||
contributionAndProofSignatureValidFormat bool
|
||||
contributionAndProofSignatureValid bool
|
||||
additionalInfo string
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("data") == "" {
|
||||
return nil, errors.New("data is required")
|
||||
}
|
||||
c.data = viper.GetString("data")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// 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 chainverifysignedcontributionandproof
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "DataMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "data is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"data": "{}",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"data": "{}",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
_, err := newCommand(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
127
cmd/chain/verify/signedcontributionandproof/output.go
Normal file
127
cmd/chain/verify/signedcontributionandproof/output.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// 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 chainverifysignedcontributionandproof
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Valid data structure: ")
|
||||
if c.itemStructureValid {
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕")
|
||||
if c.additionalInfo != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(c.additionalInfo)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
builder.WriteString("Validator known: ")
|
||||
if c.validatorKnown {
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕")
|
||||
if c.additionalInfo != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(c.additionalInfo)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
builder.WriteString("Validator in sync committee: ")
|
||||
if c.validatorInSyncCommittee {
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕")
|
||||
if c.additionalInfo != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(c.additionalInfo)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
builder.WriteString("Validator is aggregator: ")
|
||||
if c.validatorIsAggregator {
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕")
|
||||
if c.additionalInfo != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(c.additionalInfo)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
builder.WriteString("Contribution signature has valid format: ")
|
||||
if c.contributionSignatureValidFormat {
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕")
|
||||
if c.additionalInfo != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(c.additionalInfo)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
builder.WriteString("Contribution and proof signature has valid format: ")
|
||||
if c.contributionAndProofSignatureValidFormat {
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕")
|
||||
if c.additionalInfo != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(c.additionalInfo)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
builder.WriteString("Contribution and proof signature is valid: ")
|
||||
if c.contributionAndProofSignatureValid {
|
||||
builder.WriteString("✓\n")
|
||||
} else {
|
||||
builder.WriteString("✕")
|
||||
if c.additionalInfo != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(c.additionalInfo)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
303
cmd/chain/verify/signedcontributionandproof/process.go
Normal file
303
cmd/chain/verify/signedcontributionandproof/process.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// 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 chainverifysignedcontributionandproof
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
// Parse the data.
|
||||
if c.data == "" {
|
||||
return errors.New("no data supplied")
|
||||
}
|
||||
c.item = &altair.SignedContributionAndProof{}
|
||||
err := json.Unmarshal([]byte(c.data), c.item)
|
||||
if err != nil {
|
||||
c.additionalInfo = err.Error()
|
||||
return nil
|
||||
}
|
||||
c.itemStructureValid = true
|
||||
|
||||
// Obtain information we need to process.
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, validatorIndex := range c.syncCommittee.Validators {
|
||||
if validatorIndex == c.item.Message.AggregatorIndex {
|
||||
c.validatorInSyncCommittee = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !c.validatorInSyncCommittee {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure the validator is an aggregator.
|
||||
isAggregator, err := c.isAggregator(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to ascertain if sync committee member is aggregator")
|
||||
}
|
||||
if !isAggregator {
|
||||
return nil
|
||||
}
|
||||
c.validatorIsAggregator = true
|
||||
|
||||
// Confirm the contribution signature.
|
||||
if err := c.confirmContributionSignature(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to confirm the contribution signature")
|
||||
}
|
||||
|
||||
// Confirm the contribution and proof signature.
|
||||
if err := c.confirmContributionAndProofSignature(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to confirm the contribution and proof signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// Connect to the client.
|
||||
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to beacon node")
|
||||
}
|
||||
|
||||
// Obtain the validator.
|
||||
var isProvider bool
|
||||
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide validator information")
|
||||
}
|
||||
|
||||
stateID := fmt.Sprintf("%d", c.item.Message.Contribution.Slot)
|
||||
validators, err := c.validatorsProvider.Validators(ctx,
|
||||
stateID,
|
||||
[]phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
|
||||
if len(validators) == 0 || validators[c.item.Message.AggregatorIndex] == nil {
|
||||
return nil
|
||||
}
|
||||
c.validatorKnown = true
|
||||
c.validator = validators[c.item.Message.AggregatorIndex]
|
||||
|
||||
// Obtain the sync committee
|
||||
syncCommitteesProvider, isProvider := c.eth2Client.(eth2client.SyncCommitteesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide sync committee information")
|
||||
}
|
||||
c.syncCommittee, err = syncCommitteesProvider.SyncCommittee(ctx, stateID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain sync committee information")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isAggregator returns true if the given
|
||||
func (c *command) isAggregator(ctx context.Context) (bool, error) {
|
||||
// Calculate the modulo.
|
||||
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
|
||||
if !isProvider {
|
||||
return false, errors.New("connection does not provide spec information")
|
||||
}
|
||||
var err error
|
||||
c.spec, err = specProvider.Spec(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to obtain spec information")
|
||||
}
|
||||
|
||||
tmp, exists := c.spec["SYNC_COMMITTEE_SIZE"]
|
||||
if !exists {
|
||||
return false, errors.New("spec does not contain SYNC_COMMITTEE_SIZE")
|
||||
}
|
||||
if _, isUint64 := tmp.(uint64); !isUint64 {
|
||||
return false, errors.New("spec returned non-integer value for SYNC_COMMITTEE_SIZE")
|
||||
}
|
||||
syncCommitteeSize := tmp.(uint64)
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "sync committee size is %d\n", syncCommitteeSize)
|
||||
}
|
||||
|
||||
tmp, exists = c.spec["SYNC_COMMITTEE_SUBNET_COUNT"]
|
||||
if !exists {
|
||||
return false, errors.New("spec does not contain SYNC_COMMITTEE_SUBNET_COUNT")
|
||||
}
|
||||
if _, isUint64 := tmp.(uint64); !isUint64 {
|
||||
return false, errors.New("spec returned non-integer value for SYNC_COMMITTEE_SUBNET_COUNT")
|
||||
}
|
||||
syncCommitteeSubnetCount := tmp.(uint64)
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "sync committee subnet count is %d\n", syncCommitteeSubnetCount)
|
||||
}
|
||||
|
||||
tmp, exists = c.spec["TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE"]
|
||||
if !exists {
|
||||
return false, errors.New("spec does not contain TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE")
|
||||
}
|
||||
if _, isUint64 := tmp.(uint64); !isUint64 {
|
||||
return false, errors.New("spec returned non-integer value for TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE")
|
||||
}
|
||||
targetAggregatorsPerSyncSubcommittee := tmp.(uint64)
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "target aggregators per sync subcommittee is %d\n", targetAggregatorsPerSyncSubcommittee)
|
||||
}
|
||||
|
||||
modulo := syncCommitteeSize / syncCommitteeSubnetCount / targetAggregatorsPerSyncSubcommittee
|
||||
if modulo < 1 {
|
||||
modulo = 1
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "modulo is %d\n", modulo)
|
||||
}
|
||||
|
||||
// Hash the selection proof.
|
||||
sigHash := sha256.New()
|
||||
n, err := sigHash.Write(c.item.Message.SelectionProof[:])
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to hash the selection proof")
|
||||
}
|
||||
if n != len(c.item.Signature[:]) {
|
||||
return false, errors.New("failed to write all bytes of the selection proof to the hash")
|
||||
}
|
||||
hash := sigHash.Sum(nil)
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "hash of selection proof is %#x\n", hash)
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint64(hash[:8])%modulo == 0, nil
|
||||
}
|
||||
|
||||
func (c *command) confirmContributionSignature(ctx context.Context) error {
|
||||
sigBytes := make([]byte, 96)
|
||||
copy(sigBytes, c.item.Message.Contribution.Signature[:])
|
||||
_, err := e2types.BLSSignatureFromBytes(sigBytes)
|
||||
if err != nil {
|
||||
c.additionalInfo = err.Error()
|
||||
return nil
|
||||
}
|
||||
c.contributionSignatureValidFormat = true
|
||||
|
||||
subCommittee := c.syncCommittee.ValidatorAggregates[c.item.Message.Contribution.SubcommitteeIndex]
|
||||
includedIndices := make([]phase0.ValidatorIndex, 0, len(subCommittee))
|
||||
for i := uint64(0); i < c.item.Message.Contribution.AggregationBits.Len(); i++ {
|
||||
if c.item.Message.Contribution.AggregationBits.BitAt(i) {
|
||||
includedIndices = append(includedIndices, subCommittee[int(i)])
|
||||
}
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Contribution validator indices: %v (%d)\n", includedIndices, len(includedIndices))
|
||||
}
|
||||
|
||||
includedValidators, err := c.validatorsProvider.Validators(ctx, "head", includedIndices)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain subcommittee validators")
|
||||
}
|
||||
if len(includedValidators) == 0 {
|
||||
return errors.New("obtained empty subcommittee validator list")
|
||||
}
|
||||
|
||||
var aggregatePubKey *e2types.BLSPublicKey
|
||||
for _, v := range includedValidators {
|
||||
pubKeyBytes := make([]byte, 48)
|
||||
copy(pubKeyBytes, v.Validator.PublicKey[:])
|
||||
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to aggregate public key")
|
||||
}
|
||||
if aggregatePubKey == nil {
|
||||
aggregatePubKey = pubKey
|
||||
} else {
|
||||
aggregatePubKey.Aggregate(pubKey)
|
||||
}
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Aggregate public key is %#x\n", aggregatePubKey.Marshal())
|
||||
}
|
||||
|
||||
// Don't have the ability to carry out the batch verification at current.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) confirmContributionAndProofSignature(ctx context.Context) error {
|
||||
sigBytes := make([]byte, 96)
|
||||
copy(sigBytes, c.item.Signature[:])
|
||||
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
|
||||
if err != nil {
|
||||
c.additionalInfo = err.Error()
|
||||
return nil
|
||||
}
|
||||
c.contributionAndProofSignatureValidFormat = true
|
||||
|
||||
pubKeyBytes := make([]byte, 48)
|
||||
copy(pubKeyBytes, c.validator.Validator.PublicKey[:])
|
||||
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to configure public key")
|
||||
}
|
||||
|
||||
objectRoot, err := c.item.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain signging root")
|
||||
}
|
||||
|
||||
tmp, exists := c.spec["DOMAIN_CONTRIBUTION_AND_PROOF"]
|
||||
if !exists {
|
||||
return errors.New("spec does not contain DOMAIN_CONTRIBUTION_AND_PROOF")
|
||||
}
|
||||
if _, isUint64 := tmp.(phase0.DomainType); !isUint64 {
|
||||
return errors.New("spec returned non-domain type value for DOMAIN_CONTRIBUTION_AND_PROOF")
|
||||
}
|
||||
contributionAndProofDomainType := tmp.(phase0.DomainType)
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "contribution and proof domain type is %#x\n", contributionAndProofDomainType)
|
||||
}
|
||||
domain, err := c.eth2Client.(eth2client.DomainProvider).Domain(ctx, contributionAndProofDomainType, phase0.Epoch(c.item.Message.Contribution.Slot/32))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain domain")
|
||||
}
|
||||
|
||||
container := &phase0.SigningData{
|
||||
ObjectRoot: objectRoot,
|
||||
Domain: domain,
|
||||
}
|
||||
signingRoot, err := container.HashTreeRoot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain signging root")
|
||||
}
|
||||
|
||||
c.contributionAndProofSignatureValid = sig.Verify(signingRoot[:], pubKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// 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 chainverifysignedcontributionandproof
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidData",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"data": "[[",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
cmd, err := newCommand(context.Background())
|
||||
require.NoError(t, err)
|
||||
err = cmd.process(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/chain/verify/signedcontributionandproof/run.go
Normal file
50
cmd/chain/verify/signedcontributionandproof/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2021 Weald Technology Trading.
|
||||
// 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 chainverifysignedcontributionandproof
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := newCommand(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to set up command")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := c.process(ctx); err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
if viper.GetBool("quiet") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -14,12 +14,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/ethdo/grpc"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
var chainInfoCmd = &cobra.Command{
|
||||
@@ -31,22 +35,47 @@ var chainInfoCmd = &cobra.Command{
|
||||
|
||||
In quiet mode this will return 0 if the chain information can be obtained, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := connect()
|
||||
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
|
||||
config, err := grpc.FetchChainConfig(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain beacon chain configuration")
|
||||
ctx := context.Background()
|
||||
|
||||
genesisTime, err := grpc.FetchGenesis(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain genesis time")
|
||||
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
errCheck(err, "Failed to obtain beacon chain specification")
|
||||
|
||||
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
errCheck(err, "Failed to obtain beacon chain genesis")
|
||||
|
||||
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
|
||||
errCheck(err, "Failed to obtain current fork")
|
||||
|
||||
if quiet {
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
fmt.Printf("Genesis time:\t\t%s\n", genesisTime.Format(time.UnixDate))
|
||||
outputIf(verbose, fmt.Sprintf("Genesis fork version:\t%0x", config["GenesisForkVersion"].([]byte)))
|
||||
outputIf(verbose, fmt.Sprintf("Seconds per slot:\t%v", config["SecondsPerSlot"].(uint64)))
|
||||
outputIf(verbose, fmt.Sprintf("Slots per epoch:\t%v", config["SlotsPerEpoch"].(uint64)))
|
||||
if genesis.GenesisTime.Unix() == 0 {
|
||||
fmt.Println("Genesis time: undefined")
|
||||
} else {
|
||||
fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate))
|
||||
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
|
||||
}
|
||||
fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot)
|
||||
fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].(spec.Version))
|
||||
fmt.Printf("Current fork version: %#x\n", fork.CurrentVersion)
|
||||
if verbose {
|
||||
forkData := &spec.ForkData{
|
||||
CurrentVersion: fork.CurrentVersion,
|
||||
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
|
||||
}
|
||||
forkDataRoot, err := forkData.HashTreeRoot()
|
||||
if err == nil {
|
||||
var forkDigest spec.ForkDigest
|
||||
copy(forkDigest[:], forkDataRoot[:])
|
||||
fmt.Printf("Fork digest: %#x\n", forkDigest)
|
||||
}
|
||||
}
|
||||
fmt.Printf("Seconds per slot: %d\n", int(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
|
||||
fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64))
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
@@ -56,10 +85,3 @@ func init() {
|
||||
chainCmd.AddCommand(chainInfoCmd)
|
||||
chainFlags(chainInfoCmd)
|
||||
}
|
||||
|
||||
func timestampToSlot(genesis int64, timestamp int64, secondsPerSlot uint64) uint64 {
|
||||
if timestamp < genesis {
|
||||
return 0
|
||||
}
|
||||
return uint64(timestamp-genesis) / secondsPerSlot
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Copyright © 2020, 2021 Weald Technology Trading.
|
||||
// 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
|
||||
@@ -14,16 +14,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/ethdo/grpc"
|
||||
"github.com/spf13/viper"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
var chainStatusSlot bool
|
||||
|
||||
var chainStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Obtain status about a chain",
|
||||
@@ -33,60 +36,113 @@ var chainStatusCmd = &cobra.Command{
|
||||
|
||||
In quiet mode this will return 0 if the chain status can be obtained, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := connect()
|
||||
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
|
||||
config, err := grpc.FetchChainConfig(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain beacon chain configuration")
|
||||
ctx := context.Background()
|
||||
|
||||
genesisTime, err := grpc.FetchGenesis(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain genesis time")
|
||||
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
info, err := grpc.FetchChainInfo(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain chain info")
|
||||
chainTime, err := standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
)
|
||||
errCheck(err, "Failed to configure chaintime service")
|
||||
|
||||
if quiet {
|
||||
os.Exit(_exitSuccess)
|
||||
finalityProvider, isProvider := eth2Client.(eth2client.FinalityProvider)
|
||||
assert(isProvider, "beacon node does not provide finality; cannot report on chain status")
|
||||
finality, err := finalityProvider.Finality(ctx, "head")
|
||||
errCheck(err, "Failed to obtain finality information")
|
||||
|
||||
slot := chainTime.CurrentSlot()
|
||||
|
||||
nextSlot := slot + 1
|
||||
nextSlotTimestamp := chainTime.StartOfSlot(nextSlot)
|
||||
|
||||
epoch := chainTime.CurrentEpoch()
|
||||
epochStartSlot := chainTime.FirstSlotOfEpoch(epoch)
|
||||
epochEndSlot := chainTime.FirstSlotOfEpoch(epoch+1) - 1
|
||||
|
||||
nextEpoch := epoch + 1
|
||||
nextEpochStartSlot := chainTime.FirstSlotOfEpoch(nextEpoch)
|
||||
nextEpochTimestamp := chainTime.StartOfEpoch(nextEpoch)
|
||||
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString("Current slot: ")
|
||||
res.WriteString(fmt.Sprintf("%d", slot))
|
||||
res.WriteString("\n")
|
||||
|
||||
res.WriteString("Current epoch: ")
|
||||
res.WriteString(fmt.Sprintf("%d", epoch))
|
||||
res.WriteString("\n")
|
||||
|
||||
if verbose {
|
||||
res.WriteString("Epoch slots: ")
|
||||
res.WriteString(fmt.Sprintf("%d", epochStartSlot))
|
||||
res.WriteString("-")
|
||||
res.WriteString(fmt.Sprintf("%d", epochEndSlot))
|
||||
res.WriteString("\n")
|
||||
}
|
||||
|
||||
slot := timestampToSlot(genesisTime.Unix(), time.Now().Unix(), config["SecondsPerSlot"].(uint64))
|
||||
if chainStatusSlot {
|
||||
fmt.Printf("Current slot: %d\n", slot)
|
||||
fmt.Printf("Justified slot: %d\n", info.GetJustifiedSlot())
|
||||
res.WriteString("Time until next slot: ")
|
||||
res.WriteString(time.Until(nextSlotTimestamp).Round(time.Second).String())
|
||||
res.WriteString("\n")
|
||||
|
||||
res.WriteString("Time until next epoch: ")
|
||||
res.WriteString(time.Until(nextEpochTimestamp).Round(time.Second).String())
|
||||
res.WriteString("\n")
|
||||
|
||||
res.WriteString("Slots until next epoch: ")
|
||||
res.WriteString(fmt.Sprintf("%d", nextEpochStartSlot-slot))
|
||||
res.WriteString("\n")
|
||||
|
||||
res.WriteString("Justified epoch: ")
|
||||
res.WriteString(fmt.Sprintf("%d", finality.Justified.Epoch))
|
||||
res.WriteString("\n")
|
||||
if verbose {
|
||||
distance := epoch - finality.Justified.Epoch
|
||||
res.WriteString("Justified epoch distance: ")
|
||||
res.WriteString(fmt.Sprintf("%d", distance))
|
||||
res.WriteString("\n")
|
||||
}
|
||||
|
||||
res.WriteString("Finalized epoch: ")
|
||||
res.WriteString(fmt.Sprintf("%d", finality.Finalized.Epoch))
|
||||
res.WriteString("\n")
|
||||
if verbose {
|
||||
distance := epoch - finality.Finalized.Epoch
|
||||
res.WriteString("Finalized epoch distance: ")
|
||||
res.WriteString(fmt.Sprintf("%d", distance))
|
||||
res.WriteString("\n")
|
||||
}
|
||||
|
||||
if epoch >= chainTime.AltairInitialEpoch() {
|
||||
period := chainTime.SlotToSyncCommitteePeriod(slot)
|
||||
periodStartEpoch := chainTime.FirstEpochOfSyncPeriod(period)
|
||||
nextPeriod := period + 1
|
||||
nextPeriodStartEpoch := chainTime.FirstEpochOfSyncPeriod(nextPeriod)
|
||||
periodEndEpoch := nextPeriodStartEpoch - 1
|
||||
nextPeriodTimestamp := chainTime.StartOfEpoch(nextPeriodStartEpoch)
|
||||
|
||||
res.WriteString("Sync committee period: ")
|
||||
res.WriteString(fmt.Sprintf("%d", period))
|
||||
res.WriteString("\n")
|
||||
|
||||
if verbose {
|
||||
distance := slot - info.GetJustifiedSlot()
|
||||
fmt.Printf("Justified slot distance: %d\n", distance)
|
||||
}
|
||||
fmt.Printf("Finalized slot: %d\n", info.GetFinalizedSlot())
|
||||
if verbose {
|
||||
distance := slot - info.GetFinalizedSlot()
|
||||
fmt.Printf("Finalized slot distance: %d\n", distance)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Prior justified slot: %d\n", info.GetFinalizedSlot())
|
||||
distance := slot - info.GetPreviousJustifiedSlot()
|
||||
fmt.Printf("Prior justified slot distance: %d\n", distance)
|
||||
}
|
||||
} else {
|
||||
slotsPerEpoch := config["SlotsPerEpoch"].(uint64)
|
||||
epoch := slot / slotsPerEpoch
|
||||
fmt.Printf("Current epoch: %d\n", epoch)
|
||||
fmt.Printf("Justified epoch: %d\n", info.GetJustifiedSlot()/slotsPerEpoch)
|
||||
if verbose {
|
||||
distance := (slot - info.GetJustifiedSlot()) / slotsPerEpoch
|
||||
fmt.Printf("Justified epoch distance %d\n", distance)
|
||||
}
|
||||
fmt.Printf("Finalized epoch: %d\n", info.GetFinalizedSlot()/slotsPerEpoch)
|
||||
if verbose {
|
||||
distance := (slot - info.GetFinalizedSlot()) / slotsPerEpoch
|
||||
fmt.Printf("Finalized epoch distance: %d\n", distance)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Prior justified epoch: %d\n", info.GetPreviousJustifiedEpoch())
|
||||
distance := (slot - info.GetPreviousJustifiedEpoch()) / slotsPerEpoch
|
||||
fmt.Printf("Prior justified epoch distance: %d\n", distance)
|
||||
res.WriteString("Sync committee epochs: ")
|
||||
res.WriteString(fmt.Sprintf("%d", periodStartEpoch))
|
||||
res.WriteString("-")
|
||||
res.WriteString(fmt.Sprintf("%d", periodEndEpoch))
|
||||
res.WriteString("\n")
|
||||
|
||||
res.WriteString("Time until next sync committee period: ")
|
||||
res.WriteString(time.Until(nextPeriodTimestamp).Round(time.Second).String())
|
||||
res.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print(res.String())
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
}
|
||||
@@ -94,6 +150,4 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainStatusCmd)
|
||||
chainFlags(chainStatusCmd)
|
||||
chainStatusCmd.Flags().BoolVar(&chainStatusSlot, "slot", false, "Print slot-based values")
|
||||
|
||||
}
|
||||
|
||||
60
cmd/chaintime.go
Normal file
60
cmd/chaintime.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
chaintime "github.com/wealdtech/ethdo/cmd/chain/time"
|
||||
)
|
||||
|
||||
var chainTimeCmd = &cobra.Command{
|
||||
Use: "time",
|
||||
Short: "Obtain info about the chain at a given time",
|
||||
Long: `Obtain info about the chain at a given time. For example:
|
||||
|
||||
ethdo chain time --slot=12345`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := chaintime.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Print(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainTimeCmd)
|
||||
chainFlags(chainTimeCmd)
|
||||
chainTimeCmd.Flags().String("slot", "", "The slot for which to obtain information")
|
||||
chainTimeCmd.Flags().String("epoch", "", "The epoch for which to obtain information")
|
||||
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS+ZZZZ)")
|
||||
}
|
||||
|
||||
func chainTimeBindings() {
|
||||
if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
45
cmd/chainverify.go
Normal file
45
cmd/chainverify.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// chainVerifyCmd represents the chain verify command
|
||||
var chainVerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify a beacon chain signature",
|
||||
Long: "Verify the signature for a given beacon chain structure is correct",
|
||||
}
|
||||
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainVerifyCmd)
|
||||
}
|
||||
|
||||
func chainVerifyFlags(cmd *cobra.Command) {
|
||||
chainFlags(cmd)
|
||||
cmd.Flags().String("validator", "", "The account, public key or index of the validator")
|
||||
cmd.Flags().String("data", "", "The data to verify, as a JSON structure")
|
||||
}
|
||||
|
||||
func chainVerifyBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("data", cmd.Flags().Lookup("data")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
50
cmd/chainverifysignedcontributionandproof.go
Normal file
50
cmd/chainverifysignedcontributionandproof.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
chainverifysignedcontributionandproof "github.com/wealdtech/ethdo/cmd/chain/verify/signedcontributionandproof"
|
||||
)
|
||||
|
||||
var chainVerifySignedContributionAndProofCmd = &cobra.Command{
|
||||
Use: "signedcontributionandproof",
|
||||
Short: "Verify a signed contribution and proof",
|
||||
Long: `Verify a signed contribution and proof. For example:
|
||||
|
||||
ethdo chain verify signedcontributionandproof --data=... --validator=...
|
||||
|
||||
validator can be an account, a public key or an index.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := chainverifysignedcontributionandproof.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Print(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chainVerifyCmd.AddCommand(chainVerifySignedContributionAndProofCmd)
|
||||
chainVerifyFlags(chainVerifySignedContributionAndProofCmd)
|
||||
}
|
||||
|
||||
func chainVerifySignedContributionAndProofBindings(cmd *cobra.Command) {
|
||||
chainVerifyBindings(cmd)
|
||||
}
|
||||
32
cmd/deposit.go
Normal file
32
cmd/deposit.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright © 2019 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// depositCmd represents the deposit command
|
||||
var depositCmd = &cobra.Command{
|
||||
Use: "deposit",
|
||||
Short: "Manage Ethereum 2 deposits",
|
||||
Long: `Manage Ethereum 2 deposits.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(depositCmd)
|
||||
}
|
||||
|
||||
func depositFlags(cmd *cobra.Command) {
|
||||
}
|
||||
278
cmd/depositverify.go
Normal file
278
cmd/depositverify.go
Normal file
@@ -0,0 +1,278 @@
|
||||
// Copyright © 2019-2021 Weald Technology 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 cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
eth2util "github.com/wealdtech/go-eth2-util"
|
||||
string2eth "github.com/wealdtech/go-string2eth"
|
||||
)
|
||||
|
||||
var depositVerifyData string
|
||||
var depositVerifyWithdrawalPubKey string
|
||||
var depositVerifyWithdrawalAddress string
|
||||
var depositVerifyValidatorPubKey string
|
||||
var depositVerifyDepositAmount string
|
||||
var depositVerifyForkVersion string
|
||||
|
||||
var depositVerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify deposit data matches the provided data",
|
||||
Long: `Verify deposit data matches the provided input data. For example:
|
||||
|
||||
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --value="32 Ether"
|
||||
|
||||
The deposit data is compared to the supplied withdrawal account/public key, validator public key, and value to ensure they match.
|
||||
|
||||
In quiet mode this will return 0 if the the data is verified correctly, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(depositVerifyData != "", "--data is required")
|
||||
var data []byte
|
||||
var err error
|
||||
// Input could be JSON or a path to JSON.
|
||||
switch {
|
||||
case strings.HasPrefix(depositVerifyData, "0x"):
|
||||
// Looks like raw binary.
|
||||
data = []byte(depositVerifyData)
|
||||
case strings.HasPrefix(depositVerifyData, "{"):
|
||||
// Looks like JSON.
|
||||
data = []byte("[" + depositVerifyData + "]")
|
||||
case strings.HasPrefix(depositVerifyData, "["):
|
||||
// Looks like JSON array.
|
||||
data = []byte(depositVerifyData)
|
||||
default:
|
||||
// Assume it's a path to JSON.
|
||||
data, err = ioutil.ReadFile(depositVerifyData)
|
||||
errCheck(err, "Failed to read deposit data file")
|
||||
if data[0] == '{' {
|
||||
data = []byte("[" + string(data) + "]")
|
||||
}
|
||||
}
|
||||
|
||||
deposits, err := util.DepositInfoFromJSON(data)
|
||||
errCheck(err, "Failed to fetch deposit data")
|
||||
|
||||
var withdrawalCredentials []byte
|
||||
if depositVerifyWithdrawalPubKey != "" {
|
||||
withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(depositVerifyWithdrawalPubKey, "0x"))
|
||||
errCheck(err, "Invalid withdrawal public key")
|
||||
assert(len(withdrawalPubKeyBytes) == 48, "Public key should be 48 bytes")
|
||||
withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
|
||||
errCheck(err, "Value supplied with --withdrawalpubkey is not a valid public key")
|
||||
withdrawalCredentials = eth2util.SHA256(withdrawalPubKey.Marshal())
|
||||
withdrawalCredentials[0] = 0x00 // BLS_WITHDRAWAL_PREFIX
|
||||
} else if depositVerifyWithdrawalAddress != "" {
|
||||
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(depositVerifyWithdrawalAddress, "0x"))
|
||||
errCheck(err, "Invalid withdrawal address")
|
||||
assert(len(withdrawalAddressBytes) == 20, "address should be 20 bytes")
|
||||
withdrawalCredentials = make([]byte, 32)
|
||||
withdrawalCredentials[0] = 0x01 // ETH1_ADDRESS_WITHDRAWAL_PREFIX
|
||||
copy(withdrawalCredentials[12:], withdrawalAddressBytes)
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
|
||||
|
||||
depositAmount := uint64(0)
|
||||
if depositVerifyDepositAmount != "" {
|
||||
depositAmount, err = string2eth.StringToGWei(depositVerifyDepositAmount)
|
||||
errCheck(err, "Invalid value")
|
||||
assert(depositAmount >= 1000000000, "deposit amount must be at least 1 Ether") // MIN_DEPOSIT_AMOUNT
|
||||
}
|
||||
|
||||
validatorPubKeys := make(map[[48]byte]bool)
|
||||
if depositVerifyValidatorPubKey != "" {
|
||||
validatorPubKeys, err = validatorPubKeysFromInput(depositVerifyValidatorPubKey)
|
||||
errCheck(err, "Failed to obtain validator public key(s))")
|
||||
}
|
||||
|
||||
failures := false
|
||||
for _, deposit := range deposits {
|
||||
if deposit.Amount == 0 {
|
||||
deposit.Amount = depositAmount
|
||||
}
|
||||
verified, err := verifyDeposit(deposit, withdrawalCredentials, validatorPubKeys, depositAmount)
|
||||
errCheck(err, fmt.Sprintf("Error attempting to verify deposit %q", deposit.Name))
|
||||
depositName := deposit.Name
|
||||
if depositName == "" {
|
||||
depositName = "Deposit"
|
||||
}
|
||||
if !verified {
|
||||
failures = true
|
||||
outputIf(!quiet, fmt.Sprintf("%s failed verification", depositName))
|
||||
} else {
|
||||
outputIf(!quiet, fmt.Sprintf("%s verified", depositName))
|
||||
}
|
||||
}
|
||||
|
||||
if failures {
|
||||
os.Exit(_exitFailure)
|
||||
}
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
}
|
||||
|
||||
func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
|
||||
pubKeys := make(map[[48]byte]bool)
|
||||
var err error
|
||||
var data []byte
|
||||
// Input could be a public key or a path to public keys.
|
||||
if strings.HasPrefix(input, "0x") {
|
||||
// Looks like a public key.
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "public key is not a hex string")
|
||||
}
|
||||
if len(pubKeyBytes) != 48 {
|
||||
return nil, errors.New("public key should be 48 bytes")
|
||||
}
|
||||
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid public key")
|
||||
}
|
||||
var key [48]byte
|
||||
copy(key[:], pubKey.Marshal())
|
||||
pubKeys[key] = true
|
||||
} else {
|
||||
// Assume it's a path to a file of public keys.
|
||||
data, err = ioutil.ReadFile(input)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find public key file")
|
||||
}
|
||||
lines := bytes.Split(bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")), []byte("\n"))
|
||||
if len(lines) == 0 {
|
||||
return nil, errors.New("file has no public keys")
|
||||
}
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(string(line), "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "public key is not a hex string")
|
||||
}
|
||||
if len(pubKeyBytes) != 48 {
|
||||
return nil, errors.New("public key should be 48 bytes")
|
||||
}
|
||||
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid public key")
|
||||
}
|
||||
var key [48]byte
|
||||
copy(key[:], pubKey.Marshal())
|
||||
pubKeys[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
return pubKeys, nil
|
||||
}
|
||||
|
||||
func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, validatorPubKeys map[[48]byte]bool, amount uint64) (bool, error) {
|
||||
if withdrawalCredentials == nil {
|
||||
outputIf(!quiet, "Withdrawal public key or address not supplied; withdrawal credentials NOT checked")
|
||||
} else {
|
||||
if !bytes.Equal(deposit.WithdrawalCredentials, withdrawalCredentials) {
|
||||
outputIf(!quiet, "Withdrawal credentials incorrect")
|
||||
return false, nil
|
||||
}
|
||||
outputIf(!quiet, "Withdrawal credentials verified")
|
||||
}
|
||||
if amount == 0 {
|
||||
outputIf(!quiet, "Amount not supplied; NOT checked")
|
||||
} else {
|
||||
if deposit.Amount != amount {
|
||||
outputIf(!quiet, "Amount incorrect")
|
||||
return false, nil
|
||||
}
|
||||
outputIf(!quiet, "Amount verified")
|
||||
}
|
||||
|
||||
if len(validatorPubKeys) == 0 {
|
||||
outputIf(!quiet, "Validator public key not suppled; NOT checked")
|
||||
} else {
|
||||
var key [48]byte
|
||||
copy(key[:], deposit.PublicKey)
|
||||
if _, exists := validatorPubKeys[key]; !exists {
|
||||
outputIf(!quiet, "Validator public key incorrect")
|
||||
return false, nil
|
||||
}
|
||||
outputIf(!quiet, "Validator public key verified")
|
||||
}
|
||||
|
||||
var pubKey spec.BLSPubKey
|
||||
copy(pubKey[:], deposit.PublicKey)
|
||||
var signature spec.BLSSignature
|
||||
copy(signature[:], deposit.Signature)
|
||||
|
||||
depositData := &spec.DepositData{
|
||||
PublicKey: pubKey,
|
||||
WithdrawalCredentials: deposit.WithdrawalCredentials,
|
||||
Amount: spec.Gwei(deposit.Amount),
|
||||
Signature: signature,
|
||||
}
|
||||
depositDataRoot, err := depositData.HashTreeRoot()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate deposit data root")
|
||||
}
|
||||
|
||||
if bytes.Equal(deposit.DepositDataRoot, depositDataRoot[:]) {
|
||||
outputIf(!quiet, "Deposit data root verified")
|
||||
} else {
|
||||
outputIf(!quiet, "Deposit data root incorrect")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(deposit.ForkVersion) == 0 {
|
||||
if depositVerifyForkVersion != "" {
|
||||
outputIf(!quiet, "Data format does not contain fork version for verification; NOT verified")
|
||||
}
|
||||
} else {
|
||||
if depositVerifyForkVersion == "" {
|
||||
outputIf(!quiet, "fork version not supplied; NOT checked")
|
||||
} else {
|
||||
forkVersion, err := hex.DecodeString(strings.TrimPrefix(depositVerifyForkVersion, "0x"))
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to decode fork version")
|
||||
}
|
||||
if bytes.Equal(deposit.ForkVersion, forkVersion) {
|
||||
outputIf(!quiet, "Fork version verified")
|
||||
} else {
|
||||
outputIf(!quiet, "Fork version incorrect")
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
depositCmd.AddCommand(depositVerifyCmd)
|
||||
depositFlags(depositVerifyCmd)
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyData, "data", "", "JSON data, or path to JSON data")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalPubKey, "withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalAddress, "withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyDepositAmount, "depositvalue", "32 Ether", "Value of the amount to be deposited")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyValidatorPubKey, "validatorpubkey", "", "Public key(s) of the account(s) that will be carrying out validation")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyForkVersion, "forkversion", "0x00000000", "Fork version of the chain of the deposit")
|
||||
}
|
||||
@@ -57,10 +57,10 @@ func assert(condition bool, msg string) {
|
||||
|
||||
// die prints an error and quits
|
||||
func die(msg string) {
|
||||
if !quiet {
|
||||
if msg != "" && !quiet {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", msg)
|
||||
}
|
||||
os.Exit(1)
|
||||
os.Exit(_exitFailure)
|
||||
}
|
||||
|
||||
// warnCheck checks for an error and warns if it is present
|
||||
|
||||
32
cmd/exit.go
Normal file
32
cmd/exit.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright © 2019 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// exitCmd represents the exit command
|
||||
var exitCmd = &cobra.Command{
|
||||
Use: "exit",
|
||||
Short: "Manage Ethereum 2 voluntary exits",
|
||||
Long: `Manage Ethereum 2 voluntary exits.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(exitCmd)
|
||||
}
|
||||
|
||||
func exitFlags(cmd *cobra.Command) {
|
||||
}
|
||||
147
cmd/exitverify.go
Normal file
147
cmd/exitverify.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var exitVerifyPubKey string
|
||||
|
||||
var exitVerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify exit data is valid",
|
||||
Long: `Verify that exit data generated by "ethdo validator exit" is correct for a given account. For example:
|
||||
|
||||
ethdo exit verify --data=exitdata.json --account=primary/current
|
||||
|
||||
In quiet mode this will return 0 if the the exit is verified correctly, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
|
||||
assert(viper.GetString("account") != "" || exitVerifyPubKey != "", "account or public key is required")
|
||||
account, err := exitVerifyAccount(ctx)
|
||||
errCheck(err, "Failed to obtain account")
|
||||
|
||||
assert(viper.GetString("exit") != "", "exit is required")
|
||||
data, err := obtainExitData(viper.GetString("exit"))
|
||||
errCheck(err, "Failed to obtain exit data")
|
||||
|
||||
// Confirm signature is good.
|
||||
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
errCheck(err, "Failed to obtain beacon chain genesis")
|
||||
|
||||
domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion[:], genesis.GenesisValidatorsRoot[:])
|
||||
var exitDomain spec.Domain
|
||||
copy(exitDomain[:], domain)
|
||||
exit := &spec.VoluntaryExit{
|
||||
Epoch: data.Exit.Message.Epoch,
|
||||
ValidatorIndex: data.Exit.Message.ValidatorIndex,
|
||||
}
|
||||
exitRoot, err := exit.HashTreeRoot()
|
||||
errCheck(err, "Failed to obtain exit hash tree root")
|
||||
signatureBytes := make([]byte, 96)
|
||||
copy(signatureBytes, data.Exit.Signature[:])
|
||||
sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
|
||||
errCheck(err, "Invalid signature")
|
||||
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
|
||||
errCheck(err, "Failed to verify voluntary exit")
|
||||
assert(verified, "Voluntary exit failed to verify")
|
||||
|
||||
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
|
||||
errCheck(err, "Failed to obtain current fork")
|
||||
assert(bytes.Equal(data.ForkVersion[:], fork.CurrentVersion[:]) || bytes.Equal(data.ForkVersion[:], fork.PreviousVersion[:]), "Exit is for an old fork version and is no longer valid")
|
||||
|
||||
outputIf(verbose, "Verified")
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
}
|
||||
|
||||
// obtainExitData obtains exit data from an input, could be JSON itself or a path to JSON.
|
||||
func obtainExitData(input string) (*util.ValidatorExitData, error) {
|
||||
var err error
|
||||
var data []byte
|
||||
// Input could be JSON or a path to JSON
|
||||
if strings.HasPrefix(input, "{") {
|
||||
// Looks like JSON
|
||||
data = []byte(input)
|
||||
} else {
|
||||
// Assume it's a path to JSON
|
||||
data, err = ioutil.ReadFile(input)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find deposit data file")
|
||||
}
|
||||
}
|
||||
exitData := &util.ValidatorExitData{}
|
||||
err = json.Unmarshal(data, exitData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "data is not valid JSON")
|
||||
}
|
||||
|
||||
return exitData, nil
|
||||
}
|
||||
|
||||
// exitVerifyAccount obtains the account for the exitVerify command.
|
||||
func exitVerifyAccount(ctx context.Context) (e2wtypes.Account, error) {
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
if viper.GetString("account") != "" {
|
||||
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account")
|
||||
}
|
||||
} else {
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(exitVerifyPubKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", exitVerifyPubKey))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", exitVerifyPubKey))
|
||||
}
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
exitCmd.AddCommand(exitVerifyCmd)
|
||||
exitFlags(exitVerifyCmd)
|
||||
exitVerifyCmd.Flags().String("exit", "", "JSON data, or path to JSON data")
|
||||
exitVerifyCmd.Flags().StringVar(&exitVerifyPubKey, "pubkey", "", "Public key for which to verify exit")
|
||||
}
|
||||
|
||||
func exitVerifyBindings() {
|
||||
if err := viper.BindPFlag("exit", exitVerifyCmd.Flags().Lookup("exit")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
59
cmd/node/events/input.go
Normal file
59
cmd/node/events/input.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 nodeevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
// Operation.
|
||||
topics []string
|
||||
eth2Client eth2client.Service
|
||||
jsonOutput bool
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.jsonOutput = viper.GetBool("json")
|
||||
|
||||
data.topics = viper.GetStringSlice("topics")
|
||||
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
109
cmd/node/events/input_internal_test.go
Normal file
109
cmd/node/events/input_internal_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 nodeevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
|
||||
},
|
||||
{
|
||||
name: "ConnectionBad",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": "localhost:1",
|
||||
"topics": []string{"one", "two"},
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
},
|
||||
{
|
||||
name: "TopicsNil",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
require.Equal(t, test.res.topics, res.topics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/node/events/process.go
Normal file
50
cmd/node/events/process.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 nodeevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) error {
|
||||
if data == nil {
|
||||
return errors.New("no data")
|
||||
}
|
||||
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, data.topics, eventHandler)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect for events")
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func eventHandler(event *api.Event) {
|
||||
if event.Data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(event)
|
||||
if err == nil {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
74
cmd/node/events/process_internal_test.go
Normal file
74
cmd/node/events/process_internal_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 nodeevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/auto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
os.Setenv("ETHDO_ALLOW_INSECURE_CONNECTIONS", "true")
|
||||
|
||||
eth2Client, err := auto.New(context.Background(),
|
||||
auto.WithLogLevel(zerolog.Disabled),
|
||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "TopicsNil",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
},
|
||||
err: "failed to connect for events: no topics supplied",
|
||||
},
|
||||
{
|
||||
name: "TopicsUnknown",
|
||||
dataIn: &dataIn{
|
||||
eth2Client: eth2Client,
|
||||
topics: []string{"foo"},
|
||||
},
|
||||
err: "failed to connect for events: unsupported event topic foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
cmd/node/events/run.go
Normal file
41
cmd/node/events/run.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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 nodeevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
if err := process(ctx, dataIn); err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
// Process generates all output.
|
||||
|
||||
return "", nil
|
||||
}
|
||||
52
cmd/nodeevents.go
Normal file
52
cmd/nodeevents.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright © 2019 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
nodeevents "github.com/wealdtech/ethdo/cmd/node/events"
|
||||
)
|
||||
|
||||
var nodeEventsCmd = &cobra.Command{
|
||||
Use: "events",
|
||||
Short: "Report events from a node",
|
||||
Long: `Report events from a node. For example:
|
||||
|
||||
ethdo node events --events=head,chain_reorg.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := nodeevents.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
nodeCmd.AddCommand(nodeEventsCmd)
|
||||
nodeFlags(nodeEventsCmd)
|
||||
nodeEventsCmd.Flags().StringSlice("topics", nil, "The topics of events for which to listen (attestation,block,chain_reorg,finalized_checkpoint,head,voluntary_exit)")
|
||||
}
|
||||
|
||||
func nodeEventsBindings() {
|
||||
if err := viper.BindPFlag("topics", nodeEventsCmd.Flags().Lookup("topics")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/ethdo/grpc"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
var nodeInfoCmd = &cobra.Command{
|
||||
@@ -31,34 +33,24 @@ var nodeInfoCmd = &cobra.Command{
|
||||
|
||||
In quiet mode this will return 0 if the node information can be obtained, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := connect()
|
||||
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
|
||||
config, err := grpc.FetchChainConfig(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain beacon chain configuration")
|
||||
ctx := context.Background()
|
||||
|
||||
genesisTime, err := grpc.FetchGenesis(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain genesis time")
|
||||
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
if quiet {
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
version, metadata, err := grpc.FetchVersion(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain version")
|
||||
version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx)
|
||||
errCheck(err, "Failed to obtain node version")
|
||||
fmt.Printf("Version: %s\n", version)
|
||||
if metadata != "" {
|
||||
fmt.Printf("Metadata: %s\n", metadata)
|
||||
}
|
||||
}
|
||||
syncing, err := grpc.FetchSyncing(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain syncing state")
|
||||
fmt.Printf("Syncing: %v\n", syncing)
|
||||
|
||||
slot := timestampToSlot(genesisTime.Unix(), time.Now().Unix(), config["SecondsPerSlot"].(uint64))
|
||||
fmt.Printf("Current slot: %d\n", slot)
|
||||
fmt.Printf("Current epoch: %d\n", slot/config["SlotsPerEpoch"].(uint64))
|
||||
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesisTime.Unix()))
|
||||
syncState, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx)
|
||||
errCheck(err, "failed to obtain node sync state")
|
||||
fmt.Printf("Syncing: %t\n", syncState.SyncDistance != 0)
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
|
||||
28
cmd/passphrases.go
Normal file
28
cmd/passphrases.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright © 2019 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// getWalletPassphrases() fetches the wallet passphrase supplied by the user.
|
||||
func getWalletPassphrase() string {
|
||||
return viper.GetString("wallet-passphrase")
|
||||
}
|
||||
|
||||
// getPassphrases() fetches the passphrases supplied by the user.
|
||||
func getPassphrases() []string {
|
||||
return viper.GetStringSlice("passphrase")
|
||||
}
|
||||
29
cmd/proposer.go
Normal file
29
cmd/proposer.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// proposerCmd represents the proposer command
|
||||
var proposerCmd = &cobra.Command{
|
||||
Use: "proposer",
|
||||
Short: "Obtain information about Ethereum 2 proposers",
|
||||
Long: "Obtain information about Ethereum 2 proposers",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(proposerCmd)
|
||||
}
|
||||
451
cmd/root.go
451
cmd/root.go
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019 Weald Technology Trading
|
||||
// Copyright © 2019 - 2021 Weald Technology Trading.
|
||||
// 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
|
||||
@@ -15,27 +15,22 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-ssz"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
dirk "github.com/wealdtech/go-eth2-wallet-dirk"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
@@ -43,96 +38,92 @@ var quiet bool
|
||||
var verbose bool
|
||||
var debug bool
|
||||
|
||||
// For transaction commands.
|
||||
var wait bool
|
||||
var generate bool
|
||||
|
||||
// Root variables, present for all commands.
|
||||
var rootStore string
|
||||
var rootAccount string
|
||||
var rootStorePassphrase string
|
||||
var rootWalletPassphrase string
|
||||
var rootAccountPassphrase string
|
||||
|
||||
// Remote connection.
|
||||
var remote bool
|
||||
var remoteAddr string
|
||||
var clientCert string
|
||||
var clientKey string
|
||||
var serverCACert string
|
||||
var remoteGRPCConn *grpc.ClientConn
|
||||
|
||||
// Prysm connection.
|
||||
var eth2GRPCConn *grpc.ClientConn
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "ethdo",
|
||||
Short: "Ethereum 2 CLI",
|
||||
Long: `Manage common Ethereum 2 tasks from the command line.`,
|
||||
PersistentPreRun: persistentPreRun,
|
||||
Use: "ethdo",
|
||||
Short: "Ethereum 2 CLI",
|
||||
Long: `Manage common Ethereum 2 tasks from the command line.`,
|
||||
PersistentPreRunE: persistentPreRunE,
|
||||
}
|
||||
|
||||
func persistentPreRun(cmd *cobra.Command, args []string) {
|
||||
func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Name() == "help" {
|
||||
// User just wants help
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.Name() == "version" {
|
||||
// User just wants the version
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// We bind viper here so that we bind to the correct command
|
||||
// Disable service logging.
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
|
||||
// We bind viper here so that we bind to the correct command.
|
||||
quiet = viper.GetBool("quiet")
|
||||
verbose = viper.GetBool("verbose")
|
||||
debug = viper.GetBool("debug")
|
||||
rootStore = viper.GetString("store")
|
||||
rootAccount = viper.GetString("account")
|
||||
rootStorePassphrase = viper.GetString("storepassphrase")
|
||||
rootWalletPassphrase = viper.GetString("walletpassphrase")
|
||||
rootAccountPassphrase = viper.GetString("passphrase")
|
||||
|
||||
// ...lots of commands have transaction-related flags (e.g.) 'wait'
|
||||
// as options but we want to bind them to this particular command and
|
||||
// this is the first chance we get
|
||||
if cmd.Flags().Lookup("wait") != nil {
|
||||
err := viper.BindPFlag("wait", cmd.Flags().Lookup("wait"))
|
||||
errCheck(err, "Failed to set wait option")
|
||||
// Command-specific bindings.
|
||||
switch commandPath(cmd) {
|
||||
case "account/create":
|
||||
accountCreateBindings()
|
||||
case "account/derive":
|
||||
accountDeriveBindings()
|
||||
case "account/import":
|
||||
accountImportBindings()
|
||||
case "attester/duties":
|
||||
attesterDutiesBindings()
|
||||
case "attester/inclusion":
|
||||
attesterInclusionBindings()
|
||||
case "block/info":
|
||||
blockInfoBindings()
|
||||
case "chain/time":
|
||||
chainTimeBindings()
|
||||
case "chain/verify/signedcontributionandproof":
|
||||
chainVerifySignedContributionAndProofBindings(cmd)
|
||||
case "exit/verify":
|
||||
exitVerifyBindings()
|
||||
case "node/events":
|
||||
nodeEventsBindings()
|
||||
case "slot/time":
|
||||
slotTimeBindings()
|
||||
case "synccommittee/members":
|
||||
synccommitteeMembersBindings()
|
||||
case "validator/depositdata":
|
||||
validatorDepositdataBindings()
|
||||
case "validator/duties":
|
||||
validatorDutiesBindings()
|
||||
case "validator/exit":
|
||||
validatorExitBindings()
|
||||
case "validator/info":
|
||||
validatorInfoBindings()
|
||||
case "validator/keycheck":
|
||||
validatorKeycheckBindings()
|
||||
case "wallet/create":
|
||||
walletCreateBindings()
|
||||
case "wallet/import":
|
||||
walletImportBindings()
|
||||
case "wallet/sharedexport":
|
||||
walletSharedExportBindings()
|
||||
case "wallet/sharedimport":
|
||||
walletSharedImportBindings()
|
||||
}
|
||||
wait = viper.GetBool("wait")
|
||||
if cmd.Flags().Lookup("generate") != nil {
|
||||
err := viper.BindPFlag("generate", cmd.Flags().Lookup("generate"))
|
||||
errCheck(err, "Failed to set generate option")
|
||||
}
|
||||
generate = viper.GetBool("generate")
|
||||
|
||||
if quiet && verbose {
|
||||
die("Cannot supply both quiet and verbose flags")
|
||||
fmt.Println("Cannot supply both quiet and verbose flags")
|
||||
}
|
||||
if quiet && debug {
|
||||
die("Cannot supply both quiet and debug flags")
|
||||
}
|
||||
if generate && wait {
|
||||
die("Cannot supply both generate and wait flags")
|
||||
fmt.Println("Cannot supply both quiet and debug flags")
|
||||
}
|
||||
|
||||
if viper.GetString("remote") == "" {
|
||||
// Set up our wallet store
|
||||
err := wallet.SetStore(rootStore, []byte(rootStorePassphrase))
|
||||
errCheck(err, "Failed to set up wallet store")
|
||||
} else {
|
||||
err := initRemote()
|
||||
errCheck(err, "Failed to connect to remote wallet")
|
||||
}
|
||||
return util.SetupStore()
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := RootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(_exitFailure)
|
||||
}
|
||||
}
|
||||
@@ -159,15 +150,40 @@ func init() {
|
||||
if err := viper.BindPFlag("account", RootCmd.PersistentFlags().Lookup("account")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("basedir", "", "Base directory for filesystem wallets")
|
||||
if err := viper.BindPFlag("basedir", RootCmd.PersistentFlags().Lookup("basedir")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := RootCmd.PersistentFlags().MarkDeprecated("basedir", "use --base-dir"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("base-dir", "", "Base directory for filesystem wallets")
|
||||
if err := viper.BindPFlag("base-dir", RootCmd.PersistentFlags().Lookup("base-dir")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("storepassphrase", "", "Passphrase for store (if applicable)")
|
||||
if err := viper.BindPFlag("storepassphrase", RootCmd.PersistentFlags().Lookup("storepassphrase")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := RootCmd.PersistentFlags().MarkDeprecated("storepassphrase", "use --store-passphrase"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("store-passphrase", "", "Passphrase for store (if applicable)")
|
||||
if err := viper.BindPFlag("store-passphrase", RootCmd.PersistentFlags().Lookup("store-passphrase")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("walletpassphrase", "", "Passphrase for wallet (if applicable)")
|
||||
if err := viper.BindPFlag("walletpassphrase", RootCmd.PersistentFlags().Lookup("walletpassphrase")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("passphrase", "", "Passphrase for account (if applicable)")
|
||||
if err := RootCmd.PersistentFlags().MarkDeprecated("walletpassphrase", "use --wallet-passphrase"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("wallet-passphrase", "", "Passphrase for wallet (if applicable)")
|
||||
if err := viper.BindPFlag("wallet-passphrase", RootCmd.PersistentFlags().Lookup("wallet-passphrase")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().StringSlice("passphrase", nil, "Passphrase for account (if applicable)")
|
||||
if err := viper.BindPFlag("passphrase", RootCmd.PersistentFlags().Lookup("passphrase")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -183,7 +199,7 @@ func init() {
|
||||
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to Ethereum 2 node via GRPC")
|
||||
RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to an Ethereum 2 node")
|
||||
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -207,6 +223,14 @@ func init() {
|
||||
if err := viper.BindPFlag("server-ca-cert", RootCmd.PersistentFlags().Lookup("server-ca-cert")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().Bool("allow-weak-passphrases", false, "allow passphrases that use common words, are short, or generally considered weak")
|
||||
if err := viper.BindPFlag("allow-weak-passphrases", RootCmd.PersistentFlags().Lookup("allow-weak-passphrases")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().Bool("allow-insecure-connections", false, "allow insecure connections to remote beacon nodes")
|
||||
if err := viper.BindPFlag("allow-insecure-connections", RootCmd.PersistentFlags().Lookup("allow-insecure-connections")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
@@ -225,6 +249,7 @@ func initConfig() {
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("ETHDO")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
@@ -244,214 +269,136 @@ func outputIf(condition bool, msg string) {
|
||||
}
|
||||
}
|
||||
|
||||
// walletAndAccountNamesFromPath breaks a path in to wallet and account names.
|
||||
func walletAndAccountNamesFromPath(path string) (string, string, error) {
|
||||
if len(path) == 0 {
|
||||
return "", "", errors.New("invalid account format")
|
||||
// walletFromInput obtains a wallet given the information in the viper variable
|
||||
// "account", or if not present the viper variable "wallet".
|
||||
func walletFromInput(ctx context.Context) (e2wtypes.Wallet, error) {
|
||||
if viper.GetString("account") != "" {
|
||||
return walletFromPath(ctx, viper.GetString("account"))
|
||||
}
|
||||
index := strings.Index(path, "/")
|
||||
if index == -1 {
|
||||
// Just the wallet
|
||||
return path, "", nil
|
||||
}
|
||||
if index == len(path)-1 {
|
||||
// Trailing /
|
||||
return path[:index], "", nil
|
||||
}
|
||||
return path[:index], path[index+1:], nil
|
||||
return walletFromPath(ctx, viper.GetString("wallet"))
|
||||
}
|
||||
|
||||
// walletFromPath obtains a wallet given a path specification.
|
||||
func walletFromPath(path string) (wtypes.Wallet, error) {
|
||||
walletName, _, err := walletAndAccountNamesFromPath(path)
|
||||
func walletFromPath(ctx context.Context, path string) (e2wtypes.Wallet, error) {
|
||||
walletName, _, err := e2wallet.WalletAndAccountNames(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, err := wallet.OpenWallet(walletName)
|
||||
if viper.GetString("remote") != "" {
|
||||
assert(viper.GetString("client-cert") != "", "remote connections require client-cert")
|
||||
assert(viper.GetString("client-key") != "", "remote connections require client-key")
|
||||
credentials, err := dirk.ComposeCredentials(ctx, viper.GetString("client-cert"), viper.GetString("client-key"), viper.GetString("server-ca-cert"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to build dirk credentials")
|
||||
}
|
||||
|
||||
endpoints, err := remotesToEndpoints([]string{viper.GetString("remote")})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse remote servers")
|
||||
}
|
||||
|
||||
return dirk.OpenWallet(ctx, walletName, credentials, endpoints)
|
||||
}
|
||||
wallet, err := e2wallet.OpenWallet(walletName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "failed to decrypt wallet") {
|
||||
return nil, errors.New("Incorrect store passphrase")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return w, nil
|
||||
return wallet, nil
|
||||
}
|
||||
|
||||
// accountFromPath obtains an account given a path specification.
|
||||
func accountFromPath(path string) (wtypes.Account, error) {
|
||||
wallet, err := walletFromPath(path)
|
||||
// walletAndAccountFromInput obtains the wallet and account given the information in the viper variable "account".
|
||||
func walletAndAccountFromInput(ctx context.Context) (e2wtypes.Wallet, e2wtypes.Account, error) {
|
||||
return walletAndAccountFromPath(ctx, viper.GetString("account"))
|
||||
}
|
||||
|
||||
// walletAndAccountFromPath obtains the wallet and account given a path specification.
|
||||
func walletAndAccountFromPath(ctx context.Context, path string) (e2wtypes.Wallet, e2wtypes.Account, error) {
|
||||
wallet, err := walletFromPath(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, errors.Wrap(err, "failed to open wallet for account")
|
||||
}
|
||||
_, accountName, err := walletAndAccountNamesFromPath(path)
|
||||
_, accountName, err := e2wallet.WalletAndAccountNames(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, errors.Wrap(err, "failed to obtain accout name")
|
||||
}
|
||||
if accountName == "" {
|
||||
return nil, errors.New("no account name")
|
||||
return nil, nil, errors.New("no account name")
|
||||
}
|
||||
|
||||
if wallet.Type() == "hierarchical deterministic" && strings.HasPrefix(accountName, "m/") && rootWalletPassphrase != "" {
|
||||
err = wallet.Unlock([]byte(rootWalletPassphrase))
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid wallet passphrase")
|
||||
if wallet.Type() == "hierarchical deterministic" && strings.HasPrefix(accountName, "m/") {
|
||||
assert(getWalletPassphrase() != "", "--walletpassphrase is required for direct path derivations")
|
||||
|
||||
locker, isLocker := wallet.(e2wtypes.WalletLocker)
|
||||
if isLocker {
|
||||
err = locker.Unlock(ctx, []byte(util.GetWalletPassphrase()))
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("failed to unlock wallet")
|
||||
}
|
||||
defer relockAccount(locker)
|
||||
}
|
||||
defer wallet.Lock()
|
||||
}
|
||||
return wallet.AccountByName(accountName)
|
||||
|
||||
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
|
||||
if !isAccountByNameProvider {
|
||||
return nil, nil, errors.New("wallet cannot obtain accounts by name")
|
||||
}
|
||||
account, err := accountByNameProvider.AccountByName(ctx, accountName)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to obtain account")
|
||||
}
|
||||
return wallet, account, nil
|
||||
}
|
||||
|
||||
// accountsFromPath obtains 0 or more accounts given a path specification.
|
||||
func accountsFromPath(path string) ([]wtypes.Account, error) {
|
||||
accounts := make([]wtypes.Account, 0)
|
||||
|
||||
// Quick check to see if it's a single account
|
||||
account, err := accountFromPath(path)
|
||||
if err == nil && account != nil {
|
||||
accounts = append(accounts, account)
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
wallet, err := walletFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, accountSpec, err := walletAndAccountNamesFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if accountSpec == "" {
|
||||
accountSpec = "^.*$"
|
||||
// bestPublicKey returns the best public key for operations.
|
||||
// It prefers the composite public key if present, otherwise the public key.
|
||||
func bestPublicKey(account e2wtypes.Account) (e2types.PublicKey, error) {
|
||||
var pubKey e2types.PublicKey
|
||||
publicKeyProvider, isCompositePublicKeyProvider := account.(e2wtypes.AccountCompositePublicKeyProvider)
|
||||
if isCompositePublicKeyProvider {
|
||||
pubKey = publicKeyProvider.CompositePublicKey()
|
||||
} else {
|
||||
accountSpec = fmt.Sprintf("^%s$", accountSpec)
|
||||
}
|
||||
re := regexp.MustCompile(accountSpec)
|
||||
|
||||
for account := range wallet.Accounts() {
|
||||
if re.Match([]byte(account.Name())) {
|
||||
accounts = append(accounts, account)
|
||||
publicKeyProvider, isPublicKeyProvider := account.(e2wtypes.AccountPublicKeyProvider)
|
||||
if isPublicKeyProvider {
|
||||
pubKey = publicKeyProvider.PublicKey()
|
||||
} else {
|
||||
return nil, errors.New("account does not provide a public key")
|
||||
}
|
||||
}
|
||||
|
||||
// Tidy up accounts by name.
|
||||
sort.Slice(accounts, func(i, j int) bool {
|
||||
return accounts[i].Name() < accounts[j].Name()
|
||||
})
|
||||
|
||||
return accounts, nil
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
// signStruct signs an arbitrary structure.
|
||||
func signStruct(account wtypes.Account, data interface{}, domain []byte) (e2types.Signature, error) {
|
||||
objRoot, err := ssz.HashTreeRoot(data)
|
||||
outputIf(debug, fmt.Sprintf("Object root is %x", objRoot))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return signRoot(account, objRoot, domain)
|
||||
}
|
||||
|
||||
// SigningContainer is the container for signing roots with a domain.
|
||||
// Contains SSZ sizes to allow for correct calculation of root.
|
||||
type SigningContainer struct {
|
||||
Root []byte `ssz-size:"32"`
|
||||
Domain []byte `ssz-size:"32"`
|
||||
}
|
||||
|
||||
// signRoot signs a root.
|
||||
func signRoot(account wtypes.Account, root [32]byte, domain []byte) (e2types.Signature, error) {
|
||||
container := &SigningContainer{
|
||||
Root: root[:],
|
||||
Domain: domain,
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Signing container:\n\troot: %x\n\tdomain: %x", container.Root, container.Domain))
|
||||
signingRoot, err := ssz.HashTreeRoot(container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Signing root: %x", signingRoot))
|
||||
return sign(account, signingRoot[:])
|
||||
}
|
||||
|
||||
// sign signs arbitrary data.
|
||||
func sign(account wtypes.Account, data []byte) (e2types.Signature, error) {
|
||||
if !account.IsUnlocked() {
|
||||
return nil, errors.New("account must be unlocked to sign")
|
||||
}
|
||||
|
||||
outputIf(debug, fmt.Sprintf("Signing %x (%d)", data, len(data)))
|
||||
return account.Sign(data)
|
||||
}
|
||||
|
||||
// addTransactionFlags adds flags used in all transactions.
|
||||
func addTransactionFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool("generate", false, "Do not send the transaction; generate and output as a hex string only")
|
||||
cmd.Flags().Bool("wait", false, "wait for the transaction to be mined before returning")
|
||||
}
|
||||
|
||||
// connect connects to an Ethereum 2 endpoint.
|
||||
func connect() error {
|
||||
connection := ""
|
||||
if viper.GetString("connection") != "" {
|
||||
connection = viper.GetString("connection")
|
||||
}
|
||||
|
||||
if connection == "" {
|
||||
return errors.New("no connection")
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Connecting to %s", connection))
|
||||
|
||||
opts := []grpc.DialOption{grpc.WithInsecure()}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
var err error
|
||||
eth2GRPCConn, err = grpc.DialContext(ctx, connection, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
func initRemote() error {
|
||||
remote = true
|
||||
remoteAddr = viper.GetString("remote")
|
||||
clientCert = viper.GetString("client-cert")
|
||||
assert(clientCert != "", "--remote requires --client-cert")
|
||||
clientKey = viper.GetString("client-key")
|
||||
assert(clientKey != "", "--remote requires --client-key")
|
||||
serverCACert = viper.GetString("server-ca-cert")
|
||||
assert(serverCACert != "", "--remote requires --server-ca-cert")
|
||||
|
||||
// Load the client certificates.
|
||||
clientPair, err := tls.LoadX509KeyPair(clientCert, clientKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to access client certificate/key")
|
||||
}
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{clientPair},
|
||||
}
|
||||
if serverCACert != "" {
|
||||
// Load the CA for the server certificate.
|
||||
serverCA, err := ioutil.ReadFile(serverCACert)
|
||||
// remotesToEndpoints generates endpoints from remote addresses.
|
||||
func remotesToEndpoints(remotes []string) ([]*dirk.Endpoint, error) {
|
||||
endpoints := make([]*dirk.Endpoint, 0)
|
||||
for _, remote := range remotes {
|
||||
parts := strings.Split(remote, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid remote %q", remote)
|
||||
}
|
||||
port, err := strconv.ParseUint(parts[1], 10, 32)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to access CA certificate")
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid port in remote %q", remote))
|
||||
}
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM(serverCA) {
|
||||
return errors.Wrap(err, "failed to add CA certificate")
|
||||
}
|
||||
tlsCfg.RootCAs = cp
|
||||
endpoints = append(endpoints, dirk.NewEndpoint(parts[0], uint32(port)))
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// relockAccount locks an account; generally called as a defer after an account is unlocked.
|
||||
func relockAccount(locker e2wtypes.AccountLocker) {
|
||||
errCheck(locker.Lock(context.Background()), "failed to re-lock account")
|
||||
}
|
||||
|
||||
func commandPath(cmd *cobra.Command) string {
|
||||
path := ""
|
||||
for {
|
||||
path = fmt.Sprintf("%s/%s", cmd.Name(), path)
|
||||
if cmd.Parent().Name() == "ethdo" {
|
||||
return strings.TrimRight(path, "/")
|
||||
}
|
||||
cmd = cmd.Parent()
|
||||
}
|
||||
|
||||
clientCreds := credentials.NewTLS(tlsCfg)
|
||||
|
||||
opts := []grpc.DialOption{
|
||||
// Require TLS.
|
||||
grpc.WithTransportCredentials(clientCreds),
|
||||
// Block until server responds.
|
||||
grpc.WithBlock(),
|
||||
}
|
||||
remoteGRPCConn, err = grpc.Dial(remoteAddr, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user