mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c91538644f | ||
|
|
3140fc5b8a | ||
|
|
a2afd37a97 | ||
|
|
d453ba9303 | ||
|
|
d0b278c0ec | ||
|
|
27a59c031b | ||
|
|
1bc139c591 | ||
|
|
7e8db1cd2e | ||
|
|
864bb30244 | ||
|
|
4209f725ba | ||
|
|
d01e789c8a | ||
|
|
85a0590d55 | ||
|
|
b9ba1ec1c2 | ||
|
|
29bffd0dbe | ||
|
|
e7a2c600f1 | ||
|
|
f2a5a93195 | ||
|
|
095c246efb | ||
|
|
581d22c7d7 | ||
|
|
1ca1e1f2d6 | ||
|
|
7c88b7c082 | ||
|
|
4d351e6d3d | ||
|
|
c46e9740d4 | ||
|
|
f9cb1054c0 | ||
|
|
4e2fa63f30 | ||
|
|
ca5eec9c8c | ||
|
|
582930d982 | ||
|
|
5a572abc6f | ||
|
|
a2d018c321 | ||
|
|
12f3154157 | ||
|
|
51689db315 | ||
|
|
1a8897ff0f | ||
|
|
5ee92bee78 | ||
|
|
eba7a0c88a | ||
|
|
f6a6224968 | ||
|
|
60e8878dc3 | ||
|
|
41d9160b63 | ||
|
|
b3d4c9af08 | ||
|
|
5a8a13d8f3 | ||
|
|
d94e1551a7 | ||
|
|
03e7f15d04 | ||
|
|
f14233ccea | ||
|
|
d5fc8fed37 | ||
|
|
2bd8fdee5e | ||
|
|
ed830d7901 | ||
|
|
6b127796f9 | ||
|
|
720edf8475 | ||
|
|
e27fae11e2 | ||
|
|
f5d31cf827 | ||
|
|
317c22366e | ||
|
|
98717b9da0 | ||
|
|
c95ba66a0b | ||
|
|
40a7c57abd | ||
|
|
7743481b8c | ||
|
|
6a5251c738 | ||
|
|
ceb5195b69 | ||
|
|
53f0de3fb9 | ||
|
|
16a94d726a | ||
|
|
9d00f6bafc | ||
|
|
d274ab3db0 | ||
|
|
ad3d8606fd | ||
|
|
30455e7c43 | ||
|
|
7eb2c68a19 | ||
|
|
2d96f7cb13 | ||
|
|
fd1e4a97bb | ||
|
|
be2270c543 | ||
|
|
0c36239b8b | ||
|
|
f78b2922ec | ||
|
|
bcf6ffdaf0 | ||
|
|
9fc184f6a1 | ||
|
|
c9a30a6e4b | ||
|
|
1ec6ddc914 | ||
|
|
2c96ef958e | ||
|
|
3c10131c45 | ||
|
|
fe0bfd4f87 | ||
|
|
290413f115 | ||
|
|
4aa6bef6a3 | ||
|
|
1b0f4e2803 | ||
|
|
301224748c | ||
|
|
1e15b836c2 | ||
|
|
1e709b7592 | ||
|
|
8744a85cb7 | ||
|
|
92ad77d8f5 | ||
|
|
2298640e4c | ||
|
|
5baef59672 | ||
|
|
e54e8affa7 | ||
|
|
97fa04a7b2 | ||
|
|
4977ee82e5 | ||
|
|
090680366c | ||
|
|
531c86847f | ||
|
|
446e437531 | ||
|
|
63d8ccf1a0 | ||
|
|
77abe0e158 | ||
|
|
547f8d9e71 | ||
|
|
e144217f25 | ||
|
|
d919810ce1 | ||
|
|
0bdf68edf6 | ||
|
|
b24341b7da | ||
|
|
384ee3dcaa | ||
|
|
3e8b1a6dad | ||
|
|
d2dec4a444 | ||
|
|
7e171bdb1e | ||
|
|
0cedf79a89 | ||
|
|
65ad1248ce | ||
|
|
e1180f97ce | ||
|
|
394b4a7cd2 | ||
|
|
fd574aae34 | ||
|
|
7fe503f51d | ||
|
|
6bfb0ef098 | ||
|
|
46c667d387 | ||
|
|
50f4a9cace | ||
|
|
cd20875744 | ||
|
|
84f682a0da | ||
|
|
6389b7dfbd | ||
|
|
0ef65b8bda | ||
|
|
4426c3279d | ||
|
|
883f9f834e | ||
|
|
e0fd3df9dd | ||
|
|
e6d3c67e39 | ||
|
|
6d0a0225c2 | ||
|
|
d60d8beb0b | ||
|
|
a657b3bc24 |
37
.github/workflows/golangci-lint.yml
vendored
37
.github/workflows/golangci-lint.yml
vendored
@@ -1,23 +1,26 @@
|
||||
name: golangci-lint
|
||||
on: [ push, pull_request ]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
- uses: actions/setup-go@v3
|
||||
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
|
||||
go-version: 1.18
|
||||
- uses: actions/checkout@v3
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
# https://github.com/golangci/golangci-lint-action/issues/535
|
||||
version: v1.47.3
|
||||
# version: latest
|
||||
args: --timeout=60m
|
||||
|
||||
304
.github/workflows/release.yml
vendored
304
.github/workflows/release.yml
vendored
@@ -4,153 +4,201 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 't*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
# Set variables that will be available to all builds.
|
||||
env_vars:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release_version: ${{ steps.release_version.outputs.release_version }}
|
||||
binary: ${{ steps.binary.outputs.binary }}
|
||||
steps:
|
||||
- id: release_version
|
||||
run: |
|
||||
RELEASE_VERSION=$(echo ${{ github.ref_name }} | sed -e 's/^[vt]//')
|
||||
echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
|
||||
- id: binary
|
||||
run: |
|
||||
BINARY=$(basename ${{ github.repository }})
|
||||
echo "binary=${BINARY}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.16
|
||||
id: go
|
||||
# Create a github release to hold the assets once built.
|
||||
create_release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: env_vars
|
||||
outputs:
|
||||
upload_url: ${{ steps.release_action.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Create Release
|
||||
id: release_action
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: Release ${{ needs.env_vars.outputs.release_version }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
# Build and pack the binaries for linux.
|
||||
build_linux:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [create_release, env_vars]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.19
|
||||
|
||||
- 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: Check out repository into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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: Select correct tag
|
||||
run: git checkout ${{ github.ref_name }}
|
||||
|
||||
- name: Build
|
||||
run: go build -v -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" .
|
||||
- name: Fetch repository dependencies
|
||||
run: go get -v -t ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v .
|
||||
- name: Compile
|
||||
run: |
|
||||
go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
|
||||
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz ${{ needs.env_vars.outputs.binary }}
|
||||
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
|
||||
- name: Upload binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Fetch xgo
|
||||
run: |
|
||||
go install github.com/wealdtech/xgo@latest
|
||||
- name: Upload hash
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
- 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 (ARM64)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
sudo apt install -y gcc-aarch64-linux-gnu libstdc++-11-pic-arm64-cross
|
||||
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
|
||||
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz ${{ needs.env_vars.outputs.binary }}
|
||||
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
|
||||
|
||||
- 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: Upload binary (ARM64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- 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: Upload hash (ARM64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
- 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
|
||||
# Build and pack the binaries for OSX.
|
||||
build_macos:
|
||||
runs-on: macos-latest
|
||||
needs: [create_release, env_vars]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.19
|
||||
|
||||
# - 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: Check out repository into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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: Select correct tag
|
||||
run: git checkout ${{ github.ref_name }}
|
||||
|
||||
- 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: Fetch repository dependencies
|
||||
run: go get -v -t ./...
|
||||
|
||||
- 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: Compile
|
||||
run: |
|
||||
go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }}"
|
||||
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz ${{ needs.env_vars.outputs.binary }}
|
||||
brew install coreutils
|
||||
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz.sha256
|
||||
- name: Upload binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- 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 hash
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz.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
|
||||
# Build and pack the binaries for Windows.
|
||||
build_windows:
|
||||
runs-on: windows-latest
|
||||
needs: [create_release, env_vars]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.19
|
||||
|
||||
# - 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: Check out repository into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# - 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
|
||||
- name: Select correct tag
|
||||
run: git checkout ${{ github.ref_name }}
|
||||
|
||||
- name: Fetch repository dependencies
|
||||
run: go get -v -t ./...
|
||||
|
||||
- name: Compile
|
||||
run: |
|
||||
go build -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
|
||||
choco install zip
|
||||
zip --junk-paths ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip ${{ needs.env_vars.outputs.binary }}.exe
|
||||
$FileHash=(certutil -hashfile ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip SHA256 | findstr /v hash | findstr /v SHA).replace(" ", "")
|
||||
echo "$FileHash" > ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe-zip.sha256
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload hash
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe-zip.sha256
|
||||
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe-zip.sha256
|
||||
asset_content_type: text/plain
|
||||
|
||||
15
.github/workflows/test.yml
vendored
Normal file
15
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
- uses: actions/checkout@v3
|
||||
- uses: n8maninger/action-golang-test@v1
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,5 +18,8 @@ coverage.html
|
||||
# Vim
|
||||
*.sw?
|
||||
|
||||
# Local JSON files
|
||||
*.json
|
||||
|
||||
# Local TODO
|
||||
TODO.md
|
||||
|
||||
169
.golangci.yml
Normal file
169
.golangci.yml
Normal file
@@ -0,0 +1,169 @@
|
||||
# This file contains all available configuration options
|
||||
# with their default values (in comments).
|
||||
#
|
||||
# This file is not a configuration example,
|
||||
# it contains the exhaustive configuration with explanations of the options.
|
||||
|
||||
# Options for analysis running.
|
||||
run:
|
||||
# The default concurrency value is the number of available CPU.
|
||||
# concurrency: 4
|
||||
|
||||
# Timeout for analysis, e.g. 30s, 5m.
|
||||
# Default: 1m
|
||||
timeout: 10m
|
||||
|
||||
# Exit code when at least one issue was found.
|
||||
# Default: 1
|
||||
# issues-exit-code: 2
|
||||
|
||||
# Include test files or not.
|
||||
# Default: true
|
||||
tests: false
|
||||
|
||||
# List of build tags, all linters use it.
|
||||
# Default: [].
|
||||
# build-tags:
|
||||
# - mytag
|
||||
|
||||
# Which dirs to skip: issues from them won't be reported.
|
||||
# Can use regexp here: `generated.*`, regexp is applied on full path.
|
||||
# Default value is empty list,
|
||||
# but default dirs are skipped independently of this option's value (see skip-dirs-use-default).
|
||||
# "/" will be replaced by current OS file path separator to properly work on Windows.
|
||||
# skip-dirs:
|
||||
# - autogenerated_by_my_lib
|
||||
|
||||
# Enables skipping of directories:
|
||||
# - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||
# Default: true
|
||||
# skip-dirs-use-default: false
|
||||
|
||||
# Which files to skip: they will be analyzed, but issues from them won't be reported.
|
||||
# Default value is empty list,
|
||||
# but there is no need to include all autogenerated files,
|
||||
# we confidently recognize autogenerated files.
|
||||
# If it's not please let us know.
|
||||
# "/" will be replaced by current OS file path separator to properly work on Windows.
|
||||
skip-files:
|
||||
- ".*_ssz\\.go$"
|
||||
|
||||
# If set we pass it to "go list -mod={option}". From "go help modules":
|
||||
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
||||
# automatic updating of go.mod described above. Instead, it fails when any changes
|
||||
# to go.mod are needed. This setting is most useful to check that go.mod does
|
||||
# not need updates, such as in a continuous integration and testing system.
|
||||
# If invoked with -mod=vendor, the go command assumes that the vendor
|
||||
# directory holds the correct copies of dependencies and ignores
|
||||
# the dependency descriptions in go.mod.
|
||||
#
|
||||
# Allowed values: readonly|vendor|mod
|
||||
# By default, it isn't set.
|
||||
modules-download-mode: readonly
|
||||
|
||||
# Allow multiple parallel golangci-lint instances running.
|
||||
# If false (default) - golangci-lint acquires file lock on start.
|
||||
allow-parallel-runners: true
|
||||
|
||||
# Define the Go version limit.
|
||||
# Mainly related to generics support since go1.18.
|
||||
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
|
||||
go: '1.19'
|
||||
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
|
||||
#
|
||||
# Multiple can be specified by separating them by comma, output can be provided
|
||||
# for each of them by separating format name and path by colon symbol.
|
||||
# Output path can be either `stdout`, `stderr` or path to the file to write to.
|
||||
# Example: "checkstyle:report.json,colored-line-number"
|
||||
#
|
||||
# Default: colored-line-number
|
||||
# format: json
|
||||
|
||||
# Print lines of code with issue.
|
||||
# Default: true
|
||||
# print-issued-lines: false
|
||||
|
||||
# Print linter name in the end of issue text.
|
||||
# Default: true
|
||||
# print-linter-name: false
|
||||
|
||||
# Make issues output unique by line.
|
||||
# Default: true
|
||||
# uniq-by-line: false
|
||||
|
||||
# Add a prefix to the output file references.
|
||||
# Default is no prefix.
|
||||
# path-prefix: ""
|
||||
|
||||
# Sort results by: filepath, line and column.
|
||||
# sort-results: true
|
||||
|
||||
|
||||
# All available settings of specific linters.
|
||||
linters-settings:
|
||||
lll:
|
||||
line-length: 132
|
||||
|
||||
stylecheck:
|
||||
checks: [ "all", "-ST1000" ]
|
||||
|
||||
tagliatelle:
|
||||
case:
|
||||
# use-field-name: true
|
||||
rules:
|
||||
json: snake
|
||||
yaml: snake
|
||||
|
||||
linters:
|
||||
# Enable all available linters.
|
||||
# Default: false
|
||||
enable-all: true
|
||||
# Disable specific linter
|
||||
# https://golangci-lint.run/usage/linters/#disabled-by-default
|
||||
disable:
|
||||
- contextcheck
|
||||
- cyclop
|
||||
- deadcode
|
||||
- dupl
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exhaustivestruct
|
||||
- exhaustruct
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funlen
|
||||
- gci
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- goerr113
|
||||
- goheader
|
||||
- golint
|
||||
- gomnd
|
||||
- ifshort
|
||||
- interfacer
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- maligned
|
||||
- nestif
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- nolintlint
|
||||
- nosnakecase
|
||||
- promlinter
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- sqlclosecheck
|
||||
- structcheck
|
||||
- unparam
|
||||
- varcheck
|
||||
- varnamelen
|
||||
- wastedassign
|
||||
- wrapcheck
|
||||
- wsl
|
||||
85
CHANGELOG.md
85
CHANGELOG.md
@@ -1,3 +1,88 @@
|
||||
1.28.0:
|
||||
- support additional mnemonic word list languages
|
||||
- increase minimum timeout for commands that fetch all validators to 2 minutes
|
||||
- provide better error messages when offline preparation file cannot be read
|
||||
- allow creation of all credential change operations related to a private key (thanks to @joaocenoura)
|
||||
|
||||
1.27.1:
|
||||
- fix issue with voluntary exits using incorrect domain (thanks to @0xTylerHolmes)
|
||||
|
||||
1.27.0:
|
||||
- use new build system
|
||||
- support S3 credentials
|
||||
- update operation of validator exit to match validator credentials set
|
||||
|
||||
1.26.5:
|
||||
- provide validator information in "chain status" verbose output
|
||||
|
||||
1.26.4:
|
||||
- provide details of BLS to execution change operations with verbose block output
|
||||
|
||||
1.26.3:
|
||||
- provide support for additional S3 store options
|
||||
- show error when attempting to delete non-filesystem wallets
|
||||
- provide additional support for Capella
|
||||
|
||||
1.26.2
|
||||
- remove check that requires capella prior to generating validator credentials change operations
|
||||
|
||||
1.26.1
|
||||
- add ability to generate validator credentials change operations prior to the fork in which they become usable
|
||||
|
||||
1.26.0
|
||||
- add commands and documentation to set user validator credentials (not usable until capella)
|
||||
|
||||
1.25.3
|
||||
- add more information to "epoch summary"
|
||||
- add "validator summary"
|
||||
|
||||
1.25.2:
|
||||
- no longer require connection parameter
|
||||
- support "block analyze" on bellatrix (thanks @tcrossland)
|
||||
- check deposit message root match for verifying deposits (thanks @aaron-alderman)
|
||||
|
||||
1.25.0:
|
||||
- add "proposer duties"
|
||||
- add deposit signature verification to "deposit verify"
|
||||
|
||||
1.24.1:
|
||||
- fix potential crash when new validators are activated
|
||||
- add "sepolia" to the list of supported networks
|
||||
|
||||
1.24.0:
|
||||
- add "validator yield"
|
||||
|
||||
1.23.1:
|
||||
- do not fetch future state for chain eth1votes
|
||||
|
||||
1.23.0:
|
||||
- do not fetch sync committee information for epoch summaries prior to Altair
|
||||
- ensure that "attester inclusion" without validator returns appropriate error
|
||||
- provide more information in "epoch summary" with verbose flag
|
||||
- add "chain eth1votes"
|
||||
|
||||
1.22.0:
|
||||
- add "ropsten" to the list of supported networks
|
||||
|
||||
1.21.0:
|
||||
- add "validator credentials get"
|
||||
|
||||
1.20.0:
|
||||
- add "chain queues"
|
||||
|
||||
1.19.1:
|
||||
- add the ability to import keystores to ethdo wallets
|
||||
- use defaults to connect to beacon nodes if no explicit connection defined
|
||||
|
||||
1.19.0:
|
||||
- add "epoch summary"
|
||||
|
||||
1.18.2:
|
||||
- tidy up output of "block info"
|
||||
|
||||
1.18.1:
|
||||
- do not show execution payload if empty
|
||||
|
||||
1.18.0:
|
||||
- add "-ssz" option to "block info"
|
||||
- add "block analyze" command
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.17-bullseye as builder
|
||||
FROM golang:1.18-bullseye as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
55
README.md
55
README.md
@@ -11,6 +11,7 @@ A command-line tool for managing common tasks in Ethereum 2.
|
||||
- [Binaries](#binaries)
|
||||
- [Docker](#docker)
|
||||
- [Source](#source)
|
||||
- [Setting up](#setting-up)
|
||||
- [Usage](#usage)
|
||||
- [Maintainers](#maintainers)
|
||||
- [Contribute](#contribute)
|
||||
@@ -34,7 +35,7 @@ docker pull wealdtech/ethdo
|
||||
`ethdo` is a standard Go program which can be installed with:
|
||||
|
||||
```sh
|
||||
GO111MODULE=on go get github.com/wealdtech/ethdo
|
||||
go install github.com/wealdtech/ethdo@latest
|
||||
```
|
||||
|
||||
Note that `ethdo` requires at least version 1.13 of go to operate. The version of go can be found with `go version`.
|
||||
@@ -61,24 +62,48 @@ 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.
|
||||
|
||||
## Setting up
|
||||
|
||||
`ethdo` needs a connection to a beacon node for many of its features. `ethdo` can connect to any beacon node that fully supports the [standard REST API](https://ethereum.github.io/beacon-APIs/) using the `--connection <beacon-node:port>` argument. The following changes are required to beacon nodes to make this available.
|
||||
|
||||
### Lighthouse
|
||||
Lighthouse disables the REST API by default. To enable it, the beacon node must be started with the `--http` parameter. If you want to access the REST API from a remote server then you should also look to change the `--http-address` and `--http-allow-origin` options as per the Lighthouse documentation.
|
||||
|
||||
The default port for the REST API is 5052, which can be changed with the `--http-port` parameter.
|
||||
|
||||
### Nimbus
|
||||
Nimbus disables the REST API by default. To enable it, the beacon node must be started with the `--rest` parameter. If you want to access the REST API from a remote server then you should also look to change the `--rest-address` and `--rest-allow-origin` options as per the Nimbus documentation.
|
||||
|
||||
The default port for the REST API is 5052, which can be changed with the `--rest-port` parameter.
|
||||
|
||||
### Prysm
|
||||
Prysm enables the REST API by default. You will need to add the parameter `--grpc-max-msg-size 268435456` to be obtain to obtain large sets of information such as the list of current validators. If you want to access the REST API from a remote server then you should also look to change the `--grpc-gateway-host` and `--grpc-gateway-corsdomain` options as per the Prysm documentation.
|
||||
|
||||
The default port for the REST API is 3500, which can be changed with the `--grpc-gateway-port` parameter.
|
||||
|
||||
### Teku
|
||||
Teku disables the REST API by default. To enable it, the beacon node must be started with the `--rest-api-enabled` parameter. If you want to access the REST API from a remote server then you should also look to change the `--rest-api-interface`, `--rest-api-host-allowlist` and `--rest-api-cors-origins` options as per the Teku documentation.
|
||||
|
||||
The default port for the REST API is 5051, which can be changed with the `--rest-api-port` parameter.
|
||||
|
||||
## Usage
|
||||
|
||||
ethdo contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
|
||||
`ethdo` contains a large number of features that are useful for day-to-day interactions with the different consensus clients.
|
||||
|
||||
### 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. When on the filesystem the locations of the created wallets and accounts are:
|
||||
`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 the filesystem store, the additional parameter `base-dir` 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
|
||||
> docker run -v $HOME/.config/ethereum2/wallets:/data ethdo --base-dir=/data wallet list
|
||||
> ```
|
||||
>
|
||||
> This will allow you to use `ethdo` with or without docker, with the same location for wallets and accounts.
|
||||
@@ -113,6 +138,24 @@ ethdo also supports environment variables. Environment variables are prefixed w
|
||||
export ETHDO_PASSPHRASE="my account passphrase"
|
||||
```
|
||||
|
||||
### S3 store options
|
||||
|
||||
Amazon S3-compatible stores have additional options available, which can be configured under the "stores.s3" key. An example configuration is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"stores": {
|
||||
"s3": {
|
||||
"bucket":"mybucketname",
|
||||
"path":"path/in/bucket",
|
||||
"passphrase":"secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Information on these and other options can be found in the S3 store repository.
|
||||
|
||||
### Output and exit status
|
||||
|
||||
If set, the `--quiet` argument will suppress all output.
|
||||
@@ -153,7 +196,7 @@ There is a [HOWTO](https://github.com/wealdtech/ethdo/blob/master/docs/howto.md)
|
||||
|
||||
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.
|
||||
Special thanks to [@SuburbanDad](https://github.com/SuburbanDad) for updating xgo to allow for cross-compilation of `ethdo` releases.
|
||||
|
||||
## Contribute
|
||||
|
||||
|
||||
300
beacon/chaininfo.go
Normal file
300
beacon/chaininfo.go
Normal file
@@ -0,0 +1,300 @@
|
||||
// Copyright © 2023 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 beacon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type ChainInfo struct {
|
||||
Version uint64
|
||||
Validators []*ValidatorInfo
|
||||
GenesisValidatorsRoot phase0.Root
|
||||
Epoch phase0.Epoch
|
||||
GenesisForkVersion phase0.Version
|
||||
CurrentForkVersion phase0.Version
|
||||
BLSToExecutionChangeDomainType phase0.DomainType
|
||||
VoluntaryExitDomainType phase0.DomainType
|
||||
}
|
||||
|
||||
type chainInfoJSON struct {
|
||||
Version string `json:"version"`
|
||||
Validators []*ValidatorInfo `json:"validators"`
|
||||
GenesisValidatorsRoot string `json:"genesis_validators_root"`
|
||||
Epoch string `json:"epoch"`
|
||||
GenesisForkVersion string `json:"genesis_fork_version"`
|
||||
CurrentForkVersion string `json:"current_fork_version"`
|
||||
BLSToExecutionChangeDomainType string `json:"bls_to_execution_change_domain_type"`
|
||||
VoluntaryExitDomainType string `json:"voluntary_exit_domain_type"`
|
||||
}
|
||||
|
||||
type chainInfoVersionJSON struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&chainInfoJSON{
|
||||
Version: fmt.Sprintf("%d", c.Version),
|
||||
Validators: c.Validators,
|
||||
GenesisValidatorsRoot: fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
|
||||
Epoch: fmt.Sprintf("%d", c.Epoch),
|
||||
GenesisForkVersion: fmt.Sprintf("%#x", c.GenesisForkVersion),
|
||||
CurrentForkVersion: fmt.Sprintf("%#x", c.CurrentForkVersion),
|
||||
BLSToExecutionChangeDomainType: fmt.Sprintf("%#x", c.BLSToExecutionChangeDomainType),
|
||||
VoluntaryExitDomainType: fmt.Sprintf("%#x", c.VoluntaryExitDomainType),
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (c *ChainInfo) UnmarshalJSON(input []byte) error {
|
||||
// See which version we are dealing with.
|
||||
var metadata chainInfoVersionJSON
|
||||
if err := json.Unmarshal(input, &metadata); err != nil {
|
||||
return errors.Wrap(err, "invalid JSON")
|
||||
}
|
||||
if metadata.Version == "" {
|
||||
return errors.New("version missing")
|
||||
}
|
||||
version, err := strconv.ParseUint(metadata.Version, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "version invalid")
|
||||
}
|
||||
if version < 2 {
|
||||
return errors.New("outdated version; please regenerate your offline data")
|
||||
}
|
||||
c.Version = version
|
||||
|
||||
var data chainInfoJSON
|
||||
if err := json.Unmarshal(input, &data); err != nil {
|
||||
return errors.Wrap(err, "invalid JSON")
|
||||
}
|
||||
|
||||
if len(data.Validators) == 0 {
|
||||
return errors.New("validators missing")
|
||||
}
|
||||
c.Validators = data.Validators
|
||||
|
||||
if data.GenesisValidatorsRoot == "" {
|
||||
return errors.New("genesis validators root missing")
|
||||
}
|
||||
genesisValidatorsRootBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisValidatorsRoot, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "genesis validators root invalid")
|
||||
}
|
||||
if len(genesisValidatorsRootBytes) != phase0.RootLength {
|
||||
return errors.New("genesis validators root incorrect length")
|
||||
}
|
||||
copy(c.GenesisValidatorsRoot[:], genesisValidatorsRootBytes)
|
||||
|
||||
if data.Epoch == "" {
|
||||
return errors.New("epoch missing")
|
||||
}
|
||||
epoch, err := strconv.ParseUint(data.Epoch, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "epoch invalid")
|
||||
}
|
||||
c.Epoch = phase0.Epoch(epoch)
|
||||
|
||||
if data.GenesisForkVersion == "" {
|
||||
return errors.New("genesis fork version missing")
|
||||
}
|
||||
genesisForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisForkVersion, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "genesis fork version invalid")
|
||||
}
|
||||
if len(genesisForkVersionBytes) != phase0.ForkVersionLength {
|
||||
return errors.New("genesis fork version incorrect length")
|
||||
}
|
||||
copy(c.GenesisForkVersion[:], genesisForkVersionBytes)
|
||||
|
||||
if data.CurrentForkVersion == "" {
|
||||
return errors.New("current fork version missing")
|
||||
}
|
||||
currentForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.CurrentForkVersion, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "current fork version invalid")
|
||||
}
|
||||
if len(currentForkVersionBytes) != phase0.ForkVersionLength {
|
||||
return errors.New("current fork version incorrect length")
|
||||
}
|
||||
copy(c.CurrentForkVersion[:], currentForkVersionBytes)
|
||||
|
||||
if data.BLSToExecutionChangeDomainType == "" {
|
||||
return errors.New("bls to execution domain type missing")
|
||||
}
|
||||
blsToExecutionChangeDomainType, err := hex.DecodeString(strings.TrimPrefix(data.BLSToExecutionChangeDomainType, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "bls to execution domain type invalid")
|
||||
}
|
||||
if len(blsToExecutionChangeDomainType) != phase0.DomainTypeLength {
|
||||
return errors.New("bls to execution domain type incorrect length")
|
||||
}
|
||||
copy(c.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType)
|
||||
|
||||
if data.VoluntaryExitDomainType == "" {
|
||||
return errors.New("voluntary exit domain type missing")
|
||||
}
|
||||
voluntaryExitDomainType, err := hex.DecodeString(strings.TrimPrefix(data.VoluntaryExitDomainType, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "voluntary exit domain type invalid")
|
||||
}
|
||||
if len(voluntaryExitDomainType) != phase0.DomainTypeLength {
|
||||
return errors.New("voluntary exit domain type incorrect length")
|
||||
}
|
||||
copy(c.VoluntaryExitDomainType[:], voluntaryExitDomainType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchValidatorInfo fetches validator info given a validator identifier.
|
||||
func (c *ChainInfo) FetchValidatorInfo(ctx context.Context, id string) (*ValidatorInfo, error) {
|
||||
var validatorInfo *ValidatorInfo
|
||||
switch {
|
||||
case id == "":
|
||||
return nil, errors.New("no validator specified")
|
||||
case strings.HasPrefix(id, "0x"):
|
||||
// A public key.
|
||||
for _, validator := range c.Validators {
|
||||
if strings.EqualFold(id, fmt.Sprintf("%#x", validator.Pubkey)) {
|
||||
validatorInfo = validator
|
||||
break
|
||||
}
|
||||
}
|
||||
case strings.Contains(id, "/"):
|
||||
// An account.
|
||||
_, account, err := util.WalletAndAccountFromPath(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to obtain account")
|
||||
}
|
||||
accPubKey, err := util.BestPublicKey(account)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to obtain public key for account")
|
||||
}
|
||||
pubkey := fmt.Sprintf("%#x", accPubKey.Marshal())
|
||||
for _, validator := range c.Validators {
|
||||
if strings.EqualFold(pubkey, fmt.Sprintf("%#x", validator.Pubkey)) {
|
||||
validatorInfo = validator
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
// An index.
|
||||
index, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse validator index")
|
||||
}
|
||||
validatorIndex := phase0.ValidatorIndex(index)
|
||||
for _, validator := range c.Validators {
|
||||
if validator.Index == validatorIndex {
|
||||
validatorInfo = validator
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if validatorInfo == nil {
|
||||
return nil, errors.New("unknown validator")
|
||||
}
|
||||
|
||||
return validatorInfo, nil
|
||||
}
|
||||
|
||||
// ObtainChainInfoFromNode obtains the chain information from a node.
|
||||
func ObtainChainInfoFromNode(ctx context.Context,
|
||||
consensusClient consensusclient.Service,
|
||||
chainTime chaintime.Service,
|
||||
) (
|
||||
*ChainInfo,
|
||||
error,
|
||||
) {
|
||||
res := &ChainInfo{
|
||||
Version: 2,
|
||||
Validators: make([]*ValidatorInfo, 0),
|
||||
Epoch: chainTime.CurrentEpoch(),
|
||||
}
|
||||
|
||||
// Obtain validators.
|
||||
validators, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
for _, validator := range validators {
|
||||
res.Validators = append(res.Validators, &ValidatorInfo{
|
||||
Index: validator.Index,
|
||||
Pubkey: validator.Validator.PublicKey,
|
||||
WithdrawalCredentials: validator.Validator.WithdrawalCredentials,
|
||||
State: validator.Status,
|
||||
})
|
||||
}
|
||||
|
||||
// Genesis validators root obtained from beacon node.
|
||||
genesis, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain genesis information")
|
||||
}
|
||||
res.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
|
||||
|
||||
// Fetch the genesis fork version from the specification.
|
||||
spec, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
tmp, exists := spec["GENESIS_FORK_VERSION"]
|
||||
if !exists {
|
||||
return nil, errors.New("capella fork version not known by chain")
|
||||
}
|
||||
var isForkVersion bool
|
||||
res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)
|
||||
if !isForkVersion {
|
||||
return nil, errors.New("could not obtain GENESIS_FORK_VERSION")
|
||||
}
|
||||
|
||||
// Fetch the current fork version from the fork schedule.
|
||||
forkSchedule, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain fork schedule")
|
||||
}
|
||||
for i := range forkSchedule {
|
||||
if forkSchedule[i].Epoch <= res.Epoch {
|
||||
res.CurrentForkVersion = forkSchedule[i].CurrentVersion
|
||||
}
|
||||
}
|
||||
|
||||
blsToExecutionChangeDomainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
|
||||
if !exists {
|
||||
return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
|
||||
}
|
||||
copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])
|
||||
|
||||
voluntaryExitDomainType, exists := spec["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
|
||||
if !exists {
|
||||
return nil, errors.New("failed to obtain DOMAIN_VOLUNTARY_EXIT")
|
||||
}
|
||||
copy(res.VoluntaryExitDomainType[:], voluntaryExitDomainType[:])
|
||||
|
||||
return res, nil
|
||||
}
|
||||
106
beacon/validatorinfo.go
Normal file
106
beacon/validatorinfo.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright © 2023 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 beacon
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ValidatorInfo struct {
|
||||
Index phase0.ValidatorIndex
|
||||
Pubkey phase0.BLSPubKey
|
||||
State apiv1.ValidatorState
|
||||
WithdrawalCredentials []byte
|
||||
}
|
||||
|
||||
type validatorInfoJSON struct {
|
||||
Index string `json:"index"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
State apiv1.ValidatorState `json:"state"`
|
||||
WithdrawalCredentials string `json:"withdrawal_credentials"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (v *ValidatorInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&validatorInfoJSON{
|
||||
Index: fmt.Sprintf("%d", v.Index),
|
||||
Pubkey: fmt.Sprintf("%#x", v.Pubkey),
|
||||
State: v.State,
|
||||
WithdrawalCredentials: fmt.Sprintf("%#x", v.WithdrawalCredentials),
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (v *ValidatorInfo) UnmarshalJSON(input []byte) error {
|
||||
var data validatorInfoJSON
|
||||
if err := json.Unmarshal(input, &data); err != nil {
|
||||
return errors.Wrap(err, "invalid JSON")
|
||||
}
|
||||
|
||||
if data.Index == "" {
|
||||
return errors.New("index missing")
|
||||
}
|
||||
index, err := strconv.ParseUint(data.Index, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for index")
|
||||
}
|
||||
v.Index = phase0.ValidatorIndex(index)
|
||||
|
||||
if data.Pubkey == "" {
|
||||
return errors.New("public key missing")
|
||||
}
|
||||
pubkey, err := hex.DecodeString(strings.TrimPrefix(data.Pubkey, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for public key")
|
||||
}
|
||||
if len(pubkey) != phase0.PublicKeyLength {
|
||||
return fmt.Errorf("incorrect length %d for public key", len(pubkey))
|
||||
}
|
||||
copy(v.Pubkey[:], pubkey)
|
||||
|
||||
if data.State == apiv1.ValidatorStateUnknown {
|
||||
return errors.New("state unknown")
|
||||
}
|
||||
v.State = data.State
|
||||
|
||||
if data.WithdrawalCredentials == "" {
|
||||
return errors.New("withdrawal credentials missing")
|
||||
}
|
||||
v.WithdrawalCredentials, err = hex.DecodeString(strings.TrimPrefix(data.WithdrawalCredentials, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for withdrawal credentials")
|
||||
}
|
||||
if len(v.WithdrawalCredentials) != phase0.HashLength {
|
||||
return fmt.Errorf("incorrect length %d for withdrawal credentials", len(v.WithdrawalCredentials))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (v *ValidatorInfo) String() string {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Err: %v\n", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// accountCmd represents the account command
|
||||
// accountCmd represents the account command.
|
||||
var accountCmd = &cobra.Command{
|
||||
Use: "account",
|
||||
Short: "Manage account",
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -42,11 +42,13 @@ func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
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))
|
||||
builder.WriteString(fmt.Sprintf("Withdrawal credentials: %#x\n", withdrawalCredentials))
|
||||
}
|
||||
if !(data.showPrivateKey || data.showWithdrawalCredentials) {
|
||||
builder.WriteString(fmt.Sprintf("Public key: %#x\n", data.key.PublicKey().Marshal()))
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestOutput(t *testing.T) {
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showPrivateKey: true,
|
||||
},
|
||||
needs: []string{"Public key", "Private key"},
|
||||
needs: []string{"Private key"},
|
||||
},
|
||||
{
|
||||
name: "WithdrawalCredentials",
|
||||
@@ -72,7 +72,7 @@ func TestOutput(t *testing.T) {
|
||||
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Public key", "Withdrawal credentials"},
|
||||
needs: []string{"Withdrawal credentials"},
|
||||
},
|
||||
{
|
||||
name: "All",
|
||||
@@ -81,7 +81,7 @@ func TestOutput(t *testing.T) {
|
||||
showPrivateKey: true,
|
||||
showWithdrawalCredentials: true,
|
||||
},
|
||||
needs: []string{"Public key", "Private key", "Withdrawal credentials"},
|
||||
needs: []string{"Private key", "Withdrawal credentials"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,57 +15,32 @@ 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"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
// 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)
|
||||
account, err := util.ParseAccount(ctx, data.mnemonic, []string{data.path}, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate key")
|
||||
return nil, errors.Wrap(err, "failed to derive account")
|
||||
}
|
||||
|
||||
key, err := account.(e2wtypes.AccountPrivateKeyProvider).PrivateKey(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account private key")
|
||||
}
|
||||
|
||||
results := &dataOut{
|
||||
showPrivateKey: data.showPrivateKey,
|
||||
showPrivateKey: data.showPrivateKey,
|
||||
showWithdrawalCredentials: data.showWithdrawalCredentials,
|
||||
key: key,
|
||||
key: key.(*e2types.BLSPrivateKey),
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestProcess(t *testing.T) {
|
||||
dataIn: &dataIn{
|
||||
path: "m/12381/3600/0/0",
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
err: "failed to derive account: no account specified",
|
||||
},
|
||||
{
|
||||
name: "MnemonicInvalid",
|
||||
@@ -48,14 +48,14 @@ func TestProcess(t *testing.T) {
|
||||
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",
|
||||
err: "failed to derive account: 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/…",
|
||||
err: "failed to derive account: path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "PathInvalid",
|
||||
@@ -63,7 +63,7 @@ func TestProcess(t *testing.T) {
|
||||
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/…",
|
||||
err: "failed to derive account: path does not match expected format m/…",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
|
||||
@@ -15,6 +15,7 @@ package accountderive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -45,5 +46,5 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
return results, nil
|
||||
return strings.TrimSuffix(results, "\n"), nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 - 2022 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
|
||||
@@ -16,6 +16,7 @@ package accountimport
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -27,12 +28,14 @@ import (
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
timeout time.Duration
|
||||
wallet e2wtypes.Wallet
|
||||
key []byte
|
||||
accountName string
|
||||
passphrase string
|
||||
walletPassphrase string
|
||||
timeout time.Duration
|
||||
wallet e2wtypes.Wallet
|
||||
key []byte
|
||||
accountName string
|
||||
passphrase string
|
||||
walletPassphrase string
|
||||
keystore []byte
|
||||
keystorePassphrase []byte
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
@@ -74,14 +77,55 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
// Wallet passphrase.
|
||||
data.walletPassphrase = util.GetWalletPassphrase()
|
||||
|
||||
// Key.
|
||||
if viper.GetString("key") == "" {
|
||||
return nil, errors.New("key is required")
|
||||
if viper.GetString("key") == "" && viper.GetString("keystore") == "" {
|
||||
return nil, errors.New("key or keystore is required")
|
||||
}
|
||||
data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "key is malformed")
|
||||
if viper.GetString("key") != "" && viper.GetString("keystore") != "" {
|
||||
return nil, errors.New("only one of key and keystore is required")
|
||||
}
|
||||
|
||||
if viper.GetString("key") != "" {
|
||||
data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "key is malformed")
|
||||
}
|
||||
}
|
||||
|
||||
if viper.GetString("keystore") != "" {
|
||||
data.keystorePassphrase = []byte(viper.GetString("keystore-passphrase"))
|
||||
if len(data.keystorePassphrase) == 0 {
|
||||
return nil, errors.New("must supply keystore passphrase with keystore-passphrase when supplying keystore")
|
||||
}
|
||||
data.keystore, err = obtainKeystore(viper.GetString("keystore"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid keystore")
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// obtainKeystore obtains keystore from an input, could be JSON itself or a path to JSON.
|
||||
func obtainKeystore(input string) ([]byte, 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 = os.ReadFile(input)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find deposit data file")
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
// exitData := &util.ValidatorExitData{}
|
||||
// err = json.Unmarshal(data, exitData)
|
||||
// if err != nil {
|
||||
// return nil, errors.Wrap(err, "data is not valid JSON")
|
||||
// }
|
||||
|
||||
// return exitData, nil
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestInput(t *testing.T) {
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
},
|
||||
err: "key is required",
|
||||
err: "key or keystore is required",
|
||||
},
|
||||
{
|
||||
name: "KeyMalformed",
|
||||
@@ -114,6 +114,26 @@ func TestInput(t *testing.T) {
|
||||
},
|
||||
err: "key is malformed: encoding/hex: invalid byte: U+0069 'i'",
|
||||
},
|
||||
{
|
||||
name: "KeyandKeystore",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"passphrase": "ce%NohGhah4ye5ra",
|
||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
"keystore": "{}",
|
||||
},
|
||||
err: "only one of key and keystore is required",
|
||||
},
|
||||
{
|
||||
name: "KeystoreNoKeystorePassphrase",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Test account",
|
||||
"keystore": "{}",
|
||||
},
|
||||
err: "must supply keystore passphrase with keystore-passphrase when supplying keystore",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// Copyright © 2019 -2022 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,14 @@ package accountimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
"github.com/wealdtech/go-ecodec"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -43,9 +48,23 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
}()
|
||||
}
|
||||
|
||||
if len(data.key) > 0 {
|
||||
return processFromKey(ctx, data)
|
||||
}
|
||||
if len(data.keystore) > 0 {
|
||||
return processFromKeystore(ctx, data)
|
||||
}
|
||||
return nil, errors.New("unsupported import mechanism")
|
||||
}
|
||||
|
||||
func processFromKey(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
results := &dataOut{}
|
||||
|
||||
account, err := data.wallet.(e2wtypes.WalletAccountImporter).ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase))
|
||||
importer, isImporter := data.wallet.(e2wtypes.WalletAccountImporter)
|
||||
if !isImporter {
|
||||
return nil, fmt.Errorf("%s wallets do not support importing accounts", data.wallet.Type())
|
||||
}
|
||||
account, err := importer.ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to import account")
|
||||
}
|
||||
@@ -53,3 +72,39 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func processFromKeystore(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
// Need to import the keystore in to a temporary wallet to fetch the private key.
|
||||
store := scratch.New()
|
||||
encryptor := keystorev4.New()
|
||||
|
||||
// Need to add a couple of fields to the keystore to make it compliant.
|
||||
keystoreData := fmt.Sprintf(`{"name":"Import","encryptor":"keystore",%s`, string(data.keystore[1:]))
|
||||
walletData := fmt.Sprintf(`{"wallet":{"name":"ImportTest","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
|
||||
encryptedData, err := ecodec.Encrypt([]byte(walletData), data.keystorePassphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wallet, err := nd.Import(ctx, encryptedData, data.keystorePassphrase, store, encryptor)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to import wallet")
|
||||
}
|
||||
|
||||
account := <-wallet.Accounts(ctx)
|
||||
privateKeyProvider, isPrivateKeyProvider := account.(e2wtypes.AccountPrivateKeyProvider)
|
||||
if !isPrivateKeyProvider {
|
||||
return nil, errors.New("account does not provide its private key")
|
||||
}
|
||||
if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker {
|
||||
if err = locker.Unlock(ctx, data.keystorePassphrase); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unlock account")
|
||||
}
|
||||
}
|
||||
key, err := privateKeyProvider.PrivateKey(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain private key")
|
||||
}
|
||||
data.key = key.Marshal()
|
||||
// We have the key from the keystore; import it.
|
||||
return processFromKey(ctx, data)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ func init() {
|
||||
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() {
|
||||
@@ -59,7 +58,4 @@ func accountCreateBindings() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,19 +47,11 @@ In quiet mode this will return 0 if the inputs can derive an account account, ot
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -48,10 +48,18 @@ func init() {
|
||||
accountCmd.AddCommand(accountImportCmd)
|
||||
accountFlags(accountImportCmd)
|
||||
accountImportCmd.Flags().String("key", "", "Private key of the account to import (0x...)")
|
||||
accountImportCmd.Flags().String("keystore", "", "Keystore, or path to keystore ")
|
||||
accountImportCmd.Flags().String("keystore-passphrase", "", "Passphrase of keystore")
|
||||
}
|
||||
|
||||
func accountImportBindings() {
|
||||
if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("keystore", accountImportCmd.Flags().Lookup("keystore")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
accountkey "github.com/wealdtech/ethdo/cmd/account/key"
|
||||
)
|
||||
|
||||
// accountKeyCmd represents the account key command
|
||||
// accountKeyCmd represents the account key command.
|
||||
var accountKeyCmd = &cobra.Command{
|
||||
Use: "key",
|
||||
Short: "Obtain the private key of an account.",
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// attestationCmd represents the attestation command
|
||||
// attestationCmd represents the attestation command.
|
||||
var attestationCmd = &cobra.Command{
|
||||
Use: "attestation",
|
||||
Short: "Obtain information about an Ethereum 2 attestation",
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// attesterCmd represents the attester command
|
||||
// attesterCmd represents the attester command.
|
||||
var attesterCmd = &cobra.Command{
|
||||
Use: "attester",
|
||||
Short: "Obtain information about Ethereum 2 attesters",
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -101,7 +100,6 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if attestation.Data.Slot == duty.Slot &&
|
||||
attestation.Data.Index == duty.CommitteeIndex &&
|
||||
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
|
||||
headCorrect := false
|
||||
targetCorrect := false
|
||||
if data.verbose {
|
||||
@@ -139,7 +137,7 @@ func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Atte
|
||||
for {
|
||||
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
return false, err
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
@@ -161,7 +159,7 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
|
||||
for {
|
||||
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
return false, err
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
|
||||
@@ -49,7 +49,7 @@ func init() {
|
||||
attesterFlags(attesterInclusionCmd)
|
||||
attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch")
|
||||
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
|
||||
attesterInclusionCmd.Flags().Int64("index", -1, "the index of the attester")
|
||||
attesterInclusionCmd.Flags().String("index", "", "the index of the attester")
|
||||
}
|
||||
|
||||
func attesterInclusionBindings() {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// blockCmd represents the block command
|
||||
// blockCmd represents the block command.
|
||||
var blockCmd = &cobra.Command{
|
||||
Use: "block",
|
||||
Short: "Obtain information about an Ethereum 2 block",
|
||||
|
||||
@@ -122,9 +122,6 @@ func newCommand(ctx context.Context) (*command, error) {
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
|
||||
@@ -37,27 +37,10 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "ValidatorsZero",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"validators": "0",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "validators must be at least 1",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"blockid": "1",
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
|
||||
@@ -266,7 +266,6 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -364,7 +363,7 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
|
||||
for {
|
||||
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
return false, err
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
@@ -393,7 +392,7 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
|
||||
for {
|
||||
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
return false, err
|
||||
}
|
||||
if header == nil {
|
||||
// No block.
|
||||
@@ -425,6 +424,20 @@ func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.Version
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
case spec.DataVersionBellatrix:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
|
||||
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
case spec.DataVersionCapella:
|
||||
c.analysis.SyncCommitee.Contributions = int(block.Capella.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
|
||||
c.analysis.SyncCommitee.PossibleContributions = int(block.Capella.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
|
||||
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
|
||||
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
|
||||
c.analysis.Value += c.analysis.SyncCommitee.Value
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported block version %d", block.Version)
|
||||
}
|
||||
|
||||
@@ -33,13 +33,13 @@ func TestProcess(t *testing.T) {
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidData",
|
||||
name: "NoBlock",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "60s",
|
||||
"validators": "1",
|
||||
"data": "[[",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"blockid": "invalid",
|
||||
},
|
||||
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"Invalid block: invalid\"}",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "ConnectionBad",
|
||||
@@ -79,7 +79,7 @@ func TestInput(t *testing.T) {
|
||||
timeout: 5 * time.Second,
|
||||
blockID: "justified",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
|
||||
},
|
||||
{
|
||||
name: "BlockIDNil",
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-bitfield"
|
||||
@@ -77,8 +78,9 @@ func outputBlockGeneral(ctx context.Context,
|
||||
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
|
||||
}
|
||||
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
|
||||
graffiti = bytes.TrimRight(graffiti, "\u0000")
|
||||
if utf8.Valid(graffiti) {
|
||||
res.WriteString(fmt.Sprintf("Graffiti: %s\n", strings.TrimRight(string(graffiti), "\u0000")))
|
||||
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
|
||||
} else {
|
||||
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti))
|
||||
}
|
||||
@@ -222,7 +224,7 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
|
||||
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(" Validator: %#x (%d)\n", validators[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
|
||||
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
|
||||
}
|
||||
}
|
||||
@@ -231,6 +233,27 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client.Service, verbose bool, ops []*capella.SignedBLSToExecutionChange) (string, error) {
|
||||
res := strings.Builder{}
|
||||
|
||||
res.WriteString(fmt.Sprintf("BLS to execution changes: %d\n", len(ops)))
|
||||
if verbose {
|
||||
for i, op := range ops {
|
||||
res.WriteString(fmt.Sprintf(" %d:\n", i))
|
||||
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{op.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[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
|
||||
res.WriteString(fmt.Sprintf(" BLS public key: %#x\n", op.Message.FromBLSPubkey))
|
||||
res.WriteString(fmt.Sprintf(" Execution address: %#x\n", op.Message.ToExecutionAddress))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
@@ -269,7 +292,7 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
|
||||
func outputCapellaBlockText(ctx context.Context, data *dataOut, signedBlock *capella.SignedBeaconBlock) (string, error) {
|
||||
if signedBlock == nil {
|
||||
return "", errors.New("no block supplied")
|
||||
}
|
||||
@@ -295,7 +318,7 @@ func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *b
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti,
|
||||
signedBlock.Message.Body.Graffiti[:],
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
@@ -350,7 +373,103 @@ func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *b
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
|
||||
tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
tmp, err = outputCapellaBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res.WriteString(tmp)
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.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)
|
||||
|
||||
tmp, err = outputBellatrixBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -385,7 +504,7 @@ func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *alta
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti,
|
||||
signedBlock.Message.Body.Graffiti[:],
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
@@ -468,7 +587,7 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
|
||||
bodyRoot,
|
||||
signedBlock.Message.ParentRoot,
|
||||
signedBlock.Message.StateRoot,
|
||||
signedBlock.Message.Body.Graffiti,
|
||||
signedBlock.Message.Body.Graffiti[:],
|
||||
data.genesisTime,
|
||||
data.slotDuration,
|
||||
data.slotsPerEpoch)
|
||||
@@ -519,9 +638,9 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBlockExecutionPayload(ctx context.Context,
|
||||
func outputCapellaBlockExecutionPayload(ctx context.Context,
|
||||
verbose bool,
|
||||
payload *bellatrix.ExecutionPayload,
|
||||
payload *capella.ExecutionPayload,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
@@ -530,11 +649,21 @@ func outputBlockExecutionPayload(ctx context.Context,
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If the block number is 0 then we're before the merge.
|
||||
if payload.BlockNumber == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
res.WriteString("Execution payload:\n")
|
||||
res.WriteString(" Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
if verbose {
|
||||
if !verbose {
|
||||
res.WriteString("Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
res.WriteString("Transactions: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
|
||||
} else {
|
||||
res.WriteString("Execution payload:\n")
|
||||
res.WriteString(" Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
|
||||
for i := 0; i < 32; i++ {
|
||||
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
|
||||
@@ -564,6 +693,72 @@ func outputBlockExecutionPayload(ctx context.Context,
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
|
||||
res.WriteString(" Logs bloom: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
|
||||
res.WriteString(" Transactions: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
|
||||
res.WriteString(" Withdrawals: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals)))
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
}
|
||||
|
||||
func outputBellatrixBlockExecutionPayload(ctx context.Context,
|
||||
verbose bool,
|
||||
payload *bellatrix.ExecutionPayload,
|
||||
) (
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
if payload == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If the block number is 0 then we're before the merge.
|
||||
if payload.BlockNumber == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
if !verbose {
|
||||
res.WriteString("Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
res.WriteString("Transactions: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
|
||||
} else {
|
||||
res.WriteString("Execution payload:\n")
|
||||
res.WriteString(" Execution block number: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
|
||||
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
|
||||
for i := 0; i < 32; i++ {
|
||||
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
|
||||
}
|
||||
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
|
||||
res.WriteString(" Base fee per gas: ")
|
||||
res.WriteString(string2eth.WeiToString(baseFeePerGas, true))
|
||||
res.WriteString("\n Block hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
|
||||
res.WriteString(" Parent hash: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
|
||||
res.WriteString(" Fee recipient: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
|
||||
res.WriteString(" Gas limit: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
|
||||
res.WriteString(" Gas used: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
|
||||
res.WriteString(" Timestamp: ")
|
||||
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
|
||||
res.WriteString(" Prev RANDAO: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
|
||||
res.WriteString(" Receipts root: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
|
||||
res.WriteString(" State root: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
|
||||
res.WriteString(" Extra data: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
|
||||
res.WriteString(" Logs bloom: ")
|
||||
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
|
||||
res.WriteString(" Transactions: ")
|
||||
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
|
||||
}
|
||||
|
||||
return res.String(), nil
|
||||
|
||||
@@ -24,13 +24,16 @@ import (
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/altair"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
"github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var jsonOutput bool
|
||||
var sszOutput bool
|
||||
var results *dataOut
|
||||
var (
|
||||
jsonOutput bool
|
||||
sszOutput bool
|
||||
results *dataOut
|
||||
)
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
@@ -75,6 +78,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
case spec.DataVersionCapella:
|
||||
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Capella); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to output block")
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unknown block version")
|
||||
}
|
||||
@@ -82,6 +89,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data.stream {
|
||||
jsonOutput = data.jsonOutput
|
||||
sszOutput = data.sszOutput
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("")
|
||||
}
|
||||
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to start block stream")
|
||||
@@ -101,13 +111,13 @@ func headEventHandler(event *api.Event) {
|
||||
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
|
||||
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
|
||||
if err != nil {
|
||||
if !jsonOutput {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to obtain block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if signedBlock == nil {
|
||||
if !jsonOutput {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("Empty beacon block")
|
||||
}
|
||||
return
|
||||
@@ -115,31 +125,41 @@ func headEventHandler(event *api.Event) {
|
||||
switch signedBlock.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
|
||||
if !jsonOutput {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
case spec.DataVersionAltair:
|
||||
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
|
||||
if !jsonOutput {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
case spec.DataVersionBellatrix:
|
||||
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
|
||||
if !jsonOutput {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
case spec.DataVersionCapella:
|
||||
if err := outputCapellaBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Capella); err != nil {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Failed to output block: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
default:
|
||||
if !jsonOutput {
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !jsonOutput && !sszOutput {
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
|
||||
@@ -155,7 +175,7 @@ func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -179,7 +199,7 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, sig
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -203,7 +223,31 @@ func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool,
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputCapellaBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *capella.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))
|
||||
case sszOutput:
|
||||
data, err := signedBlock.MarshalSSZ()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate SSZ")
|
||||
}
|
||||
fmt.Printf("%x\n", data)
|
||||
default:
|
||||
data, err := outputCapellaBlockText(ctx, results, signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate text")
|
||||
}
|
||||
fmt.Print(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// chainCmd represents the chain command
|
||||
// chainCmd represents the chain command.
|
||||
var chainCmd = &cobra.Command{
|
||||
Use: "chain",
|
||||
Short: "Obtain information about an Ethereum 2 chain",
|
||||
|
||||
84
cmd/chain/eth1votes/command.go
Normal file
84
cmd/chain/eth1votes/command.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Input.
|
||||
xepoch string
|
||||
xperiod string
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
beaconStateProvider eth2client.BeaconStateProvider
|
||||
slotsPerEpoch uint64
|
||||
epochsPerEth1VotingPeriod uint64
|
||||
|
||||
// Output.
|
||||
slot phase0.Slot
|
||||
epoch phase0.Epoch
|
||||
period uint64
|
||||
incumbent *phase0.ETH1Data
|
||||
eth1DataVotes []*phase0.ETH1Data
|
||||
votes map[string]*vote
|
||||
}
|
||||
|
||||
type vote struct {
|
||||
Vote *phase0.ETH1Data `json:"vote"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
json: viper.GetBool("json"),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
c.xepoch = viper.GetString("epoch")
|
||||
c.xperiod = viper.GetString("period")
|
||||
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
64
cmd/chain/eth1votes/command_internal_test.go
Normal file
64
cmd/chain/eth1votes/command_internal_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
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: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
cmd/chain/eth1votes/output.go
Normal file
125
cmd/chain/eth1votes/output.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
)
|
||||
|
||||
type jsonOutput struct {
|
||||
Period uint64 `json:"period"`
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Incumbent *phase0.ETH1Data `json:"incumbent"`
|
||||
Votes []*vote `json:"votes"`
|
||||
}
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.json {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
return c.outputText(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(ctx context.Context) (string, error) {
|
||||
votes := make([]*vote, 0, len(c.votes))
|
||||
totalVotes := 0
|
||||
for _, vote := range c.votes {
|
||||
votes = append(votes, vote)
|
||||
totalVotes += vote.Count
|
||||
}
|
||||
sort.Slice(votes, func(i int, j int) bool {
|
||||
if votes[i].Count != votes[j].Count {
|
||||
return votes[i].Count > votes[j].Count
|
||||
}
|
||||
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
|
||||
})
|
||||
|
||||
output := &jsonOutput{
|
||||
Period: c.period,
|
||||
Epoch: c.epoch,
|
||||
Slot: c.slot,
|
||||
Incumbent: c.incumbent,
|
||||
Votes: votes,
|
||||
}
|
||||
data, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputText(ctx context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Voting period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", c.period))
|
||||
|
||||
if c.verbose {
|
||||
builder.WriteString("Incumbent: ")
|
||||
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
|
||||
}
|
||||
|
||||
votes := make([]*vote, 0, len(c.votes))
|
||||
totalVotes := 0
|
||||
for _, vote := range c.votes {
|
||||
votes = append(votes, vote)
|
||||
totalVotes += vote.Count
|
||||
}
|
||||
sort.Slice(votes, func(i int, j int) bool {
|
||||
if votes[i].Count != votes[j].Count {
|
||||
return votes[i].Count > votes[j].Count
|
||||
}
|
||||
return votes[i].Vote.DepositCount < votes[j].Vote.DepositCount
|
||||
})
|
||||
|
||||
slot := c.chainTime.CurrentSlot()
|
||||
if slot > c.slot {
|
||||
slot = c.slot
|
||||
}
|
||||
|
||||
slotsThroughPeriod := slot + 1 - phase0.Slot(c.period*(c.slotsPerEpoch*c.epochsPerEth1VotingPeriod))
|
||||
builder.WriteString("Slots through period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d (%d)\n", slotsThroughPeriod, c.slot))
|
||||
|
||||
builder.WriteString("Votes this period: ")
|
||||
builder.WriteString(fmt.Sprintf("%d\n", totalVotes))
|
||||
|
||||
if len(votes) > 0 {
|
||||
if c.verbose {
|
||||
for _, vote := range votes {
|
||||
builder.WriteString(fmt.Sprintf(" block %#x, deposit count %d: %d vote", vote.Vote.BlockHash, vote.Vote.DepositCount, vote.Count))
|
||||
if vote.Count != 1 {
|
||||
builder.WriteString("s")
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf(" (%0.2f%%)\n", 100.0*float64(vote.Count)/float64(slotsThroughPeriod)))
|
||||
}
|
||||
} else {
|
||||
builder.WriteString(fmt.Sprintf("Leading vote is for block %#x with %d votes (%0.2f%%)\n", votes[0].Vote.BlockHash, votes[0].Count, 100.0*float64(votes[0].Count)/float64(slotsThroughPeriod)))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(builder.String(), "\n"), nil
|
||||
}
|
||||
164
cmd/chain/eth1votes/process.go
Normal file
164
cmd/chain/eth1votes/process.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
// Obtain information we need to process.
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
if c.xperiod != "" {
|
||||
period, err := strconv.ParseUint(c.xperiod, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.epoch = phase0.Epoch(c.epochsPerEth1VotingPeriod*(period+1)) - 1
|
||||
} else {
|
||||
c.epoch, err = util.ParseEpoch(ctx, c.chainTime, c.xepoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Do not fetch from the future.
|
||||
if c.epoch > c.chainTime.CurrentEpoch() {
|
||||
c.epoch = c.chainTime.CurrentEpoch()
|
||||
}
|
||||
|
||||
// Need to fetch the state from the last slot of the epoch.
|
||||
fetchSlot := c.chainTime.FirstSlotOfEpoch(c.epoch+1) - 1
|
||||
// Do not fetch from the future.
|
||||
if fetchSlot > c.chainTime.CurrentSlot() {
|
||||
fetchSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain state")
|
||||
}
|
||||
if state == nil {
|
||||
return errors.New("state not returned by beacon node")
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
data, err := json.Marshal(state)
|
||||
if err == nil {
|
||||
fmt.Printf("%s\n", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
switch state.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
c.slot = phase0.Slot(state.Phase0.Slot)
|
||||
c.incumbent = state.Phase0.ETH1Data
|
||||
c.eth1DataVotes = state.Phase0.ETH1DataVotes
|
||||
case spec.DataVersionAltair:
|
||||
c.slot = state.Altair.Slot
|
||||
c.incumbent = state.Altair.ETH1Data
|
||||
c.eth1DataVotes = state.Altair.ETH1DataVotes
|
||||
case spec.DataVersionBellatrix:
|
||||
c.slot = state.Bellatrix.Slot
|
||||
c.incumbent = state.Bellatrix.ETH1Data
|
||||
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
|
||||
case spec.DataVersionCapella:
|
||||
c.slot = state.Capella.Slot
|
||||
c.incumbent = state.Capella.ETH1Data
|
||||
c.eth1DataVotes = state.Capella.ETH1DataVotes
|
||||
default:
|
||||
return fmt.Errorf("unhandled beacon state version %v", state.Version)
|
||||
}
|
||||
|
||||
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
|
||||
|
||||
c.votes = make(map[string]*vote)
|
||||
for _, eth1Vote := range c.eth1DataVotes {
|
||||
key := fmt.Sprintf("%#x:%d", eth1Vote.BlockHash, eth1Vote.DepositCount)
|
||||
if _, exists := c.votes[key]; !exists {
|
||||
c.votes[key] = &vote{
|
||||
Vote: eth1Vote,
|
||||
}
|
||||
}
|
||||
c.votes[key].Count++
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
var isProvider bool
|
||||
c.beaconStateProvider, isProvider = c.eth2Client.(eth2client.BeaconStateProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon state")
|
||||
}
|
||||
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide spec information")
|
||||
}
|
||||
|
||||
spec, err := specProvider.Spec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain spec")
|
||||
}
|
||||
|
||||
tmp, exists := spec["SLOTS_PER_EPOCH"]
|
||||
if !exists {
|
||||
return errors.New("spec did not contain SLOTS_PER_EPOCH")
|
||||
}
|
||||
var good bool
|
||||
c.slotsPerEpoch, good = tmp.(uint64)
|
||||
if !good {
|
||||
return errors.New("SLOTS_PER_EPOCH value invalid")
|
||||
}
|
||||
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
|
||||
if !exists {
|
||||
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
|
||||
}
|
||||
c.epochsPerEth1VotingPeriod, good = tmp.(uint64)
|
||||
if !good {
|
||||
return errors.New("EPOCHS_PER_ETH1_VOTING_PERIOD value invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
cmd/chain/eth1votes/process_internal_test.go
Normal file
66
cmd/chain/eth1votes/process_internal_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"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")
|
||||
}
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidEpoch",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"epoch": "invalid",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "failed to parse epoch: strconv.ParseInt: parsing \"invalid\": invalid syntax",
|
||||
},
|
||||
}
|
||||
|
||||
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/eth1votes/run.go
Normal file
50
cmd/chain/eth1votes/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2022 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 chaineth1votes
|
||||
|
||||
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
|
||||
}
|
||||
72
cmd/chain/queues/command.go
Normal file
72
cmd/chain/queues/command.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright © 2022 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 chainqueues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
json bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Input.
|
||||
epoch string
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
chainTime chaintime.Service
|
||||
|
||||
// Output.
|
||||
activationQueue int
|
||||
exitQueue int
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
json: viper.GetBool("json"),
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("epoch") != "" {
|
||||
c.epoch = viper.GetString("epoch")
|
||||
}
|
||||
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
64
cmd/chain/queues/command_internal_test.go
Normal file
64
cmd/chain/queues/command_internal_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright © 2022 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 chainqueues
|
||||
|
||||
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: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
cmd/chain/queues/output.go
Normal file
63
cmd/chain/queues/output.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2022 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 chainqueues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type jsonOutput struct {
|
||||
ActivationQueue int `json:"activation_queue"`
|
||||
ExitQueue int `json:"exit_queue"`
|
||||
}
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.json {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
return c.outputText(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(ctx context.Context) (string, error) {
|
||||
output := &jsonOutput{
|
||||
ActivationQueue: c.activationQueue,
|
||||
ExitQueue: c.exitQueue,
|
||||
}
|
||||
data, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputText(ctx context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
if c.activationQueue > 0 {
|
||||
builder.WriteString(fmt.Sprintf("Activation queue: %d\n", c.activationQueue))
|
||||
}
|
||||
if c.exitQueue > 0 {
|
||||
builder.WriteString(fmt.Sprintf("Exit queue: %d\n", c.exitQueue))
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(builder.String(), "\n"), nil
|
||||
}
|
||||
81
cmd/chain/queues/process.go
Normal file
81
cmd/chain/queues/process.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright © 2022 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 chainqueues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
// Obtain information we need to process.
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
epoch, err := util.ParseEpoch(ctx, c.chainTime, c.epoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)), nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validators")
|
||||
}
|
||||
|
||||
for _, validator := range validators {
|
||||
if validator.Validator == nil {
|
||||
continue
|
||||
}
|
||||
if validator.Validator.ActivationEligibilityEpoch <= epoch && validator.Validator.ActivationEpoch > epoch {
|
||||
c.activationQueue++
|
||||
}
|
||||
if validator.Validator.ExitEpoch != 0xffffffffffffffff && validator.Validator.ExitEpoch > epoch {
|
||||
c.exitQueue++
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
var isProvider bool
|
||||
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide validator information")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
66
cmd/chain/queues/process_internal_test.go
Normal file
66
cmd/chain/queues/process_internal_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright © 2022 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 chainqueues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"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")
|
||||
}
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidEpoch",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"epoch": "invalid",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "failed to parse epoch: strconv.ParseInt: parsing \"invalid\": invalid syntax",
|
||||
},
|
||||
}
|
||||
|
||||
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/queues/run.go
Normal file
50
cmd/chain/queues/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2022 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 chainqueues
|
||||
|
||||
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
|
||||
}
|
||||
@@ -71,9 +71,6 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
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")
|
||||
|
||||
|
||||
@@ -60,14 +60,6 @@ func TestInput(t *testing.T) {
|
||||
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{}{
|
||||
|
||||
@@ -76,9 +76,6 @@ func newCommand(ctx context.Context) (*command, error) {
|
||||
}
|
||||
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")
|
||||
|
||||
|
||||
@@ -44,14 +44,6 @@ func TestInput(t *testing.T) {
|
||||
},
|
||||
err: "data is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"data": "{}",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -38,6 +38,7 @@ func (c *command) process(ctx context.Context) error {
|
||||
err := json.Unmarshal([]byte(c.data), c.item)
|
||||
if err != nil {
|
||||
c.additionalInfo = err.Error()
|
||||
//nolint:nilerr
|
||||
return nil
|
||||
}
|
||||
c.itemStructureValid = true
|
||||
@@ -124,7 +125,7 @@ func (c *command) setup(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isAggregator returns true if the given
|
||||
// isAggregator returns true if the given.
|
||||
func (c *command) isAggregator(ctx context.Context) (bool, error) {
|
||||
// Calculate the modulo.
|
||||
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
|
||||
@@ -204,6 +205,7 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
|
||||
_, err := e2types.BLSSignatureFromBytes(sigBytes)
|
||||
if err != nil {
|
||||
c.additionalInfo = err.Error()
|
||||
//nolint:nilerr
|
||||
return nil
|
||||
}
|
||||
c.contributionSignatureValidFormat = true
|
||||
@@ -256,6 +258,7 @@ func (c *command) confirmContributionAndProofSignature(ctx context.Context) erro
|
||||
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
|
||||
if err != nil {
|
||||
c.additionalInfo = err.Error()
|
||||
//nolint:nilerr
|
||||
return nil
|
||||
}
|
||||
c.contributionAndProofSignatureValidFormat = true
|
||||
|
||||
67
cmd/chaineth1votes.go
Normal file
67
cmd/chaineth1votes.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright © 2022 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"
|
||||
chaineth1votes "github.com/wealdtech/ethdo/cmd/chain/eth1votes"
|
||||
)
|
||||
|
||||
var chainEth1VotesCmd = &cobra.Command{
|
||||
Use: "eth1votes",
|
||||
Short: "Show chain execution votes",
|
||||
Long: `Show beacon chain execution votes. For example:
|
||||
|
||||
ethdo chain eth1votes
|
||||
|
||||
Note that this will fetch the votes made in blocks up to the end of the provided epoch.
|
||||
|
||||
In quiet mode this will return 0 if there is a majority for the votes, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := chaineth1votes.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainEth1VotesCmd)
|
||||
chainFlags(chainEth1VotesCmd)
|
||||
chainEth1VotesCmd.Flags().String("epoch", "", "epoch for which to fetch the votes")
|
||||
chainEth1VotesCmd.Flags().String("period", "", "period for which to fetch the votes")
|
||||
chainEth1VotesCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func chainEth1VotesBindings() {
|
||||
if err := viper.BindPFlag("epoch", chainEth1VotesCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("period", chainEth1VotesCmd.Flags().Lookup("period")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", chainEth1VotesCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Copyright © 2020, 2022 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
|
||||
@@ -53,6 +53,11 @@ In quiet mode this will return 0 if the chain information can be obtained, other
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if viper.GetBool("prepare-offline") {
|
||||
fmt.Printf("Add the following to your command to run it offline:\n --offline --genesis-validators=root=%#x --fork-version=%#x\n", genesis.GenesisValidatorsRoot, fork.CurrentVersion)
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
|
||||
if genesis.GenesisTime.Unix() == 0 {
|
||||
fmt.Println("Genesis time: undefined")
|
||||
} else {
|
||||
@@ -84,4 +89,11 @@ In quiet mode this will return 0 if the chain information can be obtained, other
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainInfoCmd)
|
||||
chainFlags(chainInfoCmd)
|
||||
chainInfoCmd.Flags().Bool("prepare-offline", false, "Provide information useful for offline commands")
|
||||
}
|
||||
|
||||
func chainInfoBindings() {
|
||||
if err := viper.BindPFlag("prepare-offline", chainInfoCmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
61
cmd/chainqueues.go
Normal file
61
cmd/chainqueues.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright © 2022 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"
|
||||
chainqueues "github.com/wealdtech/ethdo/cmd/chain/queues"
|
||||
)
|
||||
|
||||
var chainQueuesCmd = &cobra.Command{
|
||||
Use: "queues",
|
||||
Short: "Show chain queues",
|
||||
Long: `Show beacon chain activation and exit queues. For example:
|
||||
|
||||
ethdo chain queues
|
||||
|
||||
In quiet mode this will return 0 if the entry and exit queues are 0, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := chainqueues.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
chainCmd.AddCommand(chainQueuesCmd)
|
||||
chainFlags(chainQueuesCmd)
|
||||
chainQueuesCmd.Flags().String("epoch", "", "epoch for which to fetch the queues")
|
||||
chainQueuesCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func chainQueuesBindings() {
|
||||
if err := viper.BindPFlag("epoch", chainQueuesCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", chainQueuesCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,13 @@ import (
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
string2eth "github.com/wealdtech/go-string2eth"
|
||||
)
|
||||
|
||||
var chainStatusCmd = &cobra.Command{
|
||||
@@ -43,7 +46,6 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
|
||||
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")
|
||||
@@ -116,6 +118,34 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
|
||||
res.WriteString("\n")
|
||||
}
|
||||
|
||||
if verbose {
|
||||
validatorsProvider, isProvider := eth2Client.(eth2client.ValidatorsProvider)
|
||||
if isProvider {
|
||||
validators, err := validatorsProvider.Validators(ctx, "head", nil)
|
||||
errCheck(err, "Failed to obtain validators information")
|
||||
// Stats of inteest.
|
||||
totalBalance := phase0.Gwei(0)
|
||||
activeEffectiveBalance := phase0.Gwei(0)
|
||||
validatorCount := make(map[apiv1.ValidatorState]int)
|
||||
for _, validator := range validators {
|
||||
validatorCount[validator.Status]++
|
||||
totalBalance += validator.Balance
|
||||
if validator.Status.IsActive() {
|
||||
activeEffectiveBalance += validator.Validator.EffectiveBalance
|
||||
}
|
||||
}
|
||||
res.WriteString(fmt.Sprintf("Total balance: %s\n", string2eth.GWeiToString(uint64(totalBalance), true)))
|
||||
res.WriteString(fmt.Sprintf("Active effective balance: %s\n", string2eth.GWeiToString(uint64(activeEffectiveBalance), true)))
|
||||
res.WriteString("Validator states:\n")
|
||||
res.WriteString(fmt.Sprintf(" Pending: %d\n", validatorCount[apiv1.ValidatorStatePendingInitialized]))
|
||||
res.WriteString(fmt.Sprintf(" Activating: %d\n", validatorCount[apiv1.ValidatorStatePendingQueued]))
|
||||
res.WriteString(fmt.Sprintf(" Active: %d\n", validatorCount[apiv1.ValidatorStateActiveOngoing]+validatorCount[apiv1.ValidatorStateActiveSlashed]))
|
||||
res.WriteString(fmt.Sprintf(" Exiting: %d\n", validatorCount[apiv1.ValidatorStateActiveExiting]))
|
||||
res.WriteString(fmt.Sprintf(" Exited: %d\n", validatorCount[apiv1.ValidatorStateExitedUnslashed]+validatorCount[apiv1.ValidatorStateExitedSlashed]+validatorCount[apiv1.ValidatorStateWithdrawalPossible]+validatorCount[apiv1.ValidatorStateWithdrawalDone]))
|
||||
res.WriteString(fmt.Sprintf(" Unknown: %d\n", validatorCount[apiv1.ValidatorStateUnknown]))
|
||||
}
|
||||
}
|
||||
|
||||
if epoch >= chainTime.AltairInitialEpoch() {
|
||||
period := chainTime.SlotToSyncCommitteePeriod(slot)
|
||||
periodStartEpoch := chainTime.FirstEpochOfSyncPeriod(period)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// chainVerifyCmd represents the chain verify command
|
||||
// chainVerifyCmd represents the chain verify command.
|
||||
var chainVerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify a beacon chain signature",
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// depositCmd represents the deposit command
|
||||
// depositCmd represents the deposit command.
|
||||
var depositCmd = &cobra.Command{
|
||||
Use: "deposit",
|
||||
Short: "Manage Ethereum 2 deposits",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019-2021 Weald Technology Limited.
|
||||
// Copyright © 2019-2022 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
|
||||
@@ -17,11 +17,10 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
@@ -30,12 +29,14 @@ import (
|
||||
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 (
|
||||
depositVerifyData string
|
||||
depositVerifyWithdrawalPubKey string
|
||||
depositVerifyWithdrawalAddress string
|
||||
depositVerifyValidatorPubKey string
|
||||
depositVerifyDepositAmount string
|
||||
depositVerifyForkVersion string
|
||||
)
|
||||
|
||||
var depositVerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
@@ -46,7 +47,7 @@ var depositVerifyCmd = &cobra.Command{
|
||||
|
||||
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.`,
|
||||
In quiet mode this will return 0 if the data is verified correctly, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(depositVerifyData != "", "--data is required")
|
||||
var data []byte
|
||||
@@ -64,7 +65,7 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis
|
||||
data = []byte(depositVerifyData)
|
||||
default:
|
||||
// Assume it's a path to JSON.
|
||||
data, err = ioutil.ReadFile(depositVerifyData)
|
||||
data, err = os.ReadFile(depositVerifyData)
|
||||
errCheck(err, "Failed to read deposit data file")
|
||||
if data[0] == '{' {
|
||||
data = []byte("[" + string(data) + "]")
|
||||
@@ -155,7 +156,7 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
|
||||
pubKeys[key] = true
|
||||
} else {
|
||||
// Assume it's a path to a file of public keys.
|
||||
data, err = ioutil.ReadFile(input)
|
||||
data, err = os.ReadFile(input)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find public key file")
|
||||
}
|
||||
@@ -219,15 +220,15 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
|
||||
outputIf(!quiet, "Validator public key verified")
|
||||
}
|
||||
|
||||
var pubKey spec.BLSPubKey
|
||||
var pubKey phase0.BLSPubKey
|
||||
copy(pubKey[:], deposit.PublicKey)
|
||||
var signature spec.BLSSignature
|
||||
var signature phase0.BLSSignature
|
||||
copy(signature[:], deposit.Signature)
|
||||
|
||||
depositData := &spec.DepositData{
|
||||
depositData := &phase0.DepositData{
|
||||
PublicKey: pubKey,
|
||||
WithdrawalCredentials: deposit.WithdrawalCredentials,
|
||||
Amount: spec.Gwei(deposit.Amount),
|
||||
Amount: phase0.Gwei(deposit.Amount),
|
||||
Signature: signature,
|
||||
}
|
||||
depositDataRoot, err := depositData.HashTreeRoot()
|
||||
@@ -248,7 +249,7 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
|
||||
}
|
||||
} else {
|
||||
if depositVerifyForkVersion == "" {
|
||||
outputIf(!quiet, "fork version not supplied; NOT checked")
|
||||
outputIf(!quiet, "fork version not supplied; not checked")
|
||||
} else {
|
||||
forkVersion, err := hex.DecodeString(strings.TrimPrefix(depositVerifyForkVersion, "0x"))
|
||||
if err != nil {
|
||||
@@ -260,6 +261,56 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
|
||||
outputIf(!quiet, "Fork version incorrect")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(deposit.DepositMessageRoot) != 32 {
|
||||
outputIf(!quiet, "Deposit message root not supplied; not checked")
|
||||
} else {
|
||||
// We can also verify the deposit message signature.
|
||||
depositMessage := &phase0.DepositMessage{
|
||||
PublicKey: pubKey,
|
||||
WithdrawalCredentials: withdrawalCredentials,
|
||||
Amount: phase0.Gwei(deposit.Amount),
|
||||
}
|
||||
depositMessageRoot, err := depositMessage.HashTreeRoot()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate deposit message root")
|
||||
}
|
||||
|
||||
if bytes.Equal(deposit.DepositMessageRoot, depositMessageRoot[:]) {
|
||||
outputIf(!quiet, "Deposit message root verified")
|
||||
} else {
|
||||
outputIf(!quiet, "Deposit message root incorrect")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
domainBytes := e2types.Domain(e2types.DomainDeposit, forkVersion, e2types.ZeroGenesisValidatorsRoot)
|
||||
var domain phase0.Domain
|
||||
copy(domain[:], domainBytes)
|
||||
container := &phase0.SigningData{
|
||||
ObjectRoot: depositMessageRoot,
|
||||
Domain: domain,
|
||||
}
|
||||
containerRoot, err := container.HashTreeRoot()
|
||||
if err != nil {
|
||||
return false, errors.New("failed to generate root for container")
|
||||
}
|
||||
|
||||
validatorPubKey, err := e2types.BLSPublicKeyFromBytes(pubKey[:])
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate validator public key")
|
||||
}
|
||||
blsSig, err := e2types.BLSSignatureFromBytes(signature[:])
|
||||
if err != nil {
|
||||
return false, errors.New("failed to verify BLS signature")
|
||||
}
|
||||
signatureVerified := blsSig.Verify(containerRoot[:], validatorPubKey)
|
||||
if signatureVerified {
|
||||
outputIf(!quiet, "Deposit message signature verified")
|
||||
} else {
|
||||
outputIf(!quiet, "Deposit message signature NOT verified")
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
40
cmd/epoch.go
Normal file
40
cmd/epoch.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright © 2022 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"
|
||||
)
|
||||
|
||||
// epochCmd represents the epoch command.
|
||||
var epochCmd = &cobra.Command{
|
||||
Use: "epoch",
|
||||
Short: "Obtain information about Ethereum 2 epochs",
|
||||
Long: "Obtain information about Ethereum 2 epochs",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(epochCmd)
|
||||
}
|
||||
|
||||
func epochFlags(cmd *cobra.Command) {
|
||||
epochSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)")
|
||||
}
|
||||
|
||||
func epochBindings() {
|
||||
if err := viper.BindPFlag("epoch", epochSummaryCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
111
cmd/epoch/summary/command.go
Normal file
111
cmd/epoch/summary/command.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright © 2022 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 epochsummary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Operation.
|
||||
epoch string
|
||||
stream bool
|
||||
jsonOutput bool
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
proposerDutiesProvider eth2client.ProposerDutiesProvider
|
||||
blocksProvider eth2client.SignedBeaconBlockProvider
|
||||
syncCommitteesProvider eth2client.SyncCommitteesProvider
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
|
||||
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
|
||||
|
||||
// Results.
|
||||
summary *epochSummary
|
||||
}
|
||||
|
||||
type epochSummary struct {
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
FirstSlot phase0.Slot `json:"first_slot"`
|
||||
LastSlot phase0.Slot `json:"last_slot"`
|
||||
Proposals []*epochProposal `json:"proposals"`
|
||||
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
|
||||
ActiveValidators int `json:"active_validators"`
|
||||
ParticipatingValidators int `json:"participating_validators"`
|
||||
HeadCorrectValidators int `json:"head_correct_validators"`
|
||||
HeadTimelyValidators int `json:"head_timely_validators"`
|
||||
SourceTimelyValidators int `json:"source_timely_validators"`
|
||||
TargetCorrectValidators int `json:"target_correct_validators"`
|
||||
TargetTimelyValidators int `json:"target_timely_validators"`
|
||||
NonParticipatingValidators []*nonParticipatingValidator `json:"nonparticipating_validators"`
|
||||
}
|
||||
|
||||
type epochProposal struct {
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Proposer phase0.ValidatorIndex `json:"proposer"`
|
||||
Block bool `json:"block"`
|
||||
}
|
||||
|
||||
type epochSyncCommittee struct {
|
||||
Index phase0.ValidatorIndex `json:"index"`
|
||||
Missed int `json:"missed"`
|
||||
}
|
||||
|
||||
type nonParticipatingValidator struct {
|
||||
Validator phase0.ValidatorIndex `json:"validator_index"`
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Committee phase0.CommitteeIndex `json:"committee_index"`
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
summary: &epochSummary{},
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
c.epoch = viper.GetString("epoch")
|
||||
c.stream = viper.GetBool("stream")
|
||||
c.jsonOutput = viper.GetBool("json")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
64
cmd/epoch/summary/command_internal_test.go
Normal file
64
cmd/epoch/summary/command_internal_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright © 2022 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 epochsummary
|
||||
|
||||
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: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
110
cmd/epoch/summary/output.go
Normal file
110
cmd/epoch/summary/output.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright © 2022 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 epochsummary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.jsonOutput {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
|
||||
return c.outputTxt(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(_ context.Context) (string, error) {
|
||||
data, err := json.Marshal(c.summary)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d:\n", c.summary.Epoch))
|
||||
|
||||
proposedBlocks := 0
|
||||
missedProposals := make([]string, 0, len(c.summary.Proposals))
|
||||
for _, proposal := range c.summary.Proposals {
|
||||
if !proposal.Block {
|
||||
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.Proposer))
|
||||
} else {
|
||||
proposedBlocks++
|
||||
}
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf(" Proposals: %d/%d (%0.2f%%)", proposedBlocks, len(missedProposals)+proposedBlocks, 100.0*float64(proposedBlocks)/float64(len(missedProposals)+proposedBlocks)))
|
||||
if c.verbose {
|
||||
for _, proposal := range c.summary.Proposals {
|
||||
if proposal.Block {
|
||||
continue
|
||||
}
|
||||
builder.WriteString("\n Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d (%d/%d)", proposal.Slot, uint64(proposal.Slot)%uint64(len(c.summary.Proposals)), len(c.summary.Proposals)))
|
||||
builder.WriteString(" validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", proposal.Proposer))
|
||||
builder.WriteString(" not proposed or not included")
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteString(fmt.Sprintf("\n Attestations: %d/%d (%0.2f%%)", c.summary.ParticipatingValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.ParticipatingValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Source timely: %d/%d (%0.2f%%)", c.summary.SourceTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.SourceTimelyValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Target correct: %d/%d (%0.2f%%)", c.summary.TargetCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetCorrectValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Target timely: %d/%d (%0.2f%%)", c.summary.TargetTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.TargetTimelyValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Head correct: %d/%d (%0.2f%%)", c.summary.HeadCorrectValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadCorrectValidators)/float64(c.summary.ActiveValidators)))
|
||||
builder.WriteString(fmt.Sprintf("\n Head timely: %d/%d (%0.2f%%)", c.summary.HeadTimelyValidators, c.summary.ActiveValidators, 100.0*float64(c.summary.HeadTimelyValidators)/float64(c.summary.ActiveValidators)))
|
||||
if c.verbose {
|
||||
// Sort list by validator index.
|
||||
for _, validator := range c.summary.NonParticipatingValidators {
|
||||
builder.WriteString("\n Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d", validator.Slot))
|
||||
builder.WriteString(" committee ")
|
||||
builder.WriteString(fmt.Sprintf("%d", validator.Committee))
|
||||
builder.WriteString(" validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", validator.Validator))
|
||||
builder.WriteString(" failed to participate")
|
||||
}
|
||||
}
|
||||
|
||||
if c.summary.Epoch >= c.chainTime.AltairInitialEpoch() {
|
||||
contributions := proposedBlocks * 512 // SYNC_COMMITTEE_SIZE
|
||||
totalMissed := 0
|
||||
for _, contribution := range c.summary.SyncCommittee {
|
||||
totalMissed += contribution.Missed
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("\n Sync committees: %d/%d (%0.2f%%)", contributions-totalMissed, contributions, 100.0*float64(contributions-totalMissed)/float64(contributions)))
|
||||
if c.verbose {
|
||||
for _, syncCommittee := range c.summary.SyncCommittee {
|
||||
builder.WriteString("\n Validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index))
|
||||
builder.WriteString(" included ")
|
||||
builder.WriteString(fmt.Sprintf("%d/%d", proposedBlocks-syncCommittee.Missed, proposedBlocks))
|
||||
builder.WriteString(fmt.Sprintf(" (%0.2f%%)", 100.0*float64(proposedBlocks-syncCommittee.Missed)/float64(proposedBlocks)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
371
cmd/epoch/summary/process.go
Normal file
371
cmd/epoch/summary/process.go
Normal file
@@ -0,0 +1,371 @@
|
||||
// Copyright © 2022 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 epochsummary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
apiv1 "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"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
// Obtain information we need to process.
|
||||
err := c.setup(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.summary.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
c.summary.FirstSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)
|
||||
c.summary.LastSlot = c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) - 1
|
||||
|
||||
if err := c.processProposerDuties(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.processAttesterDuties(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.processSyncCommitteeDuties(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processProposerDuties(ctx context.Context) error {
|
||||
duties, err := c.proposerDutiesProvider.ProposerDuties(ctx, c.summary.Epoch, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
if duties == nil {
|
||||
return errors.New("empty proposer duties")
|
||||
}
|
||||
for _, duty := range duties {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", duty.Slot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", duty.Slot))
|
||||
}
|
||||
present := block != nil
|
||||
c.summary.Proposals = append(c.summary.Proposals, &epochProposal{
|
||||
Slot: duty.Slot,
|
||||
Proposer: duty.ValidatorIndex,
|
||||
Block: present,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorIndex]*apiv1.Validator, error) {
|
||||
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(c.summary.Epoch)), nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validators for epoch")
|
||||
}
|
||||
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
for _, validator := range validators {
|
||||
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
|
||||
activeValidators[validator.Index] = validator
|
||||
}
|
||||
}
|
||||
|
||||
return activeValidators, nil
|
||||
}
|
||||
|
||||
func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
activeValidators, err := c.activeValidators(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.summary.ActiveValidators = len(activeValidators)
|
||||
|
||||
// Obtain number of validators that voted for blocks in the epoch.
|
||||
// These votes can be included anywhere from the second slot of
|
||||
// the epoch to the first slot of the next-but-one epoch.
|
||||
firstSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) + 1
|
||||
lastSlot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch + 2)
|
||||
if lastSlot > c.chainTime.CurrentSlot() {
|
||||
lastSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
|
||||
var votes map[phase0.ValidatorIndex]struct{}
|
||||
var participations map[phase0.ValidatorIndex]*nonParticipatingValidator
|
||||
c.summary.ParticipatingValidators, c.summary.HeadCorrectValidators, c.summary.HeadTimelyValidators, c.summary.SourceTimelyValidators, c.summary.TargetCorrectValidators, c.summary.TargetTimelyValidators, votes, participations, err = c.processSlots(ctx, firstSlot, lastSlot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
|
||||
for activeValidatorIndex := range activeValidators {
|
||||
if _, exists := votes[activeValidatorIndex]; !exists {
|
||||
if _, exists := participations[activeValidatorIndex]; exists {
|
||||
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, participations[activeValidatorIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(c.summary.NonParticipatingValidators, func(i int, j int) bool {
|
||||
if c.summary.NonParticipatingValidators[i].Slot != c.summary.NonParticipatingValidators[j].Slot {
|
||||
return c.summary.NonParticipatingValidators[i].Slot < c.summary.NonParticipatingValidators[j].Slot
|
||||
}
|
||||
if c.summary.NonParticipatingValidators[i].Committee != c.summary.NonParticipatingValidators[j].Committee {
|
||||
return c.summary.NonParticipatingValidators[i].Committee < c.summary.NonParticipatingValidators[j].Committee
|
||||
}
|
||||
return c.summary.NonParticipatingValidators[i].Validator < c.summary.NonParticipatingValidators[j].Validator
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processSlots(ctx context.Context,
|
||||
firstSlot phase0.Slot,
|
||||
lastSlot phase0.Slot,
|
||||
) (
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
map[phase0.ValidatorIndex]struct{},
|
||||
map[phase0.ValidatorIndex]*nonParticipatingValidator,
|
||||
error,
|
||||
) {
|
||||
votes := make(map[phase0.ValidatorIndex]struct{})
|
||||
headCorrects := make(map[phase0.ValidatorIndex]struct{})
|
||||
headTimelys := make(map[phase0.ValidatorIndex]struct{})
|
||||
sourceTimelys := make(map[phase0.ValidatorIndex]struct{})
|
||||
targetCorrects := make(map[phase0.ValidatorIndex]struct{})
|
||||
targetTimelys := make(map[phase0.ValidatorIndex]struct{})
|
||||
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
|
||||
|
||||
// Need a cache of beacon block headers to reduce lookup times.
|
||||
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
|
||||
|
||||
for slot := firstSlot; slot <= lastSlot; slot++ {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
||||
}
|
||||
if block == nil {
|
||||
// No block at this slot; that's fine.
|
||||
continue
|
||||
}
|
||||
slot, err := block.Slot()
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
if attestation.Data.Slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch) || attestation.Data.Slot >= c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1) {
|
||||
// Outside of this epoch's range.
|
||||
continue
|
||||
}
|
||||
slotCommittees, exists := allCommittees[attestation.Data.Slot]
|
||||
if !exists {
|
||||
beaconCommittees, err := c.beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", attestation.Data.Slot))
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain committees for slot %d", attestation.Data.Slot))
|
||||
}
|
||||
for _, beaconCommittee := range beaconCommittees {
|
||||
if _, exists := allCommittees[beaconCommittee.Slot]; !exists {
|
||||
allCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
}
|
||||
allCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
|
||||
for _, index := range beaconCommittee.Validators {
|
||||
participations[index] = &nonParticipatingValidator{
|
||||
Validator: index,
|
||||
Slot: beaconCommittee.Slot,
|
||||
Committee: beaconCommittee.Index,
|
||||
}
|
||||
}
|
||||
}
|
||||
slotCommittees = allCommittees[attestation.Data.Slot]
|
||||
}
|
||||
committee := slotCommittees[attestation.Data.Index]
|
||||
|
||||
inclusionDistance := slot - attestation.Data.Slot
|
||||
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, 0, nil, nil, err
|
||||
}
|
||||
|
||||
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
|
||||
if attestation.AggregationBits.BitAt(i) {
|
||||
votes[committee[int(i)]] = struct{}{}
|
||||
if _, exists := headCorrects[committee[int(i)]]; !exists && headCorrect {
|
||||
headCorrects[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := headTimelys[committee[int(i)]]; !exists && headCorrect && inclusionDistance == 1 {
|
||||
headTimelys[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := sourceTimelys[committee[int(i)]]; !exists && inclusionDistance <= 5 {
|
||||
sourceTimelys[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := targetCorrects[committee[int(i)]]; !exists && targetCorrect {
|
||||
targetCorrects[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
if _, exists := targetTimelys[committee[int(i)]]; !exists && targetCorrect && inclusionDistance <= 32 {
|
||||
targetTimelys[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(votes),
|
||||
len(headCorrects),
|
||||
len(headTimelys),
|
||||
len(sourceTimelys),
|
||||
len(targetCorrects),
|
||||
len(targetTimelys),
|
||||
votes,
|
||||
participations,
|
||||
nil
|
||||
}
|
||||
|
||||
func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
if c.summary.Epoch < c.chainTime.AltairInitialEpoch() {
|
||||
// The epoch is pre-Altair. No info but no error.
|
||||
return nil
|
||||
}
|
||||
|
||||
committee, err := c.syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", c.summary.FirstSlot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain sync committee")
|
||||
}
|
||||
if len(committee.Validators) == 0 {
|
||||
return errors.Wrap(err, "empty sync committee")
|
||||
}
|
||||
|
||||
missed := make(map[phase0.ValidatorIndex]int)
|
||||
for _, index := range committee.Validators {
|
||||
missed[index] = 0
|
||||
}
|
||||
|
||||
for slot := c.summary.FirstSlot; slot <= c.summary.LastSlot; slot++ {
|
||||
block, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to obtain block for slot %d", slot))
|
||||
}
|
||||
if block == nil {
|
||||
// If the block is missed we don't count the sync aggregate miss.
|
||||
continue
|
||||
}
|
||||
var aggregate *altair.SyncAggregate
|
||||
switch block.Version {
|
||||
case spec.DataVersionPhase0:
|
||||
// No sync committees in this fork.
|
||||
return nil
|
||||
case spec.DataVersionAltair:
|
||||
aggregate = block.Altair.Message.Body.SyncAggregate
|
||||
case spec.DataVersionBellatrix:
|
||||
aggregate = block.Bellatrix.Message.Body.SyncAggregate
|
||||
case spec.DataVersionCapella:
|
||||
aggregate = block.Capella.Message.Body.SyncAggregate
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
for i := uint64(0); i < aggregate.SyncCommitteeBits.Len(); i++ {
|
||||
if !aggregate.SyncCommitteeBits.BitAt(i) {
|
||||
missed[committee.Validators[int(i)]]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.summary.SyncCommittee = make([]*epochSyncCommittee, 0, len(missed))
|
||||
for index, count := range missed {
|
||||
if count > 0 {
|
||||
c.summary.SyncCommittee = append(c.summary.SyncCommittee, &epochSyncCommittee{
|
||||
Index: index,
|
||||
Missed: count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(c.summary.SyncCommittee, func(i int, j int) bool {
|
||||
missedDiff := c.summary.SyncCommittee[i].Missed - c.summary.SyncCommittee[j].Missed
|
||||
if missedDiff != 0 {
|
||||
// Actually want to order by missed descending, so invert the expected condition.
|
||||
return missedDiff > 0
|
||||
}
|
||||
// Then order by validator index.
|
||||
return c.summary.SyncCommittee[i].Index < c.summary.SyncCommittee[j].Index
|
||||
})
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
var isProvider bool
|
||||
c.proposerDutiesProvider, isProvider = c.eth2Client.(eth2client.ProposerDutiesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide proposer duties")
|
||||
}
|
||||
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide signed beacon blocks")
|
||||
}
|
||||
c.syncCommitteesProvider, isProvider = c.eth2Client.(eth2client.SyncCommitteesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide sync committee duties")
|
||||
}
|
||||
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide validators")
|
||||
}
|
||||
c.beaconCommitteesProvider, isProvider = c.eth2Client.(eth2client.BeaconCommitteesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon committees")
|
||||
}
|
||||
c.beaconBlockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide beacon block headers")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
cmd/epoch/summary/process_internal_test.go
Normal file
62
cmd/epoch/summary/process_internal_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright © 2022 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 epochsummary
|
||||
|
||||
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": "60s",
|
||||
"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/epoch/summary/run.go
Normal file
50
cmd/epoch/summary/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2022 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 epochsummary
|
||||
|
||||
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
|
||||
}
|
||||
58
cmd/epochsummary.go
Normal file
58
cmd/epochsummary.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright © 2022 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"
|
||||
epochsummary "github.com/wealdtech/ethdo/cmd/epoch/summary"
|
||||
)
|
||||
|
||||
var epochSummaryCmd = &cobra.Command{
|
||||
Use: "summary",
|
||||
Short: "Obtain summary information about an epoch",
|
||||
Long: `Obtain summary information about an epoch. For example:
|
||||
|
||||
ethdo epoch summary --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if information for the epoch is found, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := epochsummary.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
epochCmd.AddCommand(epochSummaryCmd)
|
||||
epochFlags(epochSummaryCmd)
|
||||
epochSummaryCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func epochSummaryBindings() {
|
||||
epochBindings()
|
||||
if err := viper.BindPFlag("json", epochSummaryCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// errCheck checks for an error and quits if it is present
|
||||
// errCheck checks for an error and quits if it is present.
|
||||
func errCheck(err error, msg string) {
|
||||
if err != nil {
|
||||
if !quiet {
|
||||
@@ -48,14 +48,14 @@ func errCheck(err error, msg string) {
|
||||
// }
|
||||
// }
|
||||
|
||||
// assert checks a condition and quits if it is false
|
||||
// assert checks a condition and quits if it is false.
|
||||
func assert(condition bool, msg string) {
|
||||
if !condition {
|
||||
die(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// die prints an error and quits
|
||||
// die prints an error and quits.
|
||||
func die(msg string) {
|
||||
if msg != "" && !quiet {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", msg)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// exitCmd represents the exit command
|
||||
// exitCmd represents the exit command.
|
||||
var exitCmd = &cobra.Command{
|
||||
Use: "exit",
|
||||
Short: "Manage Ethereum 2 voluntary exits",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Copyright © 2020, 2022 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
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -42,7 +41,7 @@ var exitVerifyCmd = &cobra.Command{
|
||||
|
||||
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.`,
|
||||
In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -97,7 +96,7 @@ func obtainExitData(input string) (*util.ValidatorExitData, error) {
|
||||
data = []byte(input)
|
||||
} else {
|
||||
// Assume it's a path to JSON
|
||||
data, err = ioutil.ReadFile(input)
|
||||
data, err = os.ReadFile(input)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find deposit data file")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// nodeCmd represents the node command
|
||||
// nodeCmd represents the node command.
|
||||
var nodeCmd = &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Obtain information about an Ethereum 2 node",
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "ConnectionBad",
|
||||
@@ -75,7 +75,7 @@ func TestInput(t *testing.T) {
|
||||
"connection": "localhost:1",
|
||||
"topics": []string{"one", "two"},
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
|
||||
},
|
||||
{
|
||||
name: "TopicsNil",
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// proposerCmd represents the proposer command
|
||||
// proposerCmd represents the proposer command.
|
||||
var proposerCmd = &cobra.Command{
|
||||
Use: "proposer",
|
||||
Short: "Obtain information about Ethereum 2 proposers",
|
||||
@@ -27,3 +27,6 @@ var proposerCmd = &cobra.Command{
|
||||
func init() {
|
||||
RootCmd.AddCommand(proposerCmd)
|
||||
}
|
||||
|
||||
func proposerFlags(cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
77
cmd/proposer/duties/command.go
Normal file
77
cmd/proposer/duties/command.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright © 2022 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 proposerduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Operation.
|
||||
epoch string
|
||||
jsonOutput bool
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
proposerDutiesProvider eth2client.ProposerDutiesProvider
|
||||
|
||||
// Results.
|
||||
results *results
|
||||
}
|
||||
|
||||
type results struct {
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Duties []*apiv1.ProposerDuty `json:"duties"`
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
results: &results{},
|
||||
}
|
||||
|
||||
// Timeout.
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
c.epoch = viper.GetString("epoch")
|
||||
c.jsonOutput = viper.GetBool("json")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
72
cmd/proposer/duties/command_internal_test.go
Normal file
72
cmd/proposer/duties/command_internal_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright © 2022 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 proposerduties
|
||||
|
||||
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: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GoodWithEpoch",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"epoch": "-1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
62
cmd/proposer/duties/output.go
Normal file
62
cmd/proposer/duties/output.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright © 2022 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 proposerduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.jsonOutput {
|
||||
return c.outputJSON(ctx)
|
||||
}
|
||||
|
||||
return c.outputTxt(ctx)
|
||||
}
|
||||
|
||||
func (c *command) outputJSON(_ context.Context) (string, error) {
|
||||
data, err := json.Marshal(c.results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
builder := strings.Builder{}
|
||||
|
||||
builder.WriteString("Epoch ")
|
||||
builder.WriteString(fmt.Sprintf("%d:\n", c.results.Epoch))
|
||||
|
||||
for _, duty := range c.results.Duties {
|
||||
builder.WriteString(" Slot ")
|
||||
builder.WriteString(fmt.Sprintf("%d: ", duty.Slot))
|
||||
builder.WriteString("validator ")
|
||||
builder.WriteString(fmt.Sprintf("%d", duty.ValidatorIndex))
|
||||
if c.verbose {
|
||||
builder.WriteString(" (pubkey ")
|
||||
builder.WriteString(fmt.Sprintf("%#x)", duty.PubKey))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(builder.String(), "\n"), nil
|
||||
}
|
||||
69
cmd/proposer/duties/process.go
Normal file
69
cmd/proposer/duties/process.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2022 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 proposerduties
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/pkg/errors"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
// Obtain information we need to process.
|
||||
err := c.setup(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.results.Epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epoch)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse epoch")
|
||||
}
|
||||
|
||||
c.results.Duties, err = c.proposerDutiesProvider.ProposerDuties(ctx, c.results.Epoch, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain proposer duties")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set up chaintime service")
|
||||
}
|
||||
|
||||
var isProvider bool
|
||||
c.proposerDutiesProvider, isProvider = c.eth2Client.(eth2client.ProposerDutiesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide proposer duties")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
62
cmd/proposer/duties/process_internal_test.go
Normal file
62
cmd/proposer/duties/process_internal_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright © 2022 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 proposerduties
|
||||
|
||||
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": "60s",
|
||||
"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/proposer/duties/run.go
Normal file
50
cmd/proposer/duties/run.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2022 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 proposerduties
|
||||
|
||||
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
|
||||
}
|
||||
61
cmd/proposerduties.go
Normal file
61
cmd/proposerduties.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright © 2022 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"
|
||||
proposerduties "github.com/wealdtech/ethdo/cmd/proposer/duties"
|
||||
)
|
||||
|
||||
var proposerDutiesCmd = &cobra.Command{
|
||||
Use: "duties",
|
||||
Short: "Obtain information about duties of an proposer",
|
||||
Long: `Obtain information about dutes of an proposer. For example:
|
||||
|
||||
ethdo proposer duties --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if duties can be obtained, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := proposerduties.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
proposerCmd.AddCommand(proposerDutiesCmd)
|
||||
proposerFlags(proposerDutiesCmd)
|
||||
proposerDutiesCmd.Flags().String("epoch", "", "the epoch for which to fetch duties")
|
||||
proposerDutiesCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func proposerDutiesBindings() {
|
||||
if err := viper.BindPFlag("epoch", proposerDutiesCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", proposerDutiesCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
76
cmd/root.go
76
cmd/root.go
@@ -33,12 +33,14 @@ import (
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var quiet bool
|
||||
var verbose bool
|
||||
var debug bool
|
||||
var (
|
||||
cfgFile string
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
)
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
// RootCmd represents the base command when called without any subcommands.
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "ethdo",
|
||||
Short: "Ethereum 2 CLI",
|
||||
@@ -77,6 +79,7 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
return util.SetupStore()
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func includeCommandBindings(cmd *cobra.Command) {
|
||||
switch commandPath(cmd) {
|
||||
case "account/create":
|
||||
@@ -93,20 +96,34 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
blockAnalyzeBindings()
|
||||
case "block/info":
|
||||
blockInfoBindings()
|
||||
case "chain/eth1votes":
|
||||
chainEth1VotesBindings()
|
||||
case "chain/info":
|
||||
chainInfoBindings()
|
||||
case "chain/queues":
|
||||
chainQueuesBindings()
|
||||
case "chain/time":
|
||||
chainTimeBindings()
|
||||
case "chain/verify/signedcontributionandproof":
|
||||
chainVerifySignedContributionAndProofBindings(cmd)
|
||||
case "epoch/summary":
|
||||
epochSummaryBindings()
|
||||
case "exit/verify":
|
||||
exitVerifyBindings()
|
||||
case "node/events":
|
||||
nodeEventsBindings()
|
||||
case "proposer/duties":
|
||||
proposerDutiesBindings()
|
||||
case "slot/time":
|
||||
slotTimeBindings()
|
||||
case "synccommittee/inclusion":
|
||||
synccommitteeInclusionBindings()
|
||||
case "synccommittee/members":
|
||||
synccommitteeMembersBindings()
|
||||
case "validator/credentials/get":
|
||||
validatorCredentialsGetBindings()
|
||||
case "validator/credentials/set":
|
||||
validatorCredentialsSetBindings()
|
||||
case "validator/depositdata":
|
||||
validatorDepositdataBindings()
|
||||
case "validator/duties":
|
||||
@@ -117,6 +134,10 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
validatorInfoBindings()
|
||||
case "validator/keycheck":
|
||||
validatorKeycheckBindings()
|
||||
case "validator/summary":
|
||||
validatorSummaryBindings()
|
||||
case "validator/yield":
|
||||
validatorYieldBindings()
|
||||
case "validator/expectation":
|
||||
validatorExpectationBindings()
|
||||
case "wallet/create":
|
||||
@@ -156,10 +177,26 @@ func init() {
|
||||
if err := viper.BindPFlag("store", RootCmd.PersistentFlags().Lookup("store")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("account", "", "Account name (in format \"wallet/account\")")
|
||||
RootCmd.PersistentFlags().String("account", "", `Account name (in format "<wallet>/<account>")`)
|
||||
if err := viper.BindPFlag("account", RootCmd.PersistentFlags().Lookup("account")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("mnemonic", "", "Mnemonic to provide access to an account")
|
||||
if err := viper.BindPFlag("mnemonic", RootCmd.PersistentFlags().Lookup("mnemonic")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("path", "", "Hierarchical derivation path used with mnemonic to provide access to an account")
|
||||
if err := viper.BindPFlag("path", RootCmd.PersistentFlags().Lookup("path")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("private-key", "", "Private key to provide access to an account")
|
||||
if err := viper.BindPFlag("private-key", RootCmd.PersistentFlags().Lookup("private-key")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("public-key", "", "public key to provide access to an account")
|
||||
if err := viper.BindPFlag("public-key", RootCmd.PersistentFlags().Lookup("public-key")); 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)
|
||||
@@ -209,7 +246,7 @@ func init() {
|
||||
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("connection", "http://localhost:3500", "URL to an Ethereum 2 node's RET API endpoint")
|
||||
RootCmd.PersistentFlags().String("connection", "", "URL to an Ethereum 2 node's REST API endpoint")
|
||||
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -307,7 +344,12 @@ func walletFromPath(ctx context.Context, path string) (e2wtypes.Wallet, error) {
|
||||
return nil, errors.Wrap(err, "failed to parse remote servers")
|
||||
}
|
||||
|
||||
return dirk.OpenWallet(ctx, walletName, credentials, endpoints)
|
||||
return dirk.Open(ctx,
|
||||
dirk.WithName(walletName),
|
||||
dirk.WithCredentials(credentials),
|
||||
dirk.WithEndpoints(endpoints),
|
||||
dirk.WithTimeout(viper.GetDuration("timeout")),
|
||||
)
|
||||
}
|
||||
wallet, err := e2wallet.OpenWallet(walletName)
|
||||
if err != nil {
|
||||
@@ -362,24 +404,6 @@ func walletAndAccountFromPath(ctx context.Context, path string) (e2wtypes.Wallet
|
||||
return wallet, account, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
publicKeyProvider, isPublicKeyProvider := account.(e2wtypes.AccountPublicKeyProvider)
|
||||
if isPublicKeyProvider {
|
||||
pubKey = publicKeyProvider.PublicKey()
|
||||
} else {
|
||||
return nil, errors.New("account does not provide a public key")
|
||||
}
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
// remotesToEndpoints generates endpoints from remote addresses.
|
||||
func remotesToEndpoints(remotes []string) ([]*dirk.Endpoint, error) {
|
||||
endpoints := make([]*dirk.Endpoint, 0)
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// signatureCmd represents the signature command
|
||||
// signatureCmd represents the signature command.
|
||||
var signatureCmd = &cobra.Command{
|
||||
Use: "signature",
|
||||
Aliases: []string{"sig"},
|
||||
@@ -31,8 +31,10 @@ func init() {
|
||||
RootCmd.AddCommand(signatureCmd)
|
||||
}
|
||||
|
||||
var dataFlag *pflag.Flag
|
||||
var domainFlag *pflag.Flag
|
||||
var (
|
||||
dataFlag *pflag.Flag
|
||||
domainFlag *pflag.Flag
|
||||
)
|
||||
|
||||
func signatureFlags(cmd *cobra.Command) {
|
||||
if dataFlag == nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
var signatureAggregateSignatures []string
|
||||
|
||||
// signatureAggregateCmd represents the signature aggregate command
|
||||
// signatureAggregateCmd represents the signature aggregate command.
|
||||
var signatureAggregateCmd = &cobra.Command{
|
||||
Use: "aggregate",
|
||||
Short: "Aggregate signatures",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2017-2020 Weald Technology Trading
|
||||
// Copyright © 2017-2023 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
|
||||
@@ -24,9 +24,10 @@ import (
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
"github.com/wealdtech/go-bytesutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
// signatureSignCmd represents the signature sign command
|
||||
// signatureSignCmd represents the signature sign command.
|
||||
var signatureSignCmd = &cobra.Command{
|
||||
Use: "sign",
|
||||
Short: "Sign a 32-byte piece of data",
|
||||
@@ -52,14 +53,20 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Domain is %#x", domain))
|
||||
|
||||
assert(viper.GetString("account") != "", "--account is required")
|
||||
_, account, err := walletAndAccountFromInput(ctx)
|
||||
var account e2wtypes.Account
|
||||
switch {
|
||||
case viper.GetString("account") != "":
|
||||
account, err = util.ParseAccount(ctx, viper.GetString("account"), util.GetPassphrases(), true)
|
||||
case viper.GetString("private-key") != "":
|
||||
account, err = util.ParseAccount(ctx, viper.GetString("private-key"), nil, true)
|
||||
}
|
||||
errCheck(err, "Failed to obtain account")
|
||||
|
||||
var specDomain spec.Domain
|
||||
copy(specDomain[:], domain)
|
||||
var fixedSizeData [32]byte
|
||||
copy(fixedSizeData[:], data)
|
||||
fmt.Printf("Signing %#x with domain %#x by public key %#x\n", fixedSizeData, specDomain, account.PublicKey().Marshal())
|
||||
signature, err := util.SignRoot(account, fixedSizeData, specDomain)
|
||||
errCheck(err, "Failed to sign")
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2017-2020 Weald Technology Trading
|
||||
// Copyright © 2017-2023 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,13 +15,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
@@ -30,10 +27,12 @@ import (
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var signatureVerifySignature string
|
||||
var signatureVerifySigner string
|
||||
var (
|
||||
signatureVerifySignature string
|
||||
signatureVerifySigner string
|
||||
)
|
||||
|
||||
// signatureVerifyCmd represents the signature verify command
|
||||
// signatureVerifyCmd represents the signature verify command.
|
||||
var signatureVerifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify signed data",
|
||||
@@ -43,6 +42,9 @@ var signatureVerifyCmd = &cobra.Command{
|
||||
|
||||
In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
|
||||
assert(viper.GetString("signature-data") != "", "--data is required")
|
||||
data, err := bytesutil.FromHexString(viper.GetString("signature-data"))
|
||||
errCheck(err, "Failed to parse data")
|
||||
@@ -61,7 +63,15 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
|
||||
assert(len(domain) == 32, "Domain data invalid")
|
||||
}
|
||||
|
||||
account, err := signatureVerifyAccount()
|
||||
var account e2wtypes.Account
|
||||
switch {
|
||||
case viper.GetString("account") != "":
|
||||
account, err = util.ParseAccount(ctx, viper.GetString("account"), nil, false)
|
||||
case viper.GetString("private-key") != "":
|
||||
account, err = util.ParseAccount(ctx, viper.GetString("private-key"), nil, false)
|
||||
case viper.GetString("public-key") != "":
|
||||
account, err = util.ParseAccount(ctx, viper.GetString("public-key"), nil, false)
|
||||
}
|
||||
errCheck(err, "Failed to obtain account")
|
||||
outputIf(debug, fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal()))
|
||||
|
||||
@@ -78,29 +88,6 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
|
||||
},
|
||||
}
|
||||
|
||||
// signatureVerifyAccount obtains the account for the signature verify command.
|
||||
func signatureVerifyAccount() (e2wtypes.Account, error) {
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
if viper.GetString("account") != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
_, 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(signatureVerifySigner, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", signatureVerifySigner))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", signatureVerifySigner))
|
||||
}
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
func init() {
|
||||
signatureCmd.AddCommand(signatureVerifyCmd)
|
||||
signatureFlags(signatureVerifyCmd)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// slotCmd represents the slot command
|
||||
// slotCmd represents the slot command.
|
||||
var slotCmd = &cobra.Command{
|
||||
Use: "slot",
|
||||
Short: "Obtain information about an Ethereum 2 slot",
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
|
||||
"timeout": "5s",
|
||||
"slot": "1",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestOutput(t *testing.T) {
|
||||
dataOut: &dataOut{
|
||||
startTime: time.Unix(1606824023, 0),
|
||||
},
|
||||
res: "2020-12-01 12:00:23 +0000 GMT",
|
||||
res: "2020-12-01 12:00:23 +0000 UTC",
|
||||
},
|
||||
{
|
||||
name: "Verbose",
|
||||
@@ -46,7 +46,7 @@ func TestOutput(t *testing.T) {
|
||||
endTime: time.Unix(1606824035, 0),
|
||||
verbose: true,
|
||||
},
|
||||
res: "2020-12-01 12:00:23 +0000 GMT - 2020-12-01 12:00:35 +0000 GMT",
|
||||
res: "2020-12-01 12:00:23 +0000 UTC - 2020-12-01 12:00:35 +0000 UTC",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// synccommitteeCmd represents the synccommittee command
|
||||
// synccommitteeCmd represents the synccommittee command.
|
||||
var synccommitteeCmd = &cobra.Command{
|
||||
Use: "synccommittee",
|
||||
Short: "Obtain information about Ethereum 2 sync committees",
|
||||
|
||||
@@ -62,9 +62,7 @@ func newCommand(ctx context.Context) (*command, error) {
|
||||
}
|
||||
c.timeout = viper.GetDuration("timeout")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
// Connection.
|
||||
c.connection = viper.GetString("connection")
|
||||
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
|
||||
@@ -37,14 +37,6 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"validators": "1",
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "NoValidator",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -87,6 +87,13 @@ func (c *command) process(ctx context.Context) error {
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
case spec.DataVersionCapella:
|
||||
aggregate = block.Capella.Message.Body.SyncAggregate
|
||||
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
|
||||
c.inclusions = append(c.inclusions, 1)
|
||||
} else {
|
||||
c.inclusions = append(c.inclusions, 2)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unhandled block version %v", block.Version)
|
||||
}
|
||||
@@ -107,7 +114,6 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -32,6 +32,14 @@ func TestProcess(t *testing.T) {
|
||||
vars map[string]interface{}
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "MissingConnection",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
},
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "InvalidConnection",
|
||||
vars: map[string]interface{}{
|
||||
@@ -39,7 +47,7 @@ func TestProcess(t *testing.T) {
|
||||
"index": "1",
|
||||
"connection": "invalid",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid: no such host",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
|
||||
@@ -59,7 +59,6 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
// Chain time.
|
||||
data.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -65,7 +65,15 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
|
||||
err: "failed to connect to any beacon node",
|
||||
},
|
||||
{
|
||||
name: "ConnectionInvalid",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": "localhost:1",
|
||||
},
|
||||
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ func TestProcess(t *testing.T) {
|
||||
|
||||
chainTime, err := standardchaintime.New(context.Background(),
|
||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||
standardchaintime.WithForkScheduleProvider(eth2Client.(eth2client.ForkScheduleProvider)),
|
||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user