mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-09 22:18:01 -05:00
Compare commits
61 Commits
v1.25.0
...
v1.27.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
35
.github/workflows/golangci-lint.yml
vendored
35
.github/workflows/golangci-lint.yml
vendored
@@ -1,23 +1,24 @@
|
||||
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.45
|
||||
|
||||
# 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.19
|
||||
- uses: actions/checkout@v3
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
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.17
|
||||
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/crazy-max/xgo@v0.14.0
|
||||
- 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,linux/arm64" 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
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
||||
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,4 +1,4 @@
|
||||
FROM golang:1.17-bullseye as builder
|
||||
FROM golang:1.18-bullseye as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
30
README.md
30
README.md
@@ -35,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`.
|
||||
@@ -64,7 +64,7 @@ Alternatively, if the beacon node is running in a separate docker container a sh
|
||||
|
||||
## 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/). The following changes are required to beacon nodes to make this available.
|
||||
`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.
|
||||
@@ -88,7 +88,7 @@ The default port for the REST API is 5051, which can be changed with the `--rest
|
||||
|
||||
## 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
|
||||
|
||||
@@ -98,12 +98,12 @@ The default port for the REST API is 5051, which can be changed with the `--rest
|
||||
- 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.
|
||||
@@ -138,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.
|
||||
@@ -178,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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ package accountimport
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -115,7 +115,7 @@ func obtainKeystore(input string) ([]byte, 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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,14 +37,6 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"blockid": "1",
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -223,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))
|
||||
}
|
||||
}
|
||||
@@ -232,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{}
|
||||
|
||||
@@ -270,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")
|
||||
}
|
||||
@@ -296,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)
|
||||
@@ -351,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
|
||||
}
|
||||
@@ -386,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)
|
||||
@@ -469,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)
|
||||
@@ -520,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,
|
||||
@@ -540,6 +658,8 @@ func outputBlockExecutionPayload(ctx context.Context,
|
||||
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: ")
|
||||
@@ -573,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,6 +24,7 @@ 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"
|
||||
)
|
||||
@@ -75,6 +76,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 +87,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 +109,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 +123,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 +173,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 +197,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 +221,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
|
||||
}
|
||||
|
||||
@@ -77,9 +77,6 @@ func newCommand(ctx context.Context) (*command, error) {
|
||||
c.xepoch = viper.GetString("epoch")
|
||||
c.xperiod = viper.GetString("period")
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
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{}{
|
||||
"timeout": "5s",
|
||||
"data": "{}",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -79,13 +79,17 @@ func (c *command) process(ctx context.Context) error {
|
||||
c.incumbent = state.Phase0.ETH1Data
|
||||
c.eth1DataVotes = state.Phase0.ETH1DataVotes
|
||||
case spec.DataVersionAltair:
|
||||
c.slot = phase0.Slot(state.Altair.Slot)
|
||||
c.slot = state.Altair.Slot
|
||||
c.incumbent = state.Altair.ETH1Data
|
||||
c.eth1DataVotes = state.Altair.ETH1DataVotes
|
||||
case spec.DataVersionBellatrix:
|
||||
c.slot = phase0.Slot(state.Bellatrix.Slot)
|
||||
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)
|
||||
}
|
||||
@@ -117,7 +121,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 {
|
||||
|
||||
@@ -65,9 +65,6 @@ func newCommand(ctx context.Context) (*command, error) {
|
||||
c.epoch = viper.GetString("epoch")
|
||||
}
|
||||
|
||||
if viper.GetString("connection") == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
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{}{
|
||||
"timeout": "5s",
|
||||
"data": "{}",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -65,7 +65,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 {
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,7 +17,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -64,7 +63,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 +154,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")
|
||||
}
|
||||
@@ -275,6 +274,13 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
|
||||
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)
|
||||
|
||||
@@ -40,13 +40,14 @@ type command struct {
|
||||
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
|
||||
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
|
||||
@@ -60,6 +61,11 @@ type epochSummary struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -94,9 +100,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,13 +37,6 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -70,6 +70,11 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -79,11 +79,10 @@ func (c *command) processProposerDuties(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
// Obtain all active validators for the given epoch.
|
||||
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 errors.Wrap(err, "failed to obtain validators for epoch")
|
||||
return nil, errors.Wrap(err, "failed to obtain validators for epoch")
|
||||
}
|
||||
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
for _, validator := range validators {
|
||||
@@ -92,6 +91,15 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -101,61 +109,13 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
lastSlot = c.chainTime.CurrentSlot()
|
||||
}
|
||||
|
||||
votes := make(map[phase0.ValidatorIndex]struct{})
|
||||
allCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
|
||||
participations := make(map[phase0.ValidatorIndex]*nonParticipatingValidator)
|
||||
|
||||
for slot := firstSlot; slot <= 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 {
|
||||
// No block at this slot; that's fine.
|
||||
continue
|
||||
}
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return 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 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]
|
||||
for i := uint64(0); i < attestation.AggregationBits.Len(); i++ {
|
||||
if attestation.AggregationBits.BitAt(i) {
|
||||
votes[committee[int(i)]] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
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.ActiveValidators = len(activeValidators)
|
||||
c.summary.ParticipatingValidators = len(votes)
|
||||
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0, len(activeValidators)-len(votes))
|
||||
for activeValidatorIndex := range activeValidators {
|
||||
if _, exists := votes[activeValidatorIndex]; !exists {
|
||||
@@ -177,6 +137,121 @@ func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
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.
|
||||
@@ -214,6 +289,8 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
|
||||
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)
|
||||
}
|
||||
@@ -258,7 +335,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 {
|
||||
@@ -286,6 +362,10 @@ func (c *command) setup(ctx context.Context) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -67,9 +67,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,13 +37,6 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -53,7 +53,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 {
|
||||
|
||||
51
cmd/root.go
51
cmd/root.go
@@ -96,6 +96,8 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
blockInfoBindings()
|
||||
case "chain/eth1votes":
|
||||
chainEth1VotesBindings()
|
||||
case "chain/info":
|
||||
chainInfoBindings()
|
||||
case "chain/queues":
|
||||
chainQueuesBindings()
|
||||
case "chain/time":
|
||||
@@ -118,6 +120,8 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
synccommitteeMembersBindings()
|
||||
case "validator/credentials/get":
|
||||
validatorCredentialsGetBindings()
|
||||
case "validator/credentials/set":
|
||||
validatorCredentialsSetBindings()
|
||||
case "validator/depositdata":
|
||||
validatorDepositdataBindings()
|
||||
case "validator/duties":
|
||||
@@ -128,6 +132,8 @@ func includeCommandBindings(cmd *cobra.Command) {
|
||||
validatorInfoBindings()
|
||||
case "validator/keycheck":
|
||||
validatorKeycheckBindings()
|
||||
case "validator/summary":
|
||||
validatorSummaryBindings()
|
||||
case "validator/yield":
|
||||
validatorYieldBindings()
|
||||
case "validator/expectation":
|
||||
@@ -169,10 +175,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)
|
||||
@@ -222,7 +244,7 @@ func init() {
|
||||
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RootCmd.PersistentFlags().String("connection", "", "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)
|
||||
}
|
||||
@@ -320,7 +342,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 {
|
||||
@@ -375,24 +402,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)
|
||||
|
||||
@@ -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,6 +24,7 @@ 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
|
||||
@@ -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"
|
||||
@@ -43,6 +40,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 +61,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 +86,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)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -30,3 +30,6 @@ func init() {
|
||||
|
||||
func validatorFlags(cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
func validatorBindings() {
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ type command struct {
|
||||
debug bool
|
||||
|
||||
// Input.
|
||||
account string
|
||||
index string
|
||||
pubKey string
|
||||
validator string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
@@ -43,7 +41,7 @@ type command struct {
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
|
||||
// Output.
|
||||
validator *apiv1.Validator
|
||||
validatorInfo *apiv1.Validator
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context) (*command, error) {
|
||||
@@ -59,31 +57,13 @@ 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")
|
||||
|
||||
c.account = viper.GetString("account")
|
||||
c.index = viper.GetString("index")
|
||||
c.pubKey = viper.GetString("pubkey")
|
||||
nonNil := 0
|
||||
if c.account != "" {
|
||||
nonNil++
|
||||
}
|
||||
if c.index != "" {
|
||||
nonNil++
|
||||
}
|
||||
if c.pubKey != "" {
|
||||
nonNil++
|
||||
}
|
||||
if nonNil == 0 {
|
||||
return nil, errors.New("one of account, index or pubkey required")
|
||||
}
|
||||
if nonNil > 1 {
|
||||
return nil, errors.New("only one of account, index and pubkey allowed")
|
||||
if viper.GetString("validator") == "" {
|
||||
return nil, errors.New("validator is required")
|
||||
}
|
||||
c.validator = viper.GetString("validator")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -37,14 +37,6 @@ func TestInput(t *testing.T) {
|
||||
vars: map[string]interface{}{},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"index": "1",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
{
|
||||
name: "NoValidatorInfo",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
ethutil "github.com/wealdtech/go-eth2-util"
|
||||
)
|
||||
|
||||
func (c *command) output(ctx context.Context) (string, error) {
|
||||
@@ -26,19 +28,38 @@ func (c *command) output(ctx context.Context) (string, error) {
|
||||
|
||||
builder := strings.Builder{}
|
||||
|
||||
switch c.validator.Validator.WithdrawalCredentials[0] {
|
||||
switch c.validatorInfo.Validator.WithdrawalCredentials[0] {
|
||||
case 0:
|
||||
builder.WriteString("BLS credentials: ")
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials))
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validatorInfo.Validator.WithdrawalCredentials))
|
||||
case 1:
|
||||
builder.WriteString("Ethereum execution address: ")
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials[12:]))
|
||||
builder.WriteString(addressBytesToEIP55(c.validatorInfo.Validator.WithdrawalCredentials[12:]))
|
||||
if c.verbose {
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("Withdrawal credentials: ")
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validator.Validator.WithdrawalCredentials))
|
||||
builder.WriteString(fmt.Sprintf("%#x", c.validatorInfo.Validator.WithdrawalCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
|
||||
func addressBytesToEIP55(address []byte) string {
|
||||
bytes := []byte(fmt.Sprintf("%x", address))
|
||||
hash := ethutil.Keccak256(bytes)
|
||||
for i := 0; i < len(bytes); i++ {
|
||||
hashByte := hash[i/2]
|
||||
if i%2 == 0 {
|
||||
hashByte >>= 4
|
||||
} else {
|
||||
hashByte &= 0xf
|
||||
}
|
||||
if bytes[i] > '9' && hashByte > 7 {
|
||||
bytes[i] -= 32
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("0x%s", string(bytes))
|
||||
}
|
||||
|
||||
@@ -15,14 +15,10 @@ package validatorcredentialsget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
@@ -68,71 +64,10 @@ func (c *command) setup(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *command) fetchValidator(ctx context.Context) error {
|
||||
if c.account != "" {
|
||||
_, account, err := util.WalletAndAccountFromInput(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to obtain account")
|
||||
}
|
||||
|
||||
accPubKey, err := util.BestPublicKey(account)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to obtain public key for account")
|
||||
}
|
||||
pubKey := phase0.BLSPubKey{}
|
||||
copy(pubKey[:], accPubKey.Marshal())
|
||||
validators, err := c.validatorsProvider.ValidatorsByPubKey(ctx,
|
||||
"head",
|
||||
[]phase0.BLSPubKey{pubKey},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return errors.New("unknown validator")
|
||||
}
|
||||
for _, validator := range validators {
|
||||
c.validator = validator
|
||||
}
|
||||
}
|
||||
if c.index != "" {
|
||||
tmp, err := strconv.ParseUint(c.index, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid validator index")
|
||||
}
|
||||
index := phase0.ValidatorIndex(tmp)
|
||||
validators, err := c.validatorsProvider.Validators(ctx,
|
||||
"head",
|
||||
[]phase0.ValidatorIndex{index},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
if _, exists := validators[index]; !exists {
|
||||
return errors.New("unknown validator")
|
||||
}
|
||||
c.validator = validators[index]
|
||||
}
|
||||
if c.pubKey != "" {
|
||||
bytes, err := hex.DecodeString(strings.TrimPrefix(c.pubKey, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid validator public key")
|
||||
}
|
||||
pubKey := phase0.BLSPubKey{}
|
||||
copy(pubKey[:], bytes)
|
||||
|
||||
validators, err := c.validatorsProvider.ValidatorsByPubKey(ctx,
|
||||
"head",
|
||||
[]phase0.BLSPubKey{pubKey},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return errors.New("unknown validator")
|
||||
}
|
||||
for _, validator := range validators {
|
||||
c.validator = validator
|
||||
}
|
||||
var err error
|
||||
c.validatorInfo, err = util.ParseValidator(ctx, c.validatorsProvider, c.validator, "head")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
105
cmd/validator/credentials/set/chaininfo.go
Normal file
105
cmd/validator/credentials/set/chaininfo.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright © 2022, 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 validatorcredentialsset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
)
|
||||
|
||||
// obtainChainInfo obtains the chain information required to create a withdrawal credentials change operation.
|
||||
func (c *command) obtainChainInfo(ctx context.Context) error {
|
||||
// Use the offline preparation file if present (and we haven't been asked to recreate it).
|
||||
if !c.prepareOffline {
|
||||
err := c.obtainChainInfoFromFile(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if c.offline {
|
||||
return fmt.Errorf("%s is unavailable or outdated; this is required to have been previously generated using --offline-preparation on an online machine and be readable in the directory in which this command is being run", offlinePreparationFilename)
|
||||
}
|
||||
|
||||
if err := c.obtainChainInfoFromNode(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// obtainChainInfoFromFile obtains chain information from a pre-generated file.
|
||||
func (c *command) obtainChainInfoFromFile(_ context.Context) error {
|
||||
_, err := os.Stat(offlinePreparationFilename)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading chain state\n", offlinePreparationFilename)
|
||||
}
|
||||
data, err := os.ReadFile(offlinePreparationFilename)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "failed to load chain state: %v\n", err)
|
||||
}
|
||||
return errors.Wrap(err, "failed to read offline preparation file")
|
||||
}
|
||||
c.chainInfo = &beacon.ChainInfo{}
|
||||
if err := json.Unmarshal(data, c.chainInfo); err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "chain state invalid: %v\n", err)
|
||||
}
|
||||
return errors.Wrap(err, "failed to parse offline preparation file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// obtainChainInfoFromNode obtains chain info from a beacon node.
|
||||
func (c *command) obtainChainInfoFromNode(ctx context.Context) error {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Populating chain info from beacon node\n")
|
||||
}
|
||||
|
||||
var err error
|
||||
c.chainInfo, err = beacon.ObtainChainInfoFromNode(ctx, c.consensusClient, c.chainTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeChainInfoToFile prepares for an offline run of this command by dumping
|
||||
// the chain information to a file.
|
||||
func (c *command) writeChainInfoToFile(_ context.Context) error {
|
||||
data, err := json.Marshal(c.chainInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(offlinePreparationFilename, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
111
cmd/validator/credentials/set/command.go
Normal file
111
cmd/validator/credentials/set/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 validatorcredentialsset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
capella "github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
offline bool
|
||||
json bool
|
||||
|
||||
// Input.
|
||||
account string
|
||||
withdrawalAccount string
|
||||
passphrases []string
|
||||
mnemonic string
|
||||
path string
|
||||
privateKey string
|
||||
validator string
|
||||
withdrawalAddressStr string
|
||||
forkVersion string
|
||||
genesisValidatorsRoot string
|
||||
prepareOffline bool
|
||||
signedOperationsInput string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Information required to generate the operations.
|
||||
withdrawalAddress bellatrix.ExecutionAddress
|
||||
chainInfo *beacon.ChainInfo
|
||||
domain phase0.Domain
|
||||
|
||||
// Processing.
|
||||
consensusClient consensusclient.Service
|
||||
chainTime chaintime.Service
|
||||
|
||||
// Output.
|
||||
signedOperations []*capella.SignedBLSToExecutionChange
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
offline: viper.GetBool("offline"),
|
||||
json: viper.GetBool("json"),
|
||||
timeout: viper.GetDuration("timeout"),
|
||||
connection: viper.GetString("connection"),
|
||||
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
|
||||
prepareOffline: viper.GetBool("prepare-offline"),
|
||||
account: viper.GetString("account"),
|
||||
withdrawalAccount: viper.GetString("withdrawal-account"),
|
||||
passphrases: util.GetPassphrases(),
|
||||
mnemonic: viper.GetString("mnemonic"),
|
||||
path: viper.GetString("path"),
|
||||
privateKey: viper.GetString("private-key"),
|
||||
signedOperationsInput: viper.GetString("signed-operations"),
|
||||
|
||||
validator: viper.GetString("validator"),
|
||||
withdrawalAddressStr: viper.GetString("withdrawal-address"),
|
||||
forkVersion: viper.GetString("fork-version"),
|
||||
genesisValidatorsRoot: viper.GetString("genesis-validators-root"),
|
||||
}
|
||||
|
||||
// Timeout is required.
|
||||
if c.timeout == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
|
||||
// We are generating information for offline use, we don't need any information
|
||||
// related to the accounts or signing.
|
||||
if c.prepareOffline {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if c.withdrawalAccount != "" && len(c.passphrases) == 0 {
|
||||
return nil, errors.New("passphrase required with withdrawal-account")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
83
cmd/validator/credentials/set/command_internal_test.go
Normal file
83
cmd/validator/credentials/set/command_internal_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 validatorcredentialsset
|
||||
|
||||
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: "NoValidatorInfo",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
},
|
||||
err: "one of account, index or pubkey required",
|
||||
},
|
||||
{
|
||||
name: "MultipleValidatorInfo",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"index": "1",
|
||||
"pubkey": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
},
|
||||
err: "only one of account, index and pubkey allowed",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"index": "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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/validator/credentials/set/output.go
Normal file
50
cmd/validator/credentials/set/output.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 validatorcredentialsset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//nolint:unparam
|
||||
func (c *command) output(_ context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.prepareOffline {
|
||||
return fmt.Sprintf("%s generated", offlinePreparationFilename), nil
|
||||
}
|
||||
|
||||
if c.json || c.offline {
|
||||
data, err := json.Marshal(c.signedOperations)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal signed operations")
|
||||
}
|
||||
if c.json {
|
||||
return string(data), nil
|
||||
}
|
||||
if err := os.WriteFile(changeOperationsFilename, data, 0600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", changeOperationsFilename))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
740
cmd/validator/credentials/set/process.go
Normal file
740
cmd/validator/credentials/set/process.go
Normal file
@@ -0,0 +1,740 @@
|
||||
// Copyright © 2022, 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 validatorcredentialsset
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
consensusclient "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
capella "github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/go-ssz"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/signing"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
ethutil "github.com/wealdtech/go-eth2-util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
// validatorPath is the regular expression that matches a validator path.
|
||||
var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
|
||||
|
||||
var offlinePreparationFilename = "offline-preparation.json"
|
||||
var changeOperationsFilename = "change-operations.json"
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.obtainChainInfo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.prepareOffline {
|
||||
return c.writeChainInfoToFile(ctx)
|
||||
}
|
||||
|
||||
if err := c.generateDomain(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.obtainOperations(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if validated, reason := c.validateOperations(ctx); !validated {
|
||||
return fmt.Errorf("operation failed validation: %s", reason)
|
||||
}
|
||||
|
||||
if c.json || c.offline {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Not broadcasting credentials change operations\n")
|
||||
}
|
||||
// Want JSON output, or cannot broadcast.
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.broadcastOperations(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperations(ctx context.Context) error {
|
||||
if c.account == "" && c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
|
||||
// No input information; fetch the operations from a file.
|
||||
err := c.obtainOperationsFromFileOrInput(ctx)
|
||||
if err == nil {
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
if c.signedOperationsInput != "" {
|
||||
return errors.Wrap(err, "failed to obtain supplied signed operations")
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", changeOperationsFilename))
|
||||
}
|
||||
|
||||
if c.mnemonic != "" {
|
||||
switch {
|
||||
case c.path != "":
|
||||
// Have a mnemonic and path.
|
||||
return c.generateOperationFromMnemonicAndPath(ctx)
|
||||
case c.validator != "":
|
||||
// Have a mnemonic and validator.
|
||||
return c.generateOperationFromMnemonicAndValidator(ctx)
|
||||
case c.privateKey != "":
|
||||
// Have a mnemonic and a private key for the withdrawal address.
|
||||
return c.generateOperationsFromMnemonicAndPrivateKey(ctx)
|
||||
default:
|
||||
// Have a mnemonic and nothing else; scan.
|
||||
return c.generateOperationsFromMnemonic(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if c.account != "" {
|
||||
switch {
|
||||
case c.withdrawalAccount != "":
|
||||
// Have an account and a withdrawal account.
|
||||
return c.generateOperationsFromAccountAndWithdrawalAccount(ctx)
|
||||
case c.privateKey != "":
|
||||
// Have an account and a private key for the withdrawal address.
|
||||
return c.generateOperationsFromAccountAndPrivateKey(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if c.validator != "" && c.privateKey != "" {
|
||||
// Have a validator and a private key for the withdrawal address.
|
||||
return c.generateOperationsFromValidatorAndPrivateKey(ctx)
|
||||
}
|
||||
|
||||
return errors.New("unsupported combination of inputs; see help for details of supported combinations")
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) error {
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[string]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||
}
|
||||
|
||||
validatorKeyPath := c.path
|
||||
match := validatorPath.Match([]byte(c.path))
|
||||
if !match {
|
||||
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
|
||||
}
|
||||
|
||||
if _, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath); err != nil {
|
||||
return errors.Wrap(err, "failed to generate operation from seed and path")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context) error {
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Scan the keys from the seed to find the path.
|
||||
maxDistance := 1024
|
||||
// Start scanning the validator keys.
|
||||
var withdrawalAccount e2wtypes.Account
|
||||
for i := 0; ; i++ {
|
||||
if i == maxDistance {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
|
||||
}
|
||||
break
|
||||
}
|
||||
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
|
||||
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate validator private key")
|
||||
}
|
||||
validatorPubkey := validatorPrivkey.PublicKey().Marshal()
|
||||
if bytes.Equal(validatorPubkey, validatorInfo.Pubkey[:]) {
|
||||
withdrawalKeyPath := strings.TrimSuffix(validatorKeyPath, "/0")
|
||||
withdrawalAccount, err = util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create withdrawal account")
|
||||
}
|
||||
|
||||
err = c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationsFromMnemonicAndPrivateKey(ctx context.Context) error {
|
||||
// Functionally identical to a simple scan, so use that.
|
||||
return c.generateOperationsFromMnemonic(ctx)
|
||||
}
|
||||
|
||||
func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[string]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||
}
|
||||
|
||||
maxDistance := 1024
|
||||
// Start scanning the validator keys.
|
||||
lastFoundIndex := 0
|
||||
for i := 0; ; i++ {
|
||||
if i-lastFoundIndex > maxDistance {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Gone %d indices without finding a validator, not scanning any further\n", maxDistance)
|
||||
}
|
||||
break
|
||||
}
|
||||
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
|
||||
|
||||
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate operation from seed and path")
|
||||
}
|
||||
if found {
|
||||
lastFoundIndex = i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationsFromAccountAndWithdrawalAccount(ctx context.Context) error {
|
||||
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
withdrawalAccount, err := util.ParseAccount(ctx, c.withdrawalAccount, c.passphrases, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatorPubkey, err := util.BestPublicKey(validatorAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator info")
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationsFromAccountAndPrivateKey(ctx context.Context) error {
|
||||
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatorPubkey, err := util.BestPublicKey(validatorAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain validator info")
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationsFromValidatorAndPrivateKey(ctx context.Context) error {
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationsFromFileOrInput(ctx context.Context) error {
|
||||
// Start off by attempting to use the provided signed operations.
|
||||
if c.signedOperationsInput != "" {
|
||||
return c.obtainOperationsFromInput(ctx)
|
||||
}
|
||||
// If not, read it from the file with the standard name.
|
||||
return c.obtainOperationsFromFile(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationsFromFile(ctx context.Context) error {
|
||||
_, err := os.Stat(changeOperationsFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read change operations file")
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading operations\n", changeOperationsFilename)
|
||||
}
|
||||
data, err := os.ReadFile(changeOperationsFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read change operations file")
|
||||
}
|
||||
if err := json.Unmarshal(data, &c.signedOperations); err != nil {
|
||||
return errors.Wrap(err, "failed to parse change operations file")
|
||||
}
|
||||
|
||||
for _, op := range c.signedOperations {
|
||||
if err := c.verifyOperation(ctx, op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationsFromInput(ctx context.Context) error {
|
||||
if strings.HasPrefix(c.signedOperationsInput, "{") {
|
||||
// This looks like a single entry; turn it in to an array.
|
||||
c.signedOperationsInput = fmt.Sprintf("[%s]", c.signedOperationsInput)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(c.signedOperationsInput, "[") {
|
||||
// This looks like a file; read it in.
|
||||
data, err := os.ReadFile(c.signedOperationsInput)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read input file")
|
||||
}
|
||||
c.signedOperationsInput = string(data)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(c.signedOperationsInput), &c.signedOperations); err != nil {
|
||||
return errors.Wrap(err, "failed to parse change operations input")
|
||||
}
|
||||
|
||||
for _, op := range c.signedOperations {
|
||||
if err := c.verifyOperation(ctx, op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
|
||||
validators map[string]*beacon.ValidatorInfo,
|
||||
seed []byte,
|
||||
path string,
|
||||
) (
|
||||
bool,
|
||||
error,
|
||||
) {
|
||||
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, path)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate validator private key")
|
||||
}
|
||||
validatorPubkey := fmt.Sprintf("%#x", validatorPrivkey.PublicKey().Marshal())
|
||||
validator, exists := validators[validatorPubkey]
|
||||
if !exists {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "No validator found with public key %s at path %s\n", validatorPubkey, path)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
fmt.Fprintf(os.Stderr, "Validator %d found with public key %s at path %s\n", validator.Index, validatorPubkey, path)
|
||||
}
|
||||
|
||||
if validator.WithdrawalCredentials[0] != byte(0) {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Validator %s has non-BLS withdrawal credentials %#x\n", validatorPubkey, validator.WithdrawalCredentials)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var withdrawalPubkey []byte
|
||||
var withdrawalAccount e2wtypes.Account
|
||||
if c.privateKey == "" {
|
||||
// Recreate the withdrawal credentials to ensure a match.
|
||||
withdrawalKeyPath := strings.TrimSuffix(path, "/0")
|
||||
withdrawalPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, withdrawalKeyPath)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate withdrawal private key")
|
||||
}
|
||||
withdrawalPubkey = withdrawalPrivkey.PublicKey().Marshal()
|
||||
withdrawalAccount, err = util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to create withdrawal account")
|
||||
}
|
||||
|
||||
} else {
|
||||
// Need the withdrawal credentials from the private key.
|
||||
withdrawalAccount, err = util.ParseAccount(ctx, c.privateKey, nil, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
withdrawalPubkey = withdrawalAccount.PublicKey().Marshal()
|
||||
}
|
||||
withdrawalCredentials := ethutil.SHA256(withdrawalPubkey)
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
if !bytes.Equal(withdrawalCredentials, validator.WithdrawalCredentials) {
|
||||
if c.verbose && c.privateKey == "" {
|
||||
fmt.Fprintf(os.Stderr, "Validator %s withdrawal credentials %#x do not match expected credentials, cannot update\n", validatorPubkey, validator.WithdrawalCredentials)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Validator %s eligible for setting credentials\n", validatorPubkey)
|
||||
}
|
||||
|
||||
err = c.generateOperationFromAccount(ctx, validator, withdrawalAccount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromAccount(ctx context.Context,
|
||||
validator *beacon.ValidatorInfo,
|
||||
withdrawalAccount e2wtypes.Account,
|
||||
) error {
|
||||
signedOperation, err := c.createSignedOperation(ctx, validator, withdrawalAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.signedOperations = append(c.signedOperations, signedOperation)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) createSignedOperation(ctx context.Context,
|
||||
validator *beacon.ValidatorInfo,
|
||||
withdrawalAccount e2wtypes.Account,
|
||||
) (
|
||||
*capella.SignedBLSToExecutionChange,
|
||||
error,
|
||||
) {
|
||||
pubkey, err := util.BestPublicKey(withdrawalAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using %#x as best public key for %s\n", pubkey.Marshal(), withdrawalAccount.Name())
|
||||
}
|
||||
blsPubkey := phase0.BLSPubKey{}
|
||||
copy(blsPubkey[:], pubkey.Marshal())
|
||||
|
||||
if err := c.parseWithdrawalAddress(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid withdrawal address")
|
||||
}
|
||||
|
||||
operation := &capella.BLSToExecutionChange{
|
||||
ValidatorIndex: validator.Index,
|
||||
FromBLSPubkey: blsPubkey,
|
||||
ToExecutionAddress: c.withdrawalAddress,
|
||||
}
|
||||
root, err := operation.HashTreeRoot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate root for credentials change operation")
|
||||
}
|
||||
|
||||
// Sign the operation.
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Signing %#x with domain %#x by public key %#x\n", root, c.domain, withdrawalAccount.PublicKey().Marshal())
|
||||
}
|
||||
signature, err := signing.SignRoot(ctx, withdrawalAccount, nil, root, c.domain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to sign credentials change operation")
|
||||
}
|
||||
|
||||
return &capella.SignedBLSToExecutionChange{
|
||||
Message: operation,
|
||||
Signature: signature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *command) parseWithdrawalAddress(_ context.Context) error {
|
||||
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(c.withdrawalAddressStr, "0x"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain execution address")
|
||||
}
|
||||
if len(withdrawalAddressBytes) != bellatrix.ExecutionAddressLength {
|
||||
return errors.New("withdrawal address must be exactly 20 bytes in length")
|
||||
}
|
||||
// Ensure the address is properly checksummed.
|
||||
checksummedAddress := addressBytesToEIP55(withdrawalAddressBytes)
|
||||
if checksummedAddress != c.withdrawalAddressStr {
|
||||
return fmt.Errorf("withdrawal address checksum does not match (expected %s)", checksummedAddress)
|
||||
}
|
||||
copy(c.withdrawalAddress[:], withdrawalAddressBytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) validateOperations(ctx context.Context) (bool, string) {
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[phase0.ValidatorIndex]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[validator.Index] = validator
|
||||
}
|
||||
|
||||
for _, signedOperation := range c.signedOperations {
|
||||
if validated, reason := c.validateOperation(ctx, validators, signedOperation); !validated {
|
||||
return validated, reason
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (c *command) verifyOperation(ctx context.Context, op *capella.SignedBLSToExecutionChange) error {
|
||||
root, err := op.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate message root")
|
||||
}
|
||||
|
||||
sigBytes := make([]byte, len(op.Signature))
|
||||
copy(sigBytes, op.Signature[:])
|
||||
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid signature")
|
||||
}
|
||||
|
||||
container := &phase0.SigningData{
|
||||
ObjectRoot: root,
|
||||
Domain: c.domain,
|
||||
}
|
||||
signingRoot, err := ssz.HashTreeRoot(container)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate signing root")
|
||||
}
|
||||
|
||||
pubkeyBytes := make([]byte, len(op.Message.FromBLSPubkey))
|
||||
copy(pubkeyBytes, op.Message.FromBLSPubkey[:])
|
||||
pubkey, err := e2types.BLSPublicKeyFromBytes(pubkeyBytes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid public key")
|
||||
}
|
||||
if !sig.Verify(signingRoot[:], pubkey) {
|
||||
return errors.New("signature does not verify")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) validateOperation(_ context.Context,
|
||||
validators map[phase0.ValidatorIndex]*beacon.ValidatorInfo,
|
||||
signedOperation *capella.SignedBLSToExecutionChange,
|
||||
) (
|
||||
bool,
|
||||
string,
|
||||
) {
|
||||
validator, exists := validators[signedOperation.Message.ValidatorIndex]
|
||||
if !exists {
|
||||
return false, "validator not known on chain"
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Credentials change operation: %v", signedOperation)
|
||||
fmt.Fprintf(os.Stderr, "On-chain validator info: %v\n", validator)
|
||||
}
|
||||
|
||||
if validator.WithdrawalCredentials[0] != byte(0) {
|
||||
return false, "validator is not using BLS withdrawal credentials"
|
||||
}
|
||||
|
||||
withdrawalCredentials := ethutil.SHA256(signedOperation.Message.FromBLSPubkey[:])
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
if !bytes.Equal(withdrawalCredentials, validator.WithdrawalCredentials) {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "validator withdrawal credentials %#x do not match calculated operation withdrawal credentials %#x\n", validator.WithdrawalCredentials, withdrawalCredentials)
|
||||
}
|
||||
return false, "validator withdrawal credentials do not match those in the operation"
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (c *command) broadcastOperations(ctx context.Context) error {
|
||||
return c.consensusClient.(consensusclient.BLSToExecutionChangesSubmitter).SubmitBLSToExecutionChanges(ctx, c.signedOperations)
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
if c.offline {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to the consensus node.
|
||||
var err error
|
||||
c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to consensus node")
|
||||
}
|
||||
|
||||
// Set up chaintime.
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
|
||||
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create chaintime service")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateDomain(ctx context.Context) error {
|
||||
genesisValidatorsRoot, err := c.obtainGenesisValidatorsRoot(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forkVersion, err := c.obtainForkVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err := (&phase0.ForkData{
|
||||
CurrentVersion: forkVersion,
|
||||
GenesisValidatorsRoot: genesisValidatorsRoot,
|
||||
}).HashTreeRoot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to calculate signature domain")
|
||||
}
|
||||
|
||||
copy(c.domain[:], c.chainInfo.BLSToExecutionChangeDomainType[:])
|
||||
copy(c.domain[4:], root[:])
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Domain is %#x\n", c.domain)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) obtainGenesisValidatorsRoot(ctx context.Context) (phase0.Root, error) {
|
||||
genesisValidatorsRoot := phase0.Root{}
|
||||
|
||||
if c.genesisValidatorsRoot != "" {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Genesis validators root supplied on the command line\n")
|
||||
}
|
||||
root, err := hex.DecodeString(strings.TrimPrefix(c.genesisValidatorsRoot, "0x"))
|
||||
if err != nil {
|
||||
return phase0.Root{}, errors.Wrap(err, "invalid genesis validators root supplied")
|
||||
}
|
||||
if len(root) != phase0.RootLength {
|
||||
return phase0.Root{}, errors.New("invalid length for genesis validators root")
|
||||
}
|
||||
copy(genesisValidatorsRoot[:], root)
|
||||
} else {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Genesis validators root obtained from chain info\n")
|
||||
}
|
||||
copy(genesisValidatorsRoot[:], c.chainInfo.GenesisValidatorsRoot[:])
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using genesis validators root %#x\n", genesisValidatorsRoot)
|
||||
}
|
||||
return genesisValidatorsRoot, nil
|
||||
}
|
||||
|
||||
func (c *command) obtainForkVersion(ctx context.Context) (phase0.Version, error) {
|
||||
forkVersion := phase0.Version{}
|
||||
|
||||
if c.forkVersion != "" {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Fork version supplied on the command line\n")
|
||||
}
|
||||
version, err := hex.DecodeString(strings.TrimPrefix(c.forkVersion, "0x"))
|
||||
if err != nil {
|
||||
return phase0.Version{}, errors.Wrap(err, "invalid fork version supplied")
|
||||
}
|
||||
if len(version) != phase0.ForkVersionLength {
|
||||
return phase0.Version{}, errors.New("invalid length for fork version")
|
||||
}
|
||||
copy(forkVersion[:], version)
|
||||
} else {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Fork version obtained from chain info\n")
|
||||
}
|
||||
// Use the genesis fork version for setting credentials as per the spec.
|
||||
copy(forkVersion[:], c.chainInfo.GenesisForkVersion[:])
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using fork version %#x\n", forkVersion)
|
||||
}
|
||||
return forkVersion, nil
|
||||
}
|
||||
|
||||
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
|
||||
func addressBytesToEIP55(address []byte) string {
|
||||
bytes := []byte(fmt.Sprintf("%x", address))
|
||||
hash := ethutil.Keccak256(bytes)
|
||||
for i := 0; i < len(bytes); i++ {
|
||||
hashByte := hash[i/2]
|
||||
if i%2 == 0 {
|
||||
hashByte >>= 4
|
||||
} else {
|
||||
hashByte &= 0xf
|
||||
}
|
||||
if bytes[i] > '9' && hashByte > 7 {
|
||||
bytes[i] -= 32
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("0x%s", string(bytes))
|
||||
}
|
||||
372
cmd/validator/credentials/set/process_internal_test.go
Normal file
372
cmd/validator/credentials/set/process_internal_test.go
Normal file
@@ -0,0 +1,372 @@
|
||||
// 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 validatorcredentialsset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||
capella "github.com/attestantio/go-eth2-client/spec/capella"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
)
|
||||
|
||||
func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command *command
|
||||
expected []*capella.SignedBLSToExecutionChange
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "MnemonicInvalid",
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
|
||||
path: "m/12381/3600/0/0/0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
},
|
||||
{
|
||||
name: "PathInvalid",
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
err: "path m/12381/3600/0/0 does not match EIP-2334 format for a validator",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0/0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
expected: []*capella.SignedBLSToExecutionChange{
|
||||
{
|
||||
Message: &capella.BLSToExecutionChange{
|
||||
ValidatorIndex: 0,
|
||||
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
|
||||
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.generateOperationFromMnemonicAndPath(ctx)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
// fmt.Printf("%v\n", test.command.signedOperations)
|
||||
require.Equal(t, test.expected, test.command.signedOperations)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command *command
|
||||
expected []*capella.SignedBLSToExecutionChange
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "MnemonicInvalid",
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
|
||||
validator: "0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
},
|
||||
{
|
||||
name: "ValidatorMissing",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
err: "no validator specified",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
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",
|
||||
validator: "0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
expected: []*capella.SignedBLSToExecutionChange{
|
||||
{
|
||||
Message: &capella.BLSToExecutionChange{
|
||||
ValidatorIndex: 0,
|
||||
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
|
||||
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GoodPubkey",
|
||||
command: &command{
|
||||
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",
|
||||
validator: "0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c87",
|
||||
chainInfo: chainInfo,
|
||||
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
expected: []*capella.SignedBLSToExecutionChange{
|
||||
{
|
||||
Message: &capella.BLSToExecutionChange{
|
||||
ValidatorIndex: 0,
|
||||
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
|
||||
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.generateOperationFromMnemonicAndValidator(ctx)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperations)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOperationFromSeedAndPath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 2,
|
||||
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
|
||||
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 3,
|
||||
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
|
||||
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
validators := make(map[string]*beacon.ValidatorInfo, len(chainInfo.Validators))
|
||||
for i := range chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", chainInfo.Validators[i].Pubkey)] = chainInfo.Validators[i]
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command *command
|
||||
seed []byte
|
||||
path string
|
||||
generated bool
|
||||
err string
|
||||
expected []*capella.SignedBLSToExecutionChange
|
||||
}{
|
||||
{
|
||||
name: "PathInvalid",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "invalid",
|
||||
err: "failed to generate validator private key: not master at path component 0",
|
||||
},
|
||||
{
|
||||
name: "ValidatorUnknown",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/999/0/0",
|
||||
},
|
||||
{
|
||||
name: "ValidatorCredentialsAlreadySet",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/2/0/0",
|
||||
},
|
||||
{
|
||||
name: "PrivateKeyInvalid",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
privateKey: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/0/0/0",
|
||||
err: "failed to create account from private key: invalid private key: err blsSecretKeyDeserialize ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/0/0/0",
|
||||
generated: true,
|
||||
expected: []*capella.SignedBLSToExecutionChange{
|
||||
{
|
||||
Message: &capella.BLSToExecutionChange{
|
||||
ValidatorIndex: 0,
|
||||
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
|
||||
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GoodPrivateKey",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
|
||||
privateKey: "0x67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/3/0/0",
|
||||
generated: true,
|
||||
expected: []*capella.SignedBLSToExecutionChange{
|
||||
{
|
||||
Message: &capella.BLSToExecutionChange{
|
||||
ValidatorIndex: 3,
|
||||
FromBLSPubkey: phase0.BLSPubKey{0x86, 0x71, 0x0a, 0xbb, 0x44, 0xb6, 0xcd, 0xa6, 0x66, 0x57, 0x7b, 0xbb, 0x25, 0x5e, 0x16, 0xd9, 0x8b, 0xf2, 0x52, 0x51, 0x76, 0x22, 0x3f, 0x35, 0x35, 0xc7, 0xdf, 0xf8, 0xe7, 0x0b, 0x3b, 0xc8, 0x92, 0xbb, 0x36, 0x11, 0x33, 0x95, 0x2b, 0x03, 0xd2, 0xb0, 0x78, 0xcd, 0x07, 0x18, 0xca, 0xf3},
|
||||
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x8d, 0x92, 0xb9, 0x1c, 0x5d, 0xfd, 0x98, 0xc7, 0x98, 0xfc, 0x94, 0xe1, 0xe6, 0x69, 0xf3, 0xaa, 0xae, 0x72, 0xb2, 0x36, 0x47, 0xde, 0x88, 0x54, 0xea, 0x16, 0x74, 0x7f, 0xfe, 0xf0, 0x4d, 0x46, 0x5c, 0x07, 0x56, 0x34, 0x03, 0x30, 0x2f, 0xbc, 0x26, 0xa2, 0x6d, 0xec, 0x10, 0x20, 0xe7, 0x67, 0x10, 0xb0, 0x4a, 0x7e, 0x4e, 0x25, 0x89, 0x7e, 0x87, 0x88, 0xda, 0xaf, 0x2b, 0xb5, 0xb7, 0x73, 0x25, 0x64, 0x80, 0xc1, 0xba, 0xf3, 0x1d, 0x33, 0x8f, 0x17, 0xa5, 0x35, 0x74, 0x80, 0xf3, 0x37, 0x0e, 0xea, 0x19, 0x15, 0xd5, 0x69, 0x7e, 0xf6, 0x68, 0xaa, 0x9c, 0x3d, 0x47, 0x19, 0x75, 0xfc},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
generated, err := test.command.generateOperationFromSeedAndPath(ctx, validators, test.seed, test.path)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.generated, generated)
|
||||
if generated {
|
||||
require.Equal(t, test.expected, test.command.signedOperations)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/validator/credentials/set/run.go
Normal file
50
cmd/validator/credentials/set/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 validatorcredentialsset
|
||||
|
||||
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
|
||||
}
|
||||
@@ -49,9 +49,6 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
|
||||
// Ethereum 2 connection.
|
||||
data.eth2Client = viper.GetString("connection")
|
||||
if data.eth2Client == "" {
|
||||
return nil, errors.New("connection is required")
|
||||
}
|
||||
data.allowInsecure = viper.GetBool("allow-insecure-connections")
|
||||
|
||||
// Account.
|
||||
|
||||
@@ -71,14 +71,6 @@ func TestInput(t *testing.T) {
|
||||
},
|
||||
err: "account, pubkey or index required",
|
||||
},
|
||||
{
|
||||
name: "ConnectionMissing",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
|
||||
},
|
||||
err: "connection is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
105
cmd/validator/exit/chaininfo.go
Normal file
105
cmd/validator/exit/chaininfo.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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 validatorexit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
)
|
||||
|
||||
// obtainChainInfo obtains the chain information required to create an exit operation.
|
||||
func (c *command) obtainChainInfo(ctx context.Context) error {
|
||||
// Use the offline preparation file if present (and we haven't been asked to recreate it).
|
||||
if !c.prepareOffline {
|
||||
err := c.obtainChainInfoFromFile(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if c.offline {
|
||||
return fmt.Errorf("%s is unavailable or outdated; this is required to have been previously generated using --offline-preparation on an online machine and be readable in the directory in which this command is being run", offlinePreparationFilename)
|
||||
}
|
||||
|
||||
if err := c.obtainChainInfoFromNode(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// obtainChainInfoFromFile obtains chain information from a pre-generated file.
|
||||
func (c *command) obtainChainInfoFromFile(_ context.Context) error {
|
||||
_, err := os.Stat(offlinePreparationFilename)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading chain state\n", offlinePreparationFilename)
|
||||
}
|
||||
data, err := os.ReadFile(offlinePreparationFilename)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "failed to load chain state: %v\n", err)
|
||||
}
|
||||
return errors.Wrap(err, "failed to read offline preparation file")
|
||||
}
|
||||
c.chainInfo = &beacon.ChainInfo{}
|
||||
if err := json.Unmarshal(data, c.chainInfo); err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "chain state invalid: %v\n", err)
|
||||
}
|
||||
return errors.Wrap(err, "failed to parse offline preparation file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// obtainChainInfoFromNode obtains chain info from a beacon node.
|
||||
func (c *command) obtainChainInfoFromNode(ctx context.Context) error {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Populating chain info from beacon node\n")
|
||||
}
|
||||
|
||||
var err error
|
||||
c.chainInfo, err = beacon.ObtainChainInfoFromNode(ctx, c.consensusClient, c.chainTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeChainInfoToFile prepares for an offline run of this command by dumping
|
||||
// the chain information to a file.
|
||||
func (c *command) writeChainInfoToFile(_ context.Context) error {
|
||||
data, err := json.Marshal(c.chainInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(offlinePreparationFilename, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
97
cmd/validator/exit/command.go
Normal file
97
cmd/validator/exit/command.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 validatorexit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
consensusclient "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/beacon"
|
||||
"github.com/wealdtech/ethdo/services/chaintime"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
offline bool
|
||||
json bool
|
||||
|
||||
// Input.
|
||||
passphrases []string
|
||||
mnemonic string
|
||||
path string
|
||||
privateKey string
|
||||
validator string
|
||||
forkVersion string
|
||||
genesisValidatorsRoot string
|
||||
prepareOffline bool
|
||||
signedOperationInput string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
connection string
|
||||
allowInsecureConnections bool
|
||||
|
||||
// Information required to generate the operations.
|
||||
chainInfo *beacon.ChainInfo
|
||||
domain phase0.Domain
|
||||
|
||||
// Processing.
|
||||
consensusClient consensusclient.Service
|
||||
chainTime chaintime.Service
|
||||
|
||||
// Output.
|
||||
signedOperation *phase0.SignedVoluntaryExit
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
c := &command{
|
||||
quiet: viper.GetBool("quiet"),
|
||||
verbose: viper.GetBool("verbose"),
|
||||
debug: viper.GetBool("debug"),
|
||||
offline: viper.GetBool("offline"),
|
||||
json: viper.GetBool("json"),
|
||||
timeout: viper.GetDuration("timeout"),
|
||||
connection: viper.GetString("connection"),
|
||||
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
|
||||
prepareOffline: viper.GetBool("prepare-offline"),
|
||||
passphrases: util.GetPassphrases(),
|
||||
mnemonic: viper.GetString("mnemonic"),
|
||||
path: viper.GetString("path"),
|
||||
privateKey: viper.GetString("private-key"),
|
||||
signedOperationInput: viper.GetString("signed-operation"),
|
||||
validator: viper.GetString("validator"),
|
||||
forkVersion: viper.GetString("fork-version"),
|
||||
genesisValidatorsRoot: viper.GetString("genesis-validators-root"),
|
||||
}
|
||||
|
||||
// Timeout is required.
|
||||
if c.timeout == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
|
||||
// We are generating information for offline use, we don't need any information
|
||||
// related to the accounts or signing.
|
||||
if c.prepareOffline {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -1,146 +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 validatorexit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
type dataIn struct {
|
||||
// System.
|
||||
timeout time.Duration
|
||||
quiet bool
|
||||
verbose bool
|
||||
debug bool
|
||||
// Operation.
|
||||
eth2Client eth2client.Service
|
||||
jsonOutput bool
|
||||
// Chain information.
|
||||
fork *spec.Fork
|
||||
currentEpoch spec.Epoch
|
||||
// Exit information.
|
||||
account e2wtypes.Account
|
||||
passphrases []string
|
||||
epoch spec.Epoch
|
||||
domain spec.Domain
|
||||
signedVoluntaryExit *spec.SignedVoluntaryExit
|
||||
}
|
||||
|
||||
func input(ctx context.Context) (*dataIn, error) {
|
||||
data := &dataIn{}
|
||||
|
||||
if viper.GetDuration("timeout") == 0 {
|
||||
return nil, errors.New("timeout is required")
|
||||
}
|
||||
data.timeout = viper.GetDuration("timeout")
|
||||
data.quiet = viper.GetBool("quiet")
|
||||
data.verbose = viper.GetBool("verbose")
|
||||
data.debug = viper.GetBool("debug")
|
||||
data.passphrases = util.GetPassphrases()
|
||||
data.jsonOutput = viper.GetBool("json")
|
||||
|
||||
switch {
|
||||
case viper.GetString("exit") != "":
|
||||
return inputJSON(ctx, data)
|
||||
case viper.GetString("account") != "":
|
||||
return inputAccount(ctx, data)
|
||||
case viper.GetString("key") != "":
|
||||
return inputKey(ctx, data)
|
||||
default:
|
||||
return nil, errors.New("must supply account, key, or pre-constructed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func inputJSON(ctx context.Context, data *dataIn) (*dataIn, error) {
|
||||
validatorData := &util.ValidatorExitData{}
|
||||
err := json.Unmarshal([]byte(viper.GetString("exit")), validatorData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.signedVoluntaryExit = validatorData.Exit
|
||||
return inputChainData(ctx, data)
|
||||
}
|
||||
|
||||
func inputAccount(ctx context.Context, data *dataIn) (*dataIn, error) {
|
||||
var err error
|
||||
_, data.account, err = util.WalletAndAccountFromInput(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain acount")
|
||||
}
|
||||
return inputChainData(ctx, data)
|
||||
}
|
||||
|
||||
func inputKey(ctx context.Context, data *dataIn) (*dataIn, error) {
|
||||
privKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decode key")
|
||||
}
|
||||
data.account, err = util.NewScratchAccount(privKeyBytes, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create acount from key")
|
||||
}
|
||||
return inputChainData(ctx, data)
|
||||
}
|
||||
|
||||
func inputChainData(ctx context.Context, data *dataIn) (*dataIn, error) {
|
||||
var err error
|
||||
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
|
||||
}
|
||||
|
||||
// Current fork.
|
||||
data.fork, err = data.eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain fork information")
|
||||
}
|
||||
|
||||
// Calculate current epoch.
|
||||
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
|
||||
}
|
||||
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
|
||||
}
|
||||
data.currentEpoch = spec.Epoch(uint64(time.Since(genesis.GenesisTime).Seconds()) / (uint64(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()) * config["SLOTS_PER_EPOCH"].(uint64)))
|
||||
|
||||
// Epoch.
|
||||
if viper.GetInt64("epoch") == -1 {
|
||||
data.epoch = data.currentEpoch
|
||||
} else {
|
||||
data.epoch = spec.Epoch(viper.GetUint64("epoch"))
|
||||
}
|
||||
|
||||
// Domain.
|
||||
domain, err := data.eth2Client.(eth2client.DomainProvider).Domain(ctx, config["DOMAIN_VOLUNTARY_EXIT"].(spec.DomainType), data.epoch)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to calculate domain")
|
||||
}
|
||||
data.domain = domain
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -1,193 +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 validatorexit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestInput(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vars map[string]interface{}
|
||||
res *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutMissing",
|
||||
vars: map[string]interface{}{
|
||||
"account": "Test wallet",
|
||||
"wallet-passphrase": "ce%NohGhah4ye5ra",
|
||||
"type": "nd",
|
||||
},
|
||||
err: "timeout is required",
|
||||
},
|
||||
{
|
||||
name: "NoMethod",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
},
|
||||
err: "must supply account, key, or pre-constructed JSON",
|
||||
},
|
||||
{
|
||||
name: "KeyInvalid",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"key": "0xinvalid",
|
||||
},
|
||||
err: "failed to decode key: encoding/hex: invalid byte: U+0069 'i'",
|
||||
},
|
||||
{
|
||||
name: "KeyBad",
|
||||
vars: map[string]interface{}{
|
||||
"timeout": "5s",
|
||||
"key": "0x00",
|
||||
},
|
||||
err: "failed to create acount from key: private key must be 32 bytes",
|
||||
},
|
||||
{
|
||||
name: "KeyGood",
|
||||
vars: map[string]interface{}{
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"allow-insecure-connections": true,
|
||||
"timeout": "5s",
|
||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AccountUnknown",
|
||||
vars: map[string]interface{}{
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"allow-insecure-connections": true,
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/unknown",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
err: "failed to obtain acount: failed to obtain account: no account with name \"unknown\"",
|
||||
},
|
||||
{
|
||||
name: "AccountGood",
|
||||
vars: map[string]interface{}{
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"allow-insecure-connections": true,
|
||||
"timeout": "5s",
|
||||
"account": "Test wallet/Interop 0",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JSONInvalid",
|
||||
vars: map[string]interface{}{
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"allow-insecure-connections": true,
|
||||
"timeout": "5s",
|
||||
"exit": `invalid`,
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
err: "invalid character 'i' looking for beginning of value",
|
||||
},
|
||||
{
|
||||
name: "JSONGood",
|
||||
vars: map[string]interface{}{
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"allow-insecure-connections": true,
|
||||
"timeout": "5s",
|
||||
"exit": `{"exit":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x00002009"}`,
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ClientBad",
|
||||
vars: map[string]interface{}{
|
||||
"connection": "localhost:1",
|
||||
"allow-insecure-connections": true,
|
||||
"timeout": "5s",
|
||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
},
|
||||
err: "failed to connect to Ethereum 2 beacon node: 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: "EpochProvided",
|
||||
vars: map[string]interface{}{
|
||||
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
|
||||
"allow-insecure-connections": true,
|
||||
"timeout": "5s",
|
||||
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
|
||||
"epoch": "123",
|
||||
},
|
||||
res: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
for k, v := range test.vars {
|
||||
viper.Set(k, v)
|
||||
}
|
||||
res, err := input(context.Background())
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res.timeout, res.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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
|
||||
@@ -16,42 +16,35 @@ package validatorexit
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
)
|
||||
|
||||
type dataOut struct {
|
||||
jsonOutput bool
|
||||
forkVersion spec.Version
|
||||
signedVoluntaryExit *spec.SignedVoluntaryExit
|
||||
}
|
||||
|
||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
||||
if data == nil {
|
||||
return "", errors.New("no data")
|
||||
//nolint:unparam
|
||||
func (c *command) output(_ context.Context) (string, error) {
|
||||
if c.quiet {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if data.signedVoluntaryExit == nil {
|
||||
return "", errors.New("no signed voluntary exit")
|
||||
if c.prepareOffline {
|
||||
return fmt.Sprintf("%s generated", offlinePreparationFilename), nil
|
||||
}
|
||||
|
||||
if data.jsonOutput {
|
||||
return outputJSON(ctx, data)
|
||||
if c.json || c.offline {
|
||||
data, err := json.Marshal(c.signedOperation)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal signed operation")
|
||||
}
|
||||
if c.json {
|
||||
return string(data), nil
|
||||
}
|
||||
if err := os.WriteFile(exitOperationFilename, data, 0600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationFilename))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func outputJSON(ctx context.Context, data *dataOut) (string, error) {
|
||||
validatorExitData := &util.ValidatorExitData{
|
||||
Exit: data.signedVoluntaryExit,
|
||||
ForkVersion: data.forkVersion,
|
||||
}
|
||||
bytes, err := json.Marshal(validatorExitData)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to generate JSON")
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
@@ -1,97 +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 validatorexit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataOut *dataOut
|
||||
res string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "SignedVoluntaryExitNil",
|
||||
dataOut: &dataOut{
|
||||
jsonOutput: true,
|
||||
},
|
||||
err: "no signed voluntary exit",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataOut: &dataOut{
|
||||
forkVersion: spec.Version{0x01, 0x02, 0x03, 0x04},
|
||||
signedVoluntaryExit: &spec.SignedVoluntaryExit{
|
||||
Message: &spec.VoluntaryExit{
|
||||
Epoch: spec.Epoch(123),
|
||||
ValidatorIndex: spec.ValidatorIndex(456),
|
||||
},
|
||||
Signature: spec.BLSSignature{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
||||
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
|
||||
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
dataOut: &dataOut{
|
||||
jsonOutput: true,
|
||||
forkVersion: spec.Version{0x01, 0x02, 0x03, 0x04},
|
||||
signedVoluntaryExit: &spec.SignedVoluntaryExit{
|
||||
Message: &spec.VoluntaryExit{
|
||||
Epoch: spec.Epoch(123),
|
||||
ValidatorIndex: spec.ValidatorIndex(456),
|
||||
},
|
||||
Signature: spec.BLSSignature{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
||||
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
|
||||
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
|
||||
},
|
||||
},
|
||||
},
|
||||
res: `{"exit":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x01020304"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := output(context.Background(), test.dataOut)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.res, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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
|
||||
@@ -14,120 +14,526 @@
|
||||
package validatorexit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
consensusclient "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/prysmaticlabs/go-ssz"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||
"github.com/wealdtech/ethdo/signing"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
ethutil "github.com/wealdtech/go-eth2-util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
// maxFutureEpochs is the farthest in the future for which an exit will be created.
|
||||
var maxFutureEpochs = spec.Epoch(1024)
|
||||
// validatorPath is the regular expression that matches a validator path.
|
||||
var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
|
||||
|
||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
var offlinePreparationFilename = "offline-preparation.json"
|
||||
var exitOperationFilename = "exit-operation.json"
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
if err := c.setup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.epoch > data.currentEpoch {
|
||||
if data.epoch-data.currentEpoch > maxFutureEpochs {
|
||||
return nil, errors.New("not generating exit for an epoch in the far future")
|
||||
if err := c.obtainChainInfo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.prepareOffline {
|
||||
return c.writeChainInfoToFile(ctx)
|
||||
}
|
||||
|
||||
if err := c.generateDomain(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.obtainOperation(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if validated, reason := c.validateOperation(ctx); !validated {
|
||||
return fmt.Errorf("operation failed validation: %s", reason)
|
||||
}
|
||||
|
||||
if c.json || c.offline {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Not broadcasting credentials change operations\n")
|
||||
}
|
||||
// Want JSON output, or cannot broadcast.
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.broadcastOperation(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperation(ctx context.Context) error {
|
||||
if (c.mnemonic == "" || c.path == "") && c.privateKey == "" && c.validator == "" {
|
||||
// No input information; fetch the operation from a file.
|
||||
err := c.obtainOperationFromFileOrInput(ctx)
|
||||
if err == nil {
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
if c.signedOperationInput != "" {
|
||||
return errors.Wrap(err, "failed to obtain supplied signed operation")
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", exitOperationFilename))
|
||||
}
|
||||
|
||||
if c.mnemonic != "" {
|
||||
switch {
|
||||
case c.path != "":
|
||||
// Have a mnemonic and path.
|
||||
return c.generateOperationFromMnemonicAndPath(ctx)
|
||||
case c.validator != "":
|
||||
// Have a mnemonic and validator.
|
||||
return c.generateOperationFromMnemonicAndValidator(ctx)
|
||||
default:
|
||||
return errors.New("mnemonic must be supplied with either a path or validator")
|
||||
}
|
||||
}
|
||||
results := &dataOut{
|
||||
forkVersion: data.fork.CurrentVersion,
|
||||
jsonOutput: data.jsonOutput,
|
||||
|
||||
if c.privateKey != "" {
|
||||
return c.generateOperationFromPrivateKey(ctx)
|
||||
}
|
||||
|
||||
validator, err := fetchValidator(ctx, data)
|
||||
if c.validator != "" {
|
||||
return c.generateOperationFromValidator(ctx)
|
||||
}
|
||||
|
||||
return errors.New("unsupported combination of inputs; see help for details of supported combinations")
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) error {
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[string]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||
}
|
||||
|
||||
validatorKeyPath := c.path
|
||||
match := validatorPath.Match([]byte(c.path))
|
||||
if !match {
|
||||
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath); err != nil {
|
||||
return errors.Wrap(err, "failed to generate operation from seed and path")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context) error {
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Scan the keys from the seed to find the path.
|
||||
maxDistance := 1024
|
||||
// Start scanning the validator keys.
|
||||
for i := 0; ; i++ {
|
||||
if i == maxDistance {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
|
||||
}
|
||||
break
|
||||
}
|
||||
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
|
||||
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate validator private key")
|
||||
}
|
||||
validatorPubkey := validatorPrivkey.PublicKey().Marshal()
|
||||
if bytes.Equal(validatorPubkey, validatorInfo.Pubkey[:]) {
|
||||
validatorAccount, err := util.ParseAccount(ctx, c.mnemonic, []string{validatorKeyPath}, true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create withdrawal account")
|
||||
}
|
||||
|
||||
err = c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromPrivateKey(ctx context.Context) error {
|
||||
validatorAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create validator account")
|
||||
}
|
||||
|
||||
validatorPubkey, err := util.BestPublicKey(validatorAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.verbose {
|
||||
fmt.Fprintf(os.Stderr, "Validator %d found with public key %s\n", validatorInfo.Index, validatorPubkey)
|
||||
}
|
||||
|
||||
if err = c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromValidator(ctx context.Context) error {
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validatorAccount, err := util.ParseAccount(ctx, c.validator, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromFileOrInput(ctx context.Context) error {
|
||||
// Start off by attempting to use the provided signed operation.
|
||||
if c.signedOperationInput != "" {
|
||||
return c.obtainOperationFromInput(ctx)
|
||||
}
|
||||
// If not, read it from the file with the standard name.
|
||||
return c.obtainOperationFromFile(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromFile(ctx context.Context) error {
|
||||
_, err := os.Stat(exitOperationFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read exit operation file")
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading operation\n", exitOperationFilename)
|
||||
}
|
||||
data, err := os.ReadFile(exitOperationFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read exit operation file")
|
||||
}
|
||||
if err := json.Unmarshal(data, &c.signedOperation); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operation file")
|
||||
}
|
||||
|
||||
if err := c.verifySignedOperation(ctx, c.signedOperation); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromInput(ctx context.Context) error {
|
||||
if !strings.HasPrefix(c.signedOperationInput, "{") {
|
||||
// This looks like a file; read it in.
|
||||
data, err := os.ReadFile(c.signedOperationInput)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read input file")
|
||||
}
|
||||
c.signedOperationInput = string(data)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(c.signedOperationInput), &c.signedOperation); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operation input")
|
||||
}
|
||||
|
||||
if err := c.verifySignedOperation(ctx, c.signedOperation); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
|
||||
validators map[string]*beacon.ValidatorInfo,
|
||||
seed []byte,
|
||||
path string,
|
||||
) error {
|
||||
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate validator private key")
|
||||
}
|
||||
|
||||
c.privateKey = fmt.Sprintf("%#x", validatorPrivkey.Marshal())
|
||||
return c.generateOperationFromPrivateKey(ctx)
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromAccount(ctx context.Context,
|
||||
validator *beacon.ValidatorInfo,
|
||||
account e2wtypes.Account,
|
||||
epoch phase0.Epoch,
|
||||
) error {
|
||||
var err error
|
||||
c.signedOperation, err = c.createSignedOperation(ctx, validator, account, epoch)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *command) createSignedOperation(ctx context.Context,
|
||||
validator *beacon.ValidatorInfo,
|
||||
account e2wtypes.Account,
|
||||
epoch phase0.Epoch,
|
||||
) (
|
||||
*phase0.SignedVoluntaryExit,
|
||||
error,
|
||||
) {
|
||||
pubkey, err := util.BestPublicKey(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exit, err := generateExit(ctx, data, validator)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate voluntary exit")
|
||||
}
|
||||
root, err := exit.HashTreeRoot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate root for voluntary exit")
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using %#x as best public key for %s\n", pubkey.Marshal(), account.Name())
|
||||
}
|
||||
blsPubkey := phase0.BLSPubKey{}
|
||||
copy(blsPubkey[:], pubkey.Marshal())
|
||||
|
||||
if data.account != nil {
|
||||
signature, err := signing.SignRoot(ctx, data.account, data.passphrases, root, data.domain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to sign voluntary exit")
|
||||
}
|
||||
|
||||
results.signedVoluntaryExit = &spec.SignedVoluntaryExit{
|
||||
Message: exit,
|
||||
Signature: signature,
|
||||
}
|
||||
} else {
|
||||
results.signedVoluntaryExit = data.signedVoluntaryExit
|
||||
}
|
||||
|
||||
if !data.jsonOutput {
|
||||
if err := broadcastExit(ctx, data, results); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to broadcast voluntary exit")
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func generateExit(ctx context.Context, data *dataIn, validator *api.Validator) (*spec.VoluntaryExit, error) {
|
||||
if data == nil {
|
||||
return nil, errors.New("no data")
|
||||
}
|
||||
|
||||
if data.signedVoluntaryExit != nil {
|
||||
return data.signedVoluntaryExit.Message, nil
|
||||
}
|
||||
|
||||
if validator == nil {
|
||||
return nil, errors.New("no validator")
|
||||
}
|
||||
|
||||
exit := &spec.VoluntaryExit{
|
||||
Epoch: data.epoch,
|
||||
operation := &phase0.VoluntaryExit{
|
||||
Epoch: epoch,
|
||||
ValidatorIndex: validator.Index,
|
||||
}
|
||||
return exit, nil
|
||||
}
|
||||
|
||||
func broadcastExit(ctx context.Context, data *dataIn, results *dataOut) error {
|
||||
return data.eth2Client.(eth2client.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, results.signedVoluntaryExit)
|
||||
}
|
||||
|
||||
func fetchValidator(ctx context.Context, data *dataIn) (*api.Validator, error) {
|
||||
// Validator.
|
||||
if data.account == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var validator *api.Validator
|
||||
validatorPubKeys := make([]spec.BLSPubKey, 1)
|
||||
pubKey, err := util.BestPublicKey(data.account)
|
||||
root, err := operation.HashTreeRoot()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain public key for account")
|
||||
return nil, errors.Wrap(err, "failed to generate root for exit operation")
|
||||
}
|
||||
copy(validatorPubKeys[0][:], pubKey.Marshal())
|
||||
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", validatorPubKeys)
|
||||
|
||||
// Sign the operation.
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Signing %#x with domain %#x by public key %#x\n", root, c.domain, account.PublicKey().Marshal())
|
||||
}
|
||||
signature, err := signing.SignRoot(ctx, account, nil, root, c.domain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validator from beacon node")
|
||||
return nil, errors.Wrap(err, "failed to sign exit operation")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return nil, errors.New("validator not known by beacon node")
|
||||
}
|
||||
for _, v := range validators {
|
||||
validator = v
|
||||
}
|
||||
if validator.Status != api.ValidatorStateActiveOngoing {
|
||||
return nil, errors.New("validator is not active; cannot exit")
|
||||
}
|
||||
return validator, nil
|
||||
|
||||
return &phase0.SignedVoluntaryExit{
|
||||
Message: operation,
|
||||
Signature: signature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVoluntaryExit) error {
|
||||
root, err := op.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate message root")
|
||||
}
|
||||
|
||||
sigBytes := make([]byte, len(op.Signature))
|
||||
copy(sigBytes, op.Signature[:])
|
||||
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
|
||||
if err != nil {
|
||||
if c.verbose {
|
||||
fmt.Fprintf(os.Stderr, "Invalid signature: %v\n", err.Error())
|
||||
}
|
||||
return errors.New("invalid signature")
|
||||
}
|
||||
|
||||
container := &phase0.SigningData{
|
||||
ObjectRoot: root,
|
||||
Domain: c.domain,
|
||||
}
|
||||
signingRoot, err := ssz.HashTreeRoot(container)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate signing root")
|
||||
}
|
||||
|
||||
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%d", op.Message.ValidatorIndex))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pubkeyBytes := make([]byte, len(validatorInfo.Pubkey[:]))
|
||||
copy(pubkeyBytes, validatorInfo.Pubkey[:])
|
||||
pubkey, err := e2types.BLSPublicKeyFromBytes(pubkeyBytes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid public key")
|
||||
}
|
||||
|
||||
if !sig.Verify(signingRoot[:], pubkey) {
|
||||
return errors.New("signature does not verify")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) validateOperation(_ context.Context,
|
||||
) (
|
||||
bool,
|
||||
string,
|
||||
) {
|
||||
var validatorInfo *beacon.ValidatorInfo
|
||||
for _, chainValidatorInfo := range c.chainInfo.Validators {
|
||||
if chainValidatorInfo.Index == c.signedOperation.Message.ValidatorIndex {
|
||||
validatorInfo = chainValidatorInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
if validatorInfo == nil {
|
||||
return false, "validator not known on chain"
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", c.signedOperation)
|
||||
fmt.Fprintf(os.Stderr, "On-chain validator info: %v\n", validatorInfo)
|
||||
}
|
||||
|
||||
if validatorInfo.State == apiv1.ValidatorStateActiveExiting ||
|
||||
validatorInfo.State == apiv1.ValidatorStateActiveSlashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateExitedUnslashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateExitedSlashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateWithdrawalPossible ||
|
||||
validatorInfo.State == apiv1.ValidatorStateWithdrawalDone {
|
||||
return false, fmt.Sprintf("validator is in state %v, not suitable to generate an exit", validatorInfo.State)
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (c *command) broadcastOperation(ctx context.Context) error {
|
||||
return c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, c.signedOperation)
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
if c.offline {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to the consensus node.
|
||||
var err error
|
||||
c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect to consensus node")
|
||||
}
|
||||
|
||||
// Set up chaintime.
|
||||
c.chainTime, err = standardchaintime.New(ctx,
|
||||
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
|
||||
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create chaintime service")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateDomain(ctx context.Context) error {
|
||||
genesisValidatorsRoot, err := c.obtainGenesisValidatorsRoot(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forkVersion, err := c.obtainForkVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err := (&phase0.ForkData{
|
||||
CurrentVersion: forkVersion,
|
||||
GenesisValidatorsRoot: genesisValidatorsRoot,
|
||||
}).HashTreeRoot()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to calculate signature domain")
|
||||
}
|
||||
|
||||
copy(c.domain[:], c.chainInfo.BLSToExecutionChangeDomainType[:])
|
||||
copy(c.domain[4:], root[:])
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Domain is %#x\n", c.domain)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) obtainGenesisValidatorsRoot(ctx context.Context) (phase0.Root, error) {
|
||||
genesisValidatorsRoot := phase0.Root{}
|
||||
|
||||
if c.genesisValidatorsRoot != "" {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Genesis validators root supplied on the command line\n")
|
||||
}
|
||||
root, err := hex.DecodeString(strings.TrimPrefix(c.genesisValidatorsRoot, "0x"))
|
||||
if err != nil {
|
||||
return phase0.Root{}, errors.Wrap(err, "invalid genesis validators root supplied")
|
||||
}
|
||||
if len(root) != phase0.RootLength {
|
||||
return phase0.Root{}, errors.New("invalid length for genesis validators root")
|
||||
}
|
||||
copy(genesisValidatorsRoot[:], root)
|
||||
} else {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Genesis validators root obtained from chain info\n")
|
||||
}
|
||||
copy(genesisValidatorsRoot[:], c.chainInfo.GenesisValidatorsRoot[:])
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using genesis validators root %#x\n", genesisValidatorsRoot)
|
||||
}
|
||||
return genesisValidatorsRoot, nil
|
||||
}
|
||||
|
||||
func (c *command) obtainForkVersion(ctx context.Context) (phase0.Version, error) {
|
||||
forkVersion := phase0.Version{}
|
||||
|
||||
if c.forkVersion != "" {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Fork version supplied on the command line\n")
|
||||
}
|
||||
version, err := hex.DecodeString(strings.TrimPrefix(c.forkVersion, "0x"))
|
||||
if err != nil {
|
||||
return phase0.Version{}, errors.Wrap(err, "invalid fork version supplied")
|
||||
}
|
||||
if len(version) != phase0.ForkVersionLength {
|
||||
return phase0.Version{}, errors.New("invalid length for fork version")
|
||||
}
|
||||
copy(forkVersion[:], version)
|
||||
} else {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Fork version obtained from chain info\n")
|
||||
}
|
||||
// Use the current fork version for generating an exit as per the spec.
|
||||
copy(forkVersion[:], c.chainInfo.CurrentForkVersion[:])
|
||||
}
|
||||
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using fork version %#x\n", forkVersion)
|
||||
}
|
||||
return forkVersion, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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
|
||||
@@ -15,215 +15,467 @@ package validatorexit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/auto"
|
||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/testutil"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
||||
}
|
||||
func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
eth2Client, err := auto.New(context.Background(),
|
||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
store := scratch.New()
|
||||
require.NoError(t, e2wallet.UseStore(store))
|
||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
||||
viper.Set("passphrase", "pass")
|
||||
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
||||
"Interop 0",
|
||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
||||
[]byte("pass"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// activeValidator := &api.Validator{
|
||||
// Index: 123,
|
||||
// Balance: 32123456789,
|
||||
// Status: api.ValidatorStateActiveOngoing,
|
||||
// Validator: &spec.Validator{
|
||||
// PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
|
||||
// WithdrawalCredentials: nil,
|
||||
// EffectiveBalance: 32000000000,
|
||||
// Slashed: false,
|
||||
// ActivationEligibilityEpoch: 0,
|
||||
// ActivationEpoch: 0,
|
||||
// ExitEpoch: 0,
|
||||
// WithdrawableEpoch: 0,
|
||||
// },
|
||||
// }
|
||||
|
||||
epochFork := &spec.Fork{
|
||||
PreviousVersion: spec.Version{0x00, 0x00, 0x00, 0x00},
|
||||
CurrentVersion: spec.Version{0x00, 0x00, 0x00, 0x00},
|
||||
Epoch: 0,
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dataIn *dataIn
|
||||
err string
|
||||
name string
|
||||
command *command
|
||||
expected *phase0.SignedVoluntaryExit
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "EpochTooLate",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
eth2Client: eth2Client,
|
||||
fork: epochFork,
|
||||
currentEpoch: 10,
|
||||
account: interop0,
|
||||
passphrases: []string{"pass"},
|
||||
epoch: 9999999,
|
||||
domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
|
||||
name: "MnemonicInvalid",
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
|
||||
path: "m/12381/3600/0/0/0",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "not generating exit for an epoch in the far future",
|
||||
err: "mnemonic is invalid",
|
||||
},
|
||||
{
|
||||
name: "AccountUnknown",
|
||||
dataIn: &dataIn{
|
||||
timeout: 5 * time.Second,
|
||||
eth2Client: eth2Client,
|
||||
fork: epochFork,
|
||||
currentEpoch: 10,
|
||||
account: interop0,
|
||||
passphrases: []string{"pass"},
|
||||
epoch: 10,
|
||||
domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
|
||||
name: "PathInvalid",
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "validator not known by beacon node",
|
||||
},
|
||||
// {
|
||||
// name: "Good",
|
||||
// dataIn: &dataIn{
|
||||
// timeout: 5 * time.Second,
|
||||
// eth2Client: eth2Client,
|
||||
// fork: epochFork,
|
||||
// currentEpoch: 10,
|
||||
// account: interop0,
|
||||
// passphrases: []string{"pass"},
|
||||
// epoch: 10,
|
||||
// domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
|
||||
// },
|
||||
// },
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := process(context.Background(), test.dataIn)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateExit(t *testing.T) {
|
||||
activeValidator := &api.Validator{
|
||||
Index: 123,
|
||||
Balance: 32123456789,
|
||||
Status: api.ValidatorStateActiveOngoing,
|
||||
Validator: &spec.Validator{
|
||||
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
|
||||
WithdrawalCredentials: nil,
|
||||
EffectiveBalance: 32000000000,
|
||||
Slashed: false,
|
||||
ActivationEligibilityEpoch: 0,
|
||||
ActivationEpoch: 0,
|
||||
ExitEpoch: 0,
|
||||
WithdrawableEpoch: 0,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
validator *api.Validator
|
||||
dataIn *dataIn
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: "no data",
|
||||
},
|
||||
{
|
||||
name: "SignedVoluntaryExitGood",
|
||||
dataIn: &dataIn{
|
||||
signedVoluntaryExit: &spec.SignedVoluntaryExit{
|
||||
Message: &spec.VoluntaryExit{
|
||||
Epoch: spec.Epoch(123),
|
||||
ValidatorIndex: spec.ValidatorIndex(456),
|
||||
},
|
||||
Signature: spec.BLSSignature{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
||||
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
|
||||
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ValidatorMissing",
|
||||
dataIn: &dataIn{},
|
||||
err: "no validator",
|
||||
},
|
||||
{
|
||||
name: "ValidatorGood",
|
||||
dataIn: &dataIn{},
|
||||
validator: activeValidator,
|
||||
err: "path m/12381/3600/0/0 does not match EIP-2334 format for a validator",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
dataIn: &dataIn{
|
||||
signedVoluntaryExit: &spec.SignedVoluntaryExit{
|
||||
Message: &spec.VoluntaryExit{
|
||||
Epoch: spec.Epoch(123),
|
||||
ValidatorIndex: spec.ValidatorIndex(456),
|
||||
},
|
||||
Signature: spec.BLSSignature{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
||||
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
|
||||
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
|
||||
},
|
||||
},
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
path: "m/12381/3600/0/0/0",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
expected: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
validator: activeValidator,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := generateExit(context.Background(), test.dataIn, test.validator)
|
||||
err := test.command.generateOperationFromMnemonicAndPath(ctx)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command *command
|
||||
expected *phase0.SignedVoluntaryExit
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "MnemonicInvalid",
|
||||
command: &command{
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
|
||||
validator: "0",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "mnemonic is invalid",
|
||||
},
|
||||
{
|
||||
name: "ValidatorMissing",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "no validator specified",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
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",
|
||||
validator: "0",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
expected: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GoodPubkey",
|
||||
command: &command{
|
||||
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",
|
||||
validator: "0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c87",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
expected: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.generateOperationFromMnemonicAndValidator(ctx)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateOperationFromSeedAndPath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 2,
|
||||
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
|
||||
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 3,
|
||||
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
|
||||
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
validators := make(map[string]*beacon.ValidatorInfo, len(chainInfo.Validators))
|
||||
for i := range chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", chainInfo.Validators[i].Pubkey)] = chainInfo.Validators[i]
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command *command
|
||||
seed []byte
|
||||
path string
|
||||
err string
|
||||
expected *phase0.SignedVoluntaryExit
|
||||
}{
|
||||
{
|
||||
name: "PathInvalid",
|
||||
command: &command{
|
||||
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",
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "invalid",
|
||||
err: "failed to generate validator private key: not master at path component 0",
|
||||
},
|
||||
{
|
||||
name: "ValidatorUnknown",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/999/0/0",
|
||||
err: "unknown validator",
|
||||
},
|
||||
// {
|
||||
// name: "ValidatorAlreadyExited",
|
||||
// command: &command{
|
||||
// chainInfo: chainInfo,
|
||||
// },
|
||||
// seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
// path: "m/12381/3600/2/0/0",
|
||||
// },
|
||||
{
|
||||
name: "GoodPath0",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/0/0/0",
|
||||
expected: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GoodPath3",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
|
||||
path: "m/12381/3600/3/0/0",
|
||||
expected: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 3,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.generateOperationFromSeedAndPath(ctx, validators, test.seed, test.path)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyOperation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command *command
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "SignatureMissing",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: "invalid signature",
|
||||
},
|
||||
{
|
||||
name: "SignatureShort",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
err: "invalid signature",
|
||||
},
|
||||
{
|
||||
name: "SignatureIncorrect",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
|
||||
},
|
||||
},
|
||||
err: "signature does not verify",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
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",
|
||||
validator: "0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.verifySignedOperation(ctx, test.command.signedOperation)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestObtainOperationFromInput(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
CurrentForkVersion: phase0.Version{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command *command
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "InvalidFilename",
|
||||
command: &command{
|
||||
signedOperationInput: `[]`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "failed to read input file: open []: no such file or directory",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
command: &command{
|
||||
signedOperationInput: `{invalid}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "failed to parse exit operation input: invalid character 'i' looking for beginning of object key string",
|
||||
},
|
||||
{
|
||||
name: "Unverifable",
|
||||
command: &command{
|
||||
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x9978b49c21603f04a3044e4c490cb4687c6e14c2daed2592e0022dcd63ebe74af11acabaae50e18a1dae96d9d256bf9f02488505c1fbb34a0b68ecc5b5f5ea53dbd00908e31ea8ca9d02083b9ef1c7d232f4bad9ea564bc587d527b774978aee"}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "signature does not verify",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x89f5c44288f95e19b6c139f2623005665b983462a228120977d81f2ef547560be22446de21a8a937d9dda4e2d2ec4175196496cdd1306dec4a125f8c861f806171504a9d6a610ec4e135047e4fb67052ecc4561360d0c3de04b6fbc4474223ff"}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.obtainOperationFromInput(ctx)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2019, 2020 Weald Technology Trading
|
||||
// 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
|
||||
@@ -21,19 +21,19 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Run runs the wallet create data command.
|
||||
// Run runs the command.
|
||||
func Run(cmd *cobra.Command) (string, error) {
|
||||
ctx := context.Background()
|
||||
dataIn, err := input(ctx)
|
||||
|
||||
c, err := newCommand(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain input")
|
||||
return "", errors.Wrap(err, "failed to set up command")
|
||||
}
|
||||
|
||||
// Further errors do not need a usage report.
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
dataOut, err := process(ctx, dataIn)
|
||||
if err != nil {
|
||||
if err := c.process(ctx); err != nil {
|
||||
return "", errors.Wrap(err, "failed to process")
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
results, err := output(ctx, dataOut)
|
||||
results, err := c.output(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to obtain output")
|
||||
}
|
||||
|
||||
@@ -58,9 +58,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,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: "ValidatorsZero",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -130,7 +130,6 @@ func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
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 {
|
||||
|
||||
140
cmd/validator/summary/command.go
Normal file
140
cmd/validator/summary/command.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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 validatorsummary
|
||||
|
||||
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
|
||||
validators []string
|
||||
jsonOutput bool
|
||||
|
||||
// Data access.
|
||||
eth2Client eth2client.Service
|
||||
chainTime chaintime.Service
|
||||
proposerDutiesProvider eth2client.ProposerDutiesProvider
|
||||
attesterDutiesProvider eth2client.AttesterDutiesProvider
|
||||
blocksProvider eth2client.SignedBeaconBlockProvider
|
||||
syncCommitteesProvider eth2client.SyncCommitteesProvider
|
||||
validatorsProvider eth2client.ValidatorsProvider
|
||||
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
|
||||
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
|
||||
|
||||
// Processing.
|
||||
validatorsByIndex map[phase0.ValidatorIndex]*apiv1.Validator
|
||||
|
||||
// Results.
|
||||
summary *validatorSummary
|
||||
}
|
||||
|
||||
type validatorSummary struct {
|
||||
Epoch phase0.Epoch `json:"epoch"`
|
||||
Validators []*apiv1.Validator `json:"validators"`
|
||||
FirstSlot phase0.Slot `json:"first_slot"`
|
||||
LastSlot phase0.Slot `json:"last_slot"`
|
||||
ActiveValidators int `json:"active_validators"`
|
||||
ParticipatingValidators int `json:"participating_validators"`
|
||||
NonParticipatingValidators []*nonParticipatingValidator `json:"non_participating_validators"`
|
||||
IncorrectHeadValidators []*validatorFault `json:"incorrect_head_validators"`
|
||||
UntimelyHeadValidators []*validatorFault `json:"untimely_head_validators"`
|
||||
UntimelySourceValidators []*validatorFault `json:"untimely_source_validators"`
|
||||
IncorrectTargetValidators []*validatorFault `json:"incorrect_target_validators"`
|
||||
UntimelyTargetValidators []*validatorFault `json:"untimely_target_validators"`
|
||||
Slots []*slot `json:"slots"`
|
||||
Proposals []*epochProposal `json:"-"`
|
||||
SyncCommittee []*epochSyncCommittee `json:"-"`
|
||||
}
|
||||
|
||||
type slot struct {
|
||||
Slot phase0.Slot `json:"slot"`
|
||||
Attestations *slotAttestations `json:"attestations"`
|
||||
}
|
||||
|
||||
type slotAttestations struct {
|
||||
Expected int `json:"expected"`
|
||||
Included int `json:"included"`
|
||||
CorrectHead int `json:"correct_head"`
|
||||
TimelyHead int `json:"timely_head"`
|
||||
CorrectTarget int `json:"correct_target"`
|
||||
TimelyTarget int `json:"timely_target"`
|
||||
TimelySource int `json:"timely_source"`
|
||||
}
|
||||
|
||||
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 validatorFault struct {
|
||||
Validator phase0.ValidatorIndex `json:"validator_index"`
|
||||
AttestationData *phase0.AttestationData `json:"attestation_data,omitempty"`
|
||||
InclusionDistance int `json:"inclusion_delay"`
|
||||
}
|
||||
|
||||
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"),
|
||||
validatorsByIndex: make(map[phase0.ValidatorIndex]*apiv1.Validator),
|
||||
summary: &validatorSummary{},
|
||||
}
|
||||
|
||||
// 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.validators = viper.GetStringSlice("validators")
|
||||
c.jsonOutput = viper.GetBool("json")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
64
cmd/validator/summary/command_internal_test.go
Normal file
64
cmd/validator/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 validatorsummary
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
86
cmd/validator/summary/output.go
Normal file
86
cmd/validator/summary/output.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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 validatorsummary
|
||||
|
||||
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))
|
||||
if len(c.summary.NonParticipatingValidators) > 0 {
|
||||
builder.WriteString(" Non-participating validators:\n")
|
||||
for _, validator := range c.summary.NonParticipatingValidators {
|
||||
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d)\n", validator.Validator, validator.Slot, validator.Committee))
|
||||
}
|
||||
}
|
||||
if len(c.summary.IncorrectHeadValidators) > 0 {
|
||||
builder.WriteString(" Incorrect head validators:\n")
|
||||
for _, validator := range c.summary.IncorrectHeadValidators {
|
||||
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index))
|
||||
}
|
||||
}
|
||||
if len(c.summary.UntimelyHeadValidators) > 0 {
|
||||
builder.WriteString(" Untimely head validators:\n")
|
||||
for _, validator := range c.summary.UntimelyHeadValidators {
|
||||
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d, inclusion distance %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index, validator.InclusionDistance))
|
||||
}
|
||||
}
|
||||
if len(c.summary.UntimelySourceValidators) > 0 {
|
||||
builder.WriteString(" Untimely source validators:\n")
|
||||
for _, validator := range c.summary.UntimelySourceValidators {
|
||||
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d, inclusion distance %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index, validator.InclusionDistance))
|
||||
}
|
||||
}
|
||||
if len(c.summary.IncorrectTargetValidators) > 0 {
|
||||
builder.WriteString(" Incorrect target validators:\n")
|
||||
for _, validator := range c.summary.IncorrectTargetValidators {
|
||||
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index))
|
||||
}
|
||||
}
|
||||
if len(c.summary.UntimelyTargetValidators) > 0 {
|
||||
builder.WriteString(" Untimely target validators:\n")
|
||||
for _, validator := range c.summary.UntimelyTargetValidators {
|
||||
builder.WriteString(fmt.Sprintf(" %d (slot %d, committee %d, inclusion distance %d)\n", validator.Validator, validator.AttestationData.Slot, validator.AttestationData.Index, validator.InclusionDistance))
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
424
cmd/validator/summary/process.go
Normal file
424
cmd/validator/summary/process.go
Normal file
@@ -0,0 +1,424 @@
|
||||
// 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 validatorsummary
|
||||
|
||||
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/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
|
||||
c.summary.Slots = make([]*slot, 1+int(c.summary.LastSlot)-int(c.summary.FirstSlot))
|
||||
for i := range c.summary.Slots {
|
||||
c.summary.Slots[i] = &slot{
|
||||
Slot: c.summary.FirstSlot + phase0.Slot(i),
|
||||
}
|
||||
}
|
||||
|
||||
c.summary.Validators, err = util.ParseValidators(ctx, c.validatorsProvider, c.validators, fmt.Sprintf("%d", c.summary.FirstSlot))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse validators")
|
||||
}
|
||||
// Reorder validators by index.
|
||||
sort.Slice(c.summary.Validators, func(i int, j int) bool {
|
||||
return c.summary.Validators[i].Index < c.summary.Validators[j].Index
|
||||
})
|
||||
|
||||
// Create a map for validator indices for easy lookup.
|
||||
c.validatorsByIndex = make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
for _, validator := range c.summary.Validators {
|
||||
c.validatorsByIndex[validator.Index] = validator
|
||||
}
|
||||
|
||||
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 {
|
||||
if _, exists := c.validatorsByIndex[duty.ValidatorIndex]; !exists {
|
||||
continue
|
||||
}
|
||||
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() (map[phase0.ValidatorIndex]*apiv1.Validator, []phase0.ValidatorIndex) {
|
||||
activeValidators := make(map[phase0.ValidatorIndex]*apiv1.Validator)
|
||||
activeValidatorIndices := make([]phase0.ValidatorIndex, 0, len(c.validatorsByIndex))
|
||||
for _, validator := range c.summary.Validators {
|
||||
if validator.Validator.ActivationEpoch <= c.summary.Epoch && validator.Validator.ExitEpoch > c.summary.Epoch {
|
||||
activeValidators[validator.Index] = validator
|
||||
activeValidatorIndices = append(activeValidatorIndices, validator.Index)
|
||||
}
|
||||
}
|
||||
|
||||
return activeValidators, activeValidatorIndices
|
||||
}
|
||||
|
||||
func (c *command) processAttesterDuties(ctx context.Context) error {
|
||||
activeValidators, activeValidatorIndices := c.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()
|
||||
}
|
||||
|
||||
// Obtain the duties for the validators to know where they should be attesting.
|
||||
duties, err := c.attesterDutiesProvider.AttesterDuties(ctx, c.summary.Epoch, activeValidatorIndices)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to obtain attester duties")
|
||||
}
|
||||
for slot := c.chainTime.FirstSlotOfEpoch(c.summary.Epoch); slot < c.chainTime.FirstSlotOfEpoch(c.summary.Epoch+1); slot++ {
|
||||
index := int(slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
||||
c.summary.Slots[index].Attestations = &slotAttestations{}
|
||||
}
|
||||
|
||||
// Need a cache of beacon block headers to reduce lookup times.
|
||||
headersCache := util.NewBeaconBlockHeaderCache(c.beaconBlockHeadersProvider)
|
||||
|
||||
// Need a map of duties to easily find the attestations we care about.
|
||||
dutiesBySlot := make(map[phase0.Slot]map[phase0.CommitteeIndex][]*apiv1.AttesterDuty)
|
||||
dutiesByValidatorIndex := make(map[phase0.ValidatorIndex]*apiv1.AttesterDuty)
|
||||
for _, duty := range duties {
|
||||
index := int(duty.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
||||
dutiesByValidatorIndex[duty.ValidatorIndex] = duty
|
||||
c.summary.Slots[index].Attestations.Expected++
|
||||
if _, exists := dutiesBySlot[duty.Slot]; !exists {
|
||||
dutiesBySlot[duty.Slot] = make(map[phase0.CommitteeIndex][]*apiv1.AttesterDuty)
|
||||
}
|
||||
if _, exists := dutiesBySlot[duty.Slot][duty.CommitteeIndex]; !exists {
|
||||
dutiesBySlot[duty.Slot][duty.CommitteeIndex] = make([]*apiv1.AttesterDuty, 0)
|
||||
}
|
||||
dutiesBySlot[duty.Slot][duty.CommitteeIndex] = append(dutiesBySlot[duty.Slot][duty.CommitteeIndex], duty)
|
||||
}
|
||||
|
||||
c.summary.IncorrectHeadValidators = make([]*validatorFault, 0)
|
||||
c.summary.UntimelyHeadValidators = make([]*validatorFault, 0)
|
||||
c.summary.UntimelySourceValidators = make([]*validatorFault, 0)
|
||||
c.summary.IncorrectTargetValidators = make([]*validatorFault, 0)
|
||||
c.summary.UntimelyTargetValidators = make([]*validatorFault, 0)
|
||||
|
||||
// Hunt through the blocks looking for attestations from the validators.
|
||||
votes := make(map[phase0.ValidatorIndex]struct{})
|
||||
for slot := firstSlot; slot <= lastSlot; slot++ {
|
||||
if err := c.processAttesterDutiesSlot(ctx, slot, dutiesBySlot, votes, headersCache, activeValidatorIndices); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Use dutiesMap and votes to work out which validators didn't participate.
|
||||
c.summary.NonParticipatingValidators = make([]*nonParticipatingValidator, 0)
|
||||
for _, index := range activeValidatorIndices {
|
||||
if _, exists := votes[index]; !exists {
|
||||
// Didn't vote.
|
||||
duty := dutiesByValidatorIndex[index]
|
||||
c.summary.NonParticipatingValidators = append(c.summary.NonParticipatingValidators, &nonParticipatingValidator{
|
||||
Validator: index,
|
||||
Slot: duty.Slot,
|
||||
Committee: duty.CommitteeIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the non-participating validators list.
|
||||
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
|
||||
})
|
||||
|
||||
c.summary.ActiveValidators = len(activeValidators)
|
||||
c.summary.ParticipatingValidators = len(votes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) processAttesterDutiesSlot(ctx context.Context,
|
||||
slot phase0.Slot,
|
||||
dutiesBySlot map[phase0.Slot]map[phase0.CommitteeIndex][]*apiv1.AttesterDuty,
|
||||
votes map[phase0.ValidatorIndex]struct{},
|
||||
headersCache *util.BeaconBlockHeaderCache,
|
||||
activeValidatorIndices []phase0.ValidatorIndex,
|
||||
) error {
|
||||
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 {
|
||||
// No block at this slot; that's fine.
|
||||
return nil
|
||||
}
|
||||
attestations, err := block.Attestations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, attestation := range attestations {
|
||||
if _, exists := dutiesBySlot[attestation.Data.Slot]; !exists {
|
||||
// We do not have any attestations for this slot.
|
||||
continue
|
||||
}
|
||||
if _, exists := dutiesBySlot[attestation.Data.Slot][attestation.Data.Index]; !exists {
|
||||
// We do not have any attestations for this committee.
|
||||
continue
|
||||
}
|
||||
for _, duty := range dutiesBySlot[attestation.Data.Slot][attestation.Data.Index] {
|
||||
if attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
|
||||
// Found it.
|
||||
if _, exists := votes[duty.ValidatorIndex]; exists {
|
||||
// Duplicate; ignore.
|
||||
continue
|
||||
}
|
||||
votes[duty.ValidatorIndex] = struct{}{}
|
||||
|
||||
// Update the metrics for the attestation.
|
||||
index := int(attestation.Data.Slot - c.chainTime.FirstSlotOfEpoch(c.summary.Epoch))
|
||||
c.summary.Slots[index].Attestations.Included++
|
||||
inclusionDelay := slot - duty.Slot
|
||||
|
||||
fault := &validatorFault{
|
||||
Validator: duty.ValidatorIndex,
|
||||
AttestationData: attestation.Data,
|
||||
InclusionDistance: int(inclusionDelay),
|
||||
}
|
||||
|
||||
headCorrect, err := util.AttestationHeadCorrect(ctx, headersCache, attestation)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to calculate if attestation had correct head vote")
|
||||
}
|
||||
if headCorrect {
|
||||
c.summary.Slots[index].Attestations.CorrectHead++
|
||||
if inclusionDelay == 1 {
|
||||
c.summary.Slots[index].Attestations.TimelyHead++
|
||||
} else {
|
||||
c.summary.UntimelyHeadValidators = append(c.summary.UntimelyHeadValidators, fault)
|
||||
}
|
||||
} else {
|
||||
c.summary.IncorrectHeadValidators = append(c.summary.IncorrectHeadValidators, fault)
|
||||
if inclusionDelay > 1 {
|
||||
c.summary.UntimelyHeadValidators = append(c.summary.UntimelyHeadValidators, fault)
|
||||
}
|
||||
}
|
||||
|
||||
if inclusionDelay <= 5 {
|
||||
c.summary.Slots[index].Attestations.TimelySource++
|
||||
} else {
|
||||
c.summary.UntimelySourceValidators = append(c.summary.UntimelySourceValidators, fault)
|
||||
}
|
||||
|
||||
targetCorrect, err := util.AttestationTargetCorrect(ctx, headersCache, c.chainTime, attestation)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to calculate if attestation had correct target vote")
|
||||
}
|
||||
if targetCorrect {
|
||||
c.summary.Slots[index].Attestations.CorrectTarget++
|
||||
if inclusionDelay <= 32 {
|
||||
c.summary.Slots[index].Attestations.TimelyTarget++
|
||||
} else {
|
||||
c.summary.UntimelyTargetValidators = append(c.summary.UntimelyTargetValidators, fault)
|
||||
}
|
||||
} else {
|
||||
c.summary.IncorrectTargetValidators = append(c.summary.IncorrectTargetValidators, fault)
|
||||
if inclusionDelay > 32 {
|
||||
c.summary.UntimelyTargetValidators = append(c.summary.UntimelyTargetValidators, fault)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(votes) == len(activeValidatorIndices) {
|
||||
// Found them all.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return 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
|
||||
// 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.attesterDutiesProvider, isProvider = c.eth2Client.(eth2client.AttesterDutiesProvider)
|
||||
if !isProvider {
|
||||
return errors.New("connection does not provide attester 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/validator/summary/process_internal_test.go
Normal file
62
cmd/validator/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 validatorsummary
|
||||
|
||||
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/validator/summary/run.go
Normal file
50
cmd/validator/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 validatorsummary
|
||||
|
||||
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,9 +72,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,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: "Good",
|
||||
vars: map[string]interface{}{
|
||||
|
||||
@@ -119,7 +119,6 @@ func (c *command) setup(ctx context.Context) error {
|
||||
if c.validators == "" {
|
||||
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 {
|
||||
|
||||
@@ -26,7 +26,7 @@ var validatorCredentialsGetCmd = &cobra.Command{
|
||||
Short: "Obtain withdrawal credentials for an Ethereum consensus validator",
|
||||
Long: `Obtain withdrawal credentials for an Ethereum consensus validator. For example:
|
||||
|
||||
ethdo validator credentials get --account=primary/validator
|
||||
ethdo validator credentials get --validator=primary/validator
|
||||
|
||||
In quiet mode this will return 0 if the validator exists, otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -47,19 +47,11 @@ In quiet mode this will return 0 if the validator exists, otherwise 1.`,
|
||||
func init() {
|
||||
validatorCredentialsCmd.AddCommand(validatorCredentialsGetCmd)
|
||||
validatorCredentialsFlags(validatorCredentialsGetCmd)
|
||||
validatorCredentialsGetCmd.Flags().String("account", "", "Account for which to fetch validator credentials")
|
||||
validatorCredentialsGetCmd.Flags().String("index", "", "Validator index for which to fetch validator credentials")
|
||||
validatorCredentialsGetCmd.Flags().String("pubkey", "", "Validator public key for which to fetch validator credentials")
|
||||
validatorCredentialsGetCmd.Flags().String("validator", "", "Validator for which to get validator credentials")
|
||||
}
|
||||
|
||||
func validatorCredentialsGetBindings() {
|
||||
if err := viper.BindPFlag("account", validatorCredentialsGetCmd.Flags().Lookup("account")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", validatorCredentialsGetCmd.Flags().Lookup("index")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("pubkey", validatorCredentialsGetCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
if err := viper.BindPFlag("validator", validatorCredentialsGetCmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
98
cmd/validatorcredentialsset.go
Normal file
98
cmd/validatorcredentialsset.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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"
|
||||
validatorcredentialsset "github.com/wealdtech/ethdo/cmd/validator/credentials/set"
|
||||
)
|
||||
|
||||
var validatorCredentialsSetCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Set withdrawal credentials for an Ethereum consensus validator",
|
||||
Long: `Set withdrawal credentials for an Ethereum consensus validator via a "change credentials" operation. For example:
|
||||
|
||||
ethdo validator credentials set --validator=primary/validator --withdrawal-address=0x00...13 --private-key=0x00...1f
|
||||
|
||||
The validator account can be specified in one of a number of ways:
|
||||
|
||||
- mnemonic using --mnemonic; this will scan the mnemonic and generate all applicable operations
|
||||
- mnemonic and path to the validator key using --mnemonic and --path; this will generate a single operation
|
||||
- mnemonic and validator index or public key --mnemonic and --validator; this will generate a single operation
|
||||
- mnemonic and withdrawal private key using --mnemonic and --private-key; this will generate all applicable operations
|
||||
- validator and withdrawal private key using --validator and --private-key; this will generate a single operation
|
||||
- account and withdrawal account using --account and --withdrawal-account; this will generate a single operation
|
||||
|
||||
In quiet mode this will return 0 if the credentials operation has been generated (and successfully broadcast if online), otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := validatorcredentialsset.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
validatorCredentialsCmd.AddCommand(validatorCredentialsSetCmd)
|
||||
validatorCredentialsFlags(validatorCredentialsSetCmd)
|
||||
validatorCredentialsSetCmd.Flags().Bool("prepare-offline", false, "Create files for offline use")
|
||||
validatorCredentialsSetCmd.Flags().String("validator", "", "Validator for which to set validator credentials")
|
||||
validatorCredentialsSetCmd.Flags().String("withdrawal-account", "", "Account with which the validator's withdrawal credentials were set")
|
||||
validatorCredentialsSetCmd.Flags().String("withdrawal-address", "", "Execution address to which to direct withdrawals")
|
||||
validatorCredentialsSetCmd.Flags().String("signed-operations", "", "Use pre-defined JSON signed operation as created by --json to transmit the credentials change operation (reads from change-operations.json if not present)")
|
||||
validatorCredentialsSetCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
|
||||
validatorCredentialsSetCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
|
||||
validatorCredentialsSetCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")
|
||||
validatorCredentialsSetCmd.Flags().String("genesis-validators-root", "", "Genesis validators root to use for signing (overrides fetching from beacon node)")
|
||||
}
|
||||
|
||||
func validatorCredentialsSetBindings() {
|
||||
if err := viper.BindPFlag("prepare-offline", validatorCredentialsSetCmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validator", validatorCredentialsSetCmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("signed-operations", validatorCredentialsSetCmd.Flags().Lookup("signed-operations")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("withdrawal-account", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-account")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("withdrawal-address", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-address")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", validatorCredentialsSetCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("offline", validatorCredentialsSetCmd.Flags().Lookup("offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("fork-version", validatorCredentialsSetCmd.Flags().Lookup("fork-version")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("genesis-validators-root", validatorCredentialsSetCmd.Flags().Lookup("genesis-validators-root")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,16 @@ var validatorExitCmd = &cobra.Command{
|
||||
Short: "Send an exit request for a validator",
|
||||
Long: `Send an exit request for a validator. For example:
|
||||
|
||||
ethdo validator exit --account=primary/validator --passphrase=secret
|
||||
ethdo validator exit --validator=12345
|
||||
|
||||
In quiet mode this will return 0 if the transaction has been generated, otherwise 1.`,
|
||||
The validator and key can be specified in one of a number of ways:
|
||||
|
||||
- mnemonic and path to the validator using --mnemonic and --path
|
||||
- mnemonic and validator index or public key using --mnemonic and --validator
|
||||
- validator private key using --private-key
|
||||
- validator account using --validator
|
||||
|
||||
In quiet mode this will return 0 if the exit operation has been generated (and successfully broadcast if online), otherwise 1.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
res, err := validatorexit.Run(cmd)
|
||||
if err != nil {
|
||||
@@ -48,22 +55,38 @@ func init() {
|
||||
validatorCmd.AddCommand(validatorExitCmd)
|
||||
validatorFlags(validatorExitCmd)
|
||||
validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)")
|
||||
validatorExitCmd.Flags().String("key", "", "Private key if validator not known by ethdo")
|
||||
validatorExitCmd.Flags().String("exit", "", "Use pre-defined JSON data as created by --json to exit")
|
||||
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
|
||||
validatorExitCmd.Flags().Bool("prepare-offline", false, "Create files for offline use")
|
||||
validatorExitCmd.Flags().String("validator", "", "Validator to exit")
|
||||
validatorExitCmd.Flags().String("signed-operation", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operation (reads from exit-operations.json if not present)")
|
||||
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
|
||||
validatorExitCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
|
||||
validatorExitCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")
|
||||
validatorExitCmd.Flags().String("genesis-validators-root", "", "Genesis validators root to use for signing (overrides fetching from beacon node)")
|
||||
}
|
||||
|
||||
func validatorExitBindings() {
|
||||
if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("key", validatorExitCmd.Flags().Lookup("key")); err != nil {
|
||||
if err := viper.BindPFlag("prepare-offline", validatorExitCmd.Flags().Lookup("prepare-offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("exit", validatorExitCmd.Flags().Lookup("exit")); err != nil {
|
||||
if err := viper.BindPFlag("validator", validatorExitCmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("signed-operation", validatorExitCmd.Flags().Lookup("signed-operation")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("offline", validatorExitCmd.Flags().Lookup("offline")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("fork-version", validatorExitCmd.Flags().Lookup("fork-version")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("genesis-validators-root", validatorExitCmd.Flags().Lookup("genesis-validators-root")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020, 2021 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
|
||||
@@ -16,10 +16,9 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -32,7 +31,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
string2eth "github.com/wealdtech/go-string2eth"
|
||||
)
|
||||
|
||||
@@ -41,7 +39,7 @@ var validatorInfoCmd = &cobra.Command{
|
||||
Short: "Obtain information about a validator",
|
||||
Long: `Obtain information about validator. For example:
|
||||
|
||||
ethdo validator info --account=primary/validator
|
||||
ethdo validator info --validator=primary/validator
|
||||
|
||||
In quiet mode this will return 0 if the validator information can be obtained, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@@ -54,32 +52,21 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
)
|
||||
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
|
||||
|
||||
account, err := validatorInfoAccount(ctx, eth2Client)
|
||||
errCheck(err, "Failed to obtain validator account")
|
||||
|
||||
pubKeys := make([]spec.BLSPubKey, 1)
|
||||
pubKey, err := util.BestPublicKey(account)
|
||||
errCheck(err, "Failed to obtain validator public key")
|
||||
copy(pubKeys[0][:], pubKey.Marshal())
|
||||
validators, err := eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", pubKeys)
|
||||
errCheck(err, "Failed to obtain validator information")
|
||||
if len(validators) == 0 {
|
||||
fmt.Println("Validator not known by beacon node")
|
||||
os.Exit(_exitSuccess)
|
||||
if viper.GetString("validator") == "" {
|
||||
fmt.Println("validator is required")
|
||||
os.Exit(_exitFailure)
|
||||
}
|
||||
|
||||
var validator *api.Validator
|
||||
for _, v := range validators {
|
||||
validator = v
|
||||
}
|
||||
validator, err := util.ParseValidator(ctx, eth2Client.(eth2client.ValidatorsProvider), viper.GetString("validator"), "head")
|
||||
errCheck(err, "Failed to obtain validator")
|
||||
|
||||
if verbose {
|
||||
network, err := util.Network(ctx, eth2Client)
|
||||
errCheck(err, "Failed to obtain network")
|
||||
outputIf(debug, fmt.Sprintf("Network is %s", network))
|
||||
pubKey, err := bestPublicKey(account)
|
||||
pubKey, err := validator.PubKey(ctx)
|
||||
if err == nil {
|
||||
deposits, totalDeposited, err := graphData(network, pubKey.Marshal())
|
||||
deposits, totalDeposited, err := graphData(network, pubKey[:])
|
||||
if err == nil && deposits > 0 {
|
||||
fmt.Printf("Number of deposits: %d\n", deposits)
|
||||
fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(uint64(totalDeposited), true))
|
||||
@@ -122,54 +109,6 @@ In quiet mode this will return 0 if the validator information can be obtained, o
|
||||
},
|
||||
}
|
||||
|
||||
// validatorInfoAccount obtains the account for the validator info command.
|
||||
func validatorInfoAccount(ctx context.Context, eth2Client eth2client.Service) (e2wtypes.Account, error) {
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
switch {
|
||||
case 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")
|
||||
}
|
||||
case viper.GetString("pubkey") != "":
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("pubkey"), "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", viper.GetString("pubkey")))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", viper.GetString("pubkey")))
|
||||
}
|
||||
case viper.GetInt64("index") != -1:
|
||||
validatorsProvider, isValidatorsProvider := eth2Client.(eth2client.ValidatorsProvider)
|
||||
if !isValidatorsProvider {
|
||||
return nil, errors.New("client does not provide validator information")
|
||||
}
|
||||
index := spec.ValidatorIndex(viper.GetInt64("index"))
|
||||
validators, err := validatorsProvider.Validators(ctx, "head", []spec.ValidatorIndex{
|
||||
index,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain validator information")
|
||||
}
|
||||
if len(validators) == 0 {
|
||||
return nil, errors.New("unknown validator index")
|
||||
}
|
||||
pubKeyBytes := make([]byte, 48)
|
||||
copy(pubKeyBytes, validators[index].Validator.PublicKey[:])
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", viper.GetString("pubkey")))
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("neither account nor public key supplied")
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// graphData returns data from the graph about number and amount of deposits
|
||||
func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error) {
|
||||
subgraph := ""
|
||||
@@ -186,7 +125,7 @@ func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error
|
||||
return 0, 0, errors.Wrap(err, "failed to check if there is already a deposit for this validator")
|
||||
}
|
||||
defer graphResp.Body.Close()
|
||||
body, err := ioutil.ReadAll(graphResp.Body)
|
||||
body, err := io.ReadAll(graphResp.Body)
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(err, "bad information returned from existing deposit check")
|
||||
}
|
||||
@@ -224,16 +163,12 @@ func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error
|
||||
|
||||
func init() {
|
||||
validatorCmd.AddCommand(validatorInfoCmd)
|
||||
validatorInfoCmd.Flags().String("pubkey", "", "Public key for which to obtain status")
|
||||
validatorInfoCmd.Flags().Int64("index", -1, "Index for which to obtain status")
|
||||
validatorInfoCmd.Flags().String("validator", "", "Public key for which to obtain status")
|
||||
validatorFlags(validatorInfoCmd)
|
||||
}
|
||||
|
||||
func validatorInfoBindings() {
|
||||
if err := viper.BindPFlag("pubkey", validatorInfoCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("index", validatorInfoCmd.Flags().Lookup("index")); err != nil {
|
||||
if err := viper.BindPFlag("validator", validatorInfoCmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ func init() {
|
||||
validatorCmd.AddCommand(validatorKeycheckCmd)
|
||||
validatorFlags(validatorKeycheckCmd)
|
||||
validatorKeycheckCmd.Flags().String("withdrawal-credentials", "", "Withdrawal credentials to check (can run offline)")
|
||||
validatorKeycheckCmd.Flags().String("mnemonic", "", "Mnemonic from which to generate withdrawal credentials")
|
||||
validatorKeycheckCmd.Flags().String("privkey", "", "Private key from which to generate withdrawal credentials")
|
||||
}
|
||||
|
||||
@@ -58,9 +57,6 @@ func validatorKeycheckBindings() {
|
||||
if err := viper.BindPFlag("withdrawal-credentials", validatorKeycheckCmd.Flags().Lookup("withdrawal-credentials")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("mnemonic", validatorKeycheckCmd.Flags().Lookup("mnemonic")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("privkey", validatorKeycheckCmd.Flags().Lookup("privkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
66
cmd/validatorsummary.go
Normal file
66
cmd/validatorsummary.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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
validatorsummary "github.com/wealdtech/ethdo/cmd/validator/summary"
|
||||
)
|
||||
|
||||
var validatorSummaryCmd = &cobra.Command{
|
||||
Use: "summary",
|
||||
Short: "Obtain summary information about validator(s) in an epoch",
|
||||
Long: `Obtain summary information about one or more validators in an epoch. For example:
|
||||
|
||||
ethdo validator summary --validators=1,2,3 --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 := validatorsummary.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if viper.GetBool("quiet") {
|
||||
return nil
|
||||
}
|
||||
if res != "" {
|
||||
fmt.Println(res)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
validatorCmd.AddCommand(validatorSummaryCmd)
|
||||
validatorFlags(validatorSummaryCmd)
|
||||
validatorSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information ()")
|
||||
validatorSummaryCmd.Flags().StringSlice("validators", nil, "the list of validators for which to obtain information")
|
||||
validatorSummaryCmd.Flags().Bool("json", false, "output data in JSON format")
|
||||
}
|
||||
|
||||
func validatorSummaryBindings() {
|
||||
validatorBindings()
|
||||
if err := viper.BindPFlag("epoch", validatorSummaryCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("validators", validatorSummaryCmd.Flags().Lookup("validators")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("json", validatorSummaryCmd.Flags().Lookup("json")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
|
||||
// ReleaseVersion is the release version of the codebase.
|
||||
// Usually overridden by tag names when building binaries.
|
||||
var ReleaseVersion = "local build (latest release 1.25.0)"
|
||||
var ReleaseVersion = "local build (latest release 1.27.0)"
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -15,6 +15,7 @@ package walletdelete
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -35,6 +36,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
||||
return nil, errors.New("cannot obtain store for the wallet")
|
||||
}
|
||||
store := storeProvider.Store()
|
||||
|
||||
if store.Name() != "filesystem" {
|
||||
return nil, fmt.Errorf("cannot delete %s wallet automatically, please remove manually", store.Name())
|
||||
}
|
||||
storeLocationProvider, isProvider := store.(e2wtypes.StoreLocationProvider)
|
||||
if !isProvider {
|
||||
return nil, errors.New("cannot obtain store location for the wallet")
|
||||
|
||||
@@ -15,7 +15,6 @@ package walletdelete
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -31,7 +30,7 @@ import (
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
base, err := ioutil.TempDir("", "")
|
||||
base, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(base)
|
||||
store := filesystem.New(filesystem.WithLocation(base))
|
||||
|
||||
@@ -15,7 +15,6 @@ package walletexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -31,7 +30,7 @@ import (
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
base, err := ioutil.TempDir("", "")
|
||||
base, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(base)
|
||||
store := filesystem.New(filesystem.WithLocation(base))
|
||||
|
||||
@@ -16,7 +16,7 @@ package walletimport
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -58,7 +58,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
}
|
||||
if !strings.HasPrefix(viper.GetString("data"), "0x") {
|
||||
// Assume this is a path; read the file and replace the path with its contents.
|
||||
fileData, err := ioutil.ReadFile(viper.GetString("data"))
|
||||
fileData, err := os.ReadFile(viper.GetString("data"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read wallet import data")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Copyright © 2021, 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,7 +15,6 @@ package walletsharedexport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -31,7 +30,7 @@ import (
|
||||
func TestProcess(t *testing.T) {
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
base, err := ioutil.TempDir("", "")
|
||||
base, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(base)
|
||||
store := filesystem.New(filesystem.WithLocation(base))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2021 Weald Technology Trading
|
||||
// Copyright © 2021, 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,7 +15,7 @@ package walletsharedimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -52,7 +52,7 @@ func input(ctx context.Context) (*dataIn, error) {
|
||||
if viper.GetString("file") == "" {
|
||||
return nil, errors.New("file is required")
|
||||
}
|
||||
data.file, err = ioutil.ReadFile(viper.GetString("file"))
|
||||
data.file, err = os.ReadFile(viper.GetString("file"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read wallet import file")
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user