Compare commits

...

76 Commits

Author SHA1 Message Date
Jim McDonald
28b90414d2 Bump version. 2022-03-11 13:39:35 +00:00
Jim McDonald
879a20a7af Support bellatrix. 2022-03-11 13:38:22 +00:00
Jim McDonald
3d49e091e5 Add block analyze. 2022-03-06 22:47:37 +00:00
Jim McDonald
3b51c67e7d Add ssz option to block info. 2022-02-16 15:06:17 +00:00
Jim McDonald
1d559c167b Update dependencies. 2022-01-31 08:10:44 +00:00
Jim McDonald
824c53f6f2 Do not write out text-based error messages whilst streaming with json output. 2022-01-24 16:59:01 +00:00
Jim McDonald
50ffdcd97c Allow custom epochs for attester duties. 2022-01-23 22:38:15 +00:00
Jim McDonald
0e79334863 Tidy up synccommittee inclusion output. 2022-01-17 22:06:08 +00:00
Jim McDonald
c6c3143dd5 Bump version. 2022-01-12 15:10:00 +00:00
Jim McDonald
4d718f614c Tidy up tests, standardise error messages. 2022-01-12 15:07:07 +00:00
Jim McDonald
9a1db9b0a4 Add synccommittee inclusion. 2022-01-12 14:05:35 +00:00
Jim McDonald
7ede620ce7 Add test for new output. 2022-01-10 15:09:46 +00:00
Jim McDonald
a41cc77c18 Add vote success info to attester inclusion. 2022-01-10 15:04:37 +00:00
Jim McDonald
ad83006069 Refactor. 2022-01-10 13:43:11 +00:00
Jim McDonald
976d758cac Add sync committee information. 2022-01-10 13:41:14 +00:00
Jim McDonald
1be72d9ea8 Reduce process time when committees not available. 2022-01-10 13:22:15 +00:00
Jim McDonald
175c33a494 Update docker file. 2021-12-06 22:33:24 +00:00
Jim McDonald
b712f70667 Fix incorrect use of validator index. 2021-12-01 11:39:00 +00:00
Jim McDonald
d4ef9d43b5 Remove rightmost null bytes from graffiti. 2021-11-14 10:40:13 +00:00
Jim McDonald
58de55b40f Bump version. 2021-11-03 13:32:09 +00:00
Jim McDonald
22dad263db Use faster method to obtain sync committees for future epochs. 2021-11-03 13:31:46 +00:00
Jim McDonald
81fa11ad45 Provide sync committee slots in chain status. 2021-11-03 13:29:26 +00:00
Jim McDonald
f5c4551c0c Clarify use of --connection. 2021-11-02 11:39:00 +00:00
Jim McDonald
a00d09e28f Do not report insecure local connection. 2021-11-01 09:46:53 +00:00
Jim McDonald
6bfd5677e6 Bump version. 2021-10-31 09:09:40 +00:00
Jim McDonald
dbe0a9d9f1 Add --validators option to validator expectation. 2021-10-31 09:08:09 +00:00
Jim McDonald
fa390ecdf7 Add "validator expectation". 2021-10-30 22:03:47 +01:00
Jim McDonald
f70abb2165 Add --period to "synccommittee members". 2021-10-30 20:54:11 +01:00
Jim McDonald
ac18cbab3e Bump version. 2021-10-27 14:30:02 +01:00
Jim McDonald
2f1c89d0a6 Update for test. 2021-10-26 18:02:51 +01:00
Jim McDonald
a3ad4181d3 Add exit/withdrawable epoch to validator info. 2021-10-26 17:29:38 +01:00
Jim McDonald
f8ac23e8d7 Merge pull request #39 from Blockdaemon/prater_launchpad
Output "prater" as network name in launchpad format
2021-10-01 15:13:06 +01:00
Jonas Pfannschmidt
b6815d1a2a Output "prater" as network name in launchpad format
Before it would output "unknown" in "eth2_network_name" when creating
a launchpad file using prater's fork version: 0x00001020
2021-10-01 12:19:03 +01:00
Jim McDonald
a79b813bd0 Add "chain verify signedcontributionandproof". 2021-09-28 20:34:01 +01:00
Jim McDonald
ad971145f0 Show both block and body root in "block info". 2021-09-25 17:21:33 +01:00
Jim McDonald
602948921c Generate SHA256 of compressed file. 2021-09-24 12:09:07 +01:00
Jim McDonald
607e969a30 Rework chain status output. 2021-09-24 08:52:09 +01:00
Jim McDonald
79f1ae9930 Update dependencies. 2021-09-21 14:23:17 +01:00
Jim McDonald
a98f681f98 Fix bad dependency. 2021-09-21 13:58:37 +01:00
Jim McDonald
e0e1f697d3 Bump version. 2021-09-21 13:47:33 +01:00
Jim McDonald
1b70a66120 Update workflow. 2021-09-21 13:44:02 +01:00
Jim McDonald
94eba96a6e Tidy-ups. 2021-09-15 22:50:49 +01:00
Jim McDonald
f052d8e307 Update dependencies. 2021-09-15 08:54:07 +01:00
Jim McDonald
df45686828 Update dependencies. 2021-09-15 08:51:28 +01:00
Jim McDonald
84d228877a Documentation updates. 2021-08-28 20:18:29 +01:00
Jim McDonald
b2b26742b0 Fix documentation. 2021-08-28 20:04:52 +01:00
Jim McDonald
9dc630c809 Add synccommittee members. 2021-08-21 00:08:50 +01:00
Jim McDonald
452430db56 Linting. 2021-08-19 13:52:06 +01:00
Jim McDonald
b0d676a734 Update version. 2021-08-19 13:50:29 +01:00
Jim McDonald
ff73470085 Merge pull request #38 from wealdtech/altair
Altair block info
2021-08-19 13:48:18 +01:00
Jim McDonald
a41349999f Add block info for Altair. 2021-08-19 13:44:06 +01:00
Jim McDonald
004f4bc41a Update for block info. 2021-08-19 13:42:22 +01:00
Jim McDonald
64c8e1a051 Updates for Altair. 2021-08-19 13:41:42 +01:00
Jim McDonald
d95d48f6b2 Add more data to "chain info". 2021-08-19 13:18:43 +01:00
Jim McDonald
3e702f0c51 Bump go build version. 2021-08-03 23:50:58 +01:00
Jim McDonald
2e36fcc3ce Use local shamir codebase. 2021-08-03 23:34:16 +01:00
Jim McDonald
aa0cda306b Update dependencies. 2021-08-03 22:58:35 +01:00
Jim McDonald
aa79f83f35 Update changelog 2021-08-03 14:05:15 +01:00
Jim McDonald
8de7e75c77 Merge pull request #36 from wealdtech/sss-export
Shared wallet export/import
2021-08-03 14:03:18 +01:00
Jim McDonald
4a1b419c0e Update documentation. 2021-08-03 13:49:28 +01:00
Jim McDonald
b6a08d5073 Tidy-ups. 2021-08-03 13:49:28 +01:00
Jim McDonald
65d2ab5d53 Tidy-ups. 2021-08-03 13:49:27 +01:00
Jim McDonald
34b03f9d53 Handle timezone in chain time. 2021-08-03 13:49:27 +01:00
Jim McDonald
dca513b8c9 Handle timezone in chain time. 2021-07-30 08:31:43 +01:00
Jim McDonald
446941be92 Add SSS import/export. 2021-07-02 22:48:30 +01:00
Jim McDonald
b76cdb01d1 Update version. 2021-05-13 12:42:14 +01:00
Jim McDonald
ce5b250ef0 Report on missing interfaces.
This update handles the situation where an ETH2 client does not provide
all required interfaces for the 'chain status' command, returning an
error rather than simply panicing.

Fixes #35.
2021-05-13 12:39:14 +01:00
Jim McDonald
2c4ccf62af Avoid crash with latest version of herumi/go-bls. 2021-05-13 12:37:46 +01:00
Jim McDonald
c7ad5194e6 Bump version number. 2021-03-14 22:03:22 +00:00
Jim McDonald
ddb866131b Merge pull request #32 from wealdtech/eth1-withdrawal-credentials
Allow use of Ethereum 1 withdrawal credentials
2021-03-14 21:47:21 +00:00
Jim McDonald
49fb03aa3a Allow use of Ethereum 1 withdrawal credentials.
Release 1.0.1 of the Ethereum 2 specification allows withdrawal
credentials to be Ethereum 1 addresses.  This enables the use of such
addresses when generating and verifying deposit data.
2021-03-12 12:53:42 +00:00
Jim McDonald
1ed3a51117 ETH1 withdrawal credentials. 2021-02-26 15:19:37 +00:00
Jim McDonald
4d5660ccbb Fix crash in attester/duties and inclusion.
A recent change for a return value going from an array to a map caused a
bad indexing in to the returned data.  This ensures that the value is
read directly from the map rather than using a hard-coded offset.
2021-02-13 22:25:26 +00:00
Jim McDonald
7596d271ad Linting. 2021-02-10 10:24:49 +00:00
Jim McDonald
943f9350f3 Add 'chain time' and 'validator keycheck' commands. 2021-02-10 10:13:24 +00:00
Jim McDonald
07863846e6 Use double quotes for Windows compatability. 2021-02-04 21:54:43 +00:00
133 changed files with 9424 additions and 1297 deletions

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

@@ -0,0 +1,23 @@
name: golangci-lint
on: [ push, pull_request ]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.29
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
args: --timeout=10m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

View File

@@ -14,7 +14,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
go-version: ^1.16
id: go
- name: Check out code into the Go module directory
@@ -50,11 +50,11 @@ jobs:
- name: Fetch xgo
run: |
go get github.com/suburbandad/xgo
go install github.com/wealdtech/xgo@latest
- name: Cross-compile linux
run: |
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" --targets="linux/amd64,linux/arm64" github.com/wealdtech/ethdo
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" --targets="linux/amd64" github.com/wealdtech/ethdo
- name: Cross-compile windows
run: |
@@ -63,20 +63,20 @@ jobs:
- name: Create windows release files
run: |
mv ethdo-windows-4.0-amd64.exe ethdo.exe
sha256sum ethdo.exe >ethdo-${RELEASE_VERSION}-windows.sha256
zip --junk-paths ethdo-${RELEASE_VERSION}-windows-exe.zip ethdo.exe
sha256sum ethdo-${RELEASE_VERSION}-windows-exe.zip >ethdo-${RELEASE_VERSION}-windows.sha256
- name: Create linux AMD64 tgz file
run: |
mv ethdo-linux-amd64 ethdo
sha256sum ethdo >ethdo-${RELEASE_VERSION}-linux-amd64.sha256
tar zcf ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz ethdo
sha256sum ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz >ethdo-${RELEASE_VERSION}-linux-amd64.sha256
- name: Create linux ARM64 tgz file
run: |
mv ethdo-linux-arm64 ethdo
sha256sum ethdo >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
# - name: Create linux ARM64 tgz file
# run: |
# mv ethdo-linux-arm64 ethdo
# tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
# sha256sum ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
- name: Create release
id: create_release
@@ -133,24 +133,24 @@ jobs:
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
asset_content_type: application/gzip
- name: Upload linux ARM64 checksum file
id: upload-release-asset-linux-arm64-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
asset_content_type: text/plain
# - name: Upload linux ARM64 checksum file
# id: upload-release-asset-linux-arm64-checksum
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
# asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
# asset_content_type: text/plain
- name: Upload linux ARM64 tgz file
id: upload-release-asset-linux-arm64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
asset_content_type: application/gzip
# - 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

View File

@@ -1,3 +1,56 @@
1.18.0:
- add "-ssz" option to "block info"
- add "block analyze" command
- support bellatrix
1.17.0:
- add sync committee information to "chain time"
- add details of vote success to "attester inclusion --verbose"
- add "synccommittee inclusion"
1.15.1:
- provide sync committee slots in "chain status"
- clarify that --connection should be a URL
1.15.0:
- add --period to "synccommittee members", can be "current", "next"
- add "validator expectation"
1.14.0:
- add "chain verify signedcontributionandproof"
- show both block and body root in "block info"
- add exit / withdrawable epoch to "validator info"
1.13.0:
- rework and provide additional information to "chain status" output
1.12.0:
- add "synccommittee members"
1.11.0
- add Altair information to "block info"
- add more information to "chain info"
1.10.2
- use local shamir code (copied from github.com/hashicorp/vault)
1.10.0
- add "wallet sharedexport" and "wallet sharedimport"
1.9.1
- Avoid crash when required interfaces for chain status command are not supported
- Avoid crash with latest version of herumi/go-bls
1.9.0
- allow use of Ethereum 1 address as withdrawal credentials
1.8.1
- fix issue where 'attester duties' and 'attester inclusion' could crash
1.8.0
- add "chain time"
- add "validator keycheck"
1.7.5:
- add "slot time"
- add "attester duties"

View File

@@ -1,4 +1,4 @@
FROM golang:1.14-buster as builder
FROM golang:1.17-bullseye as builder
WORKDIR /app
@@ -10,10 +10,12 @@ COPY . .
RUN go build
FROM debian:buster-slim
FROM debian:bullseye-slim
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/ethdo /app
ENTRYPOINT ["/app/ethdo"]
ENTRYPOINT ["/app/ethdo"]

View File

@@ -25,6 +25,9 @@ import (
func blsPrivateKey(input string) *e2types.BLSPrivateKey {
data, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
key, err := e2types.BLSPrivateKeyFromBytes(data)
if err != nil {
panic(err)
@@ -58,7 +61,7 @@ func TestOutput(t *testing.T) {
{
name: "PrivatKey",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
},
needs: []string{"Public key", "Private key"},
@@ -75,7 +78,7 @@ func TestOutput(t *testing.T) {
name: "All",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
showPrivateKey: true,
showWithdrawalCredentials: true,
},
needs: []string{"Public key", "Private key", "Withdrawal credentials"},

View File

@@ -27,6 +27,3 @@ var attestationCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(attestationCmd)
}
func attestationFlags(cmd *cobra.Command) {
}

View File

@@ -63,17 +63,19 @@ func input(ctx context.Context) (*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")
return nil, err
}
// Required data.
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch
epoch := viper.GetInt64("epoch")
if epoch == -1 {
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {

View File

@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
}

View File

@@ -66,7 +66,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
validator := validators[0]
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,

View File

@@ -15,12 +15,14 @@ package attesterinclusion
import (
"context"
"fmt"
"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/services/chaintime"
"github.com/wealdtech/ethdo/util"
)
@@ -34,9 +36,11 @@ type dataIn struct {
slotsPerEpoch uint64
// Operation.
eth2Client eth2client.Service
chainTime chaintime.Service
epoch spec.Epoch
account string
pubKey string
index string
}
func input(ctx context.Context) (*dataIn, error) {
@@ -50,18 +54,24 @@ func input(ctx context.Context) (*dataIn, error) {
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Account or pubkey.
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account or pubkey is required")
}
// Account.
data.account = viper.GetString("account")
// PubKey.
data.pubKey = viper.GetString("pubkey")
// ID.
data.index = viper.GetString("index")
if viper.GetString("account") == "" && viper.GetString("index") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account, index or pubkey is required")
}
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
return nil, err
}
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
@@ -84,6 +94,9 @@ func input(ctx context.Context) (*dataIn, error) {
}
}
data.epoch = spec.Epoch(epoch)
if data.debug {
fmt.Printf("Epoch is %d\n", data.epoch)
}
return data, nil
}

View File

@@ -61,11 +61,11 @@ func TestInput(t *testing.T) {
err: "timeout is required",
},
{
name: "AccountMissing",
name: "IndexMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "account or pubkey is required",
err: "account, index or pubkey is required",
},
{
name: "ConnectionMissing",
@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -16,8 +16,9 @@ package attesterinclusion
import (
"context"
"fmt"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
@@ -25,22 +26,67 @@ type dataOut struct {
debug bool
quiet bool
verbose bool
slot spec.Slot
attestation *phase0.Attestation
slot phase0.Slot
attestationIndex uint64
inclusionDelay spec.Slot
inclusionDelay phase0.Slot
found bool
headCorrect bool
headTimely bool
sourceTimely bool
targetCorrect bool
targetTimely bool
}
func output(ctx context.Context, data *dataOut) (string, error) {
buf := strings.Builder{}
if data == nil {
return "", errors.New("no data")
return buf.String(), errors.New("no data")
}
if !data.quiet {
if data.found {
return fmt.Sprintf("Attestation included in block %d, attestation %d (inclusion delay %d)", data.slot, data.attestationIndex, data.inclusionDelay), nil
buf.WriteString("Attestation included in block ")
buf.WriteString(fmt.Sprintf("%d", data.slot))
buf.WriteString(", index ")
buf.WriteString(fmt.Sprintf("%d", data.attestationIndex))
if data.verbose {
buf.WriteString("\nInclusion delay: ")
buf.WriteString(fmt.Sprintf("%d", data.inclusionDelay))
buf.WriteString("\nHead correct: ")
if data.headCorrect {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nHead timely: ")
if data.headTimely {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nSource timely: ")
if data.sourceTimely {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nTarget correct: ")
if data.targetCorrect {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nTarget timely: ")
if data.targetTimely {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
}
} else {
buf.WriteString("Attestation not found")
}
return "Attestation not found", nil
}
return "", nil
return buf.String(), nil
}

View File

@@ -44,7 +44,29 @@ func TestOutput(t *testing.T) {
attestationIndex: 456,
inclusionDelay: 7,
},
res: "Attestation included in block 123, attestation 456 (inclusion delay 7)",
res: `Attestation included in block 123, index 456`,
},
{
name: "Verbose",
dataOut: &dataOut{
verbose: true,
found: true,
slot: 123,
attestationIndex: 456,
inclusionDelay: 7,
headCorrect: true,
headTimely: false,
sourceTimely: false,
targetCorrect: true,
targetTimely: true,
},
res: `Attestation included in block 123, index 456
Inclusion delay: 7
Head correct: ✓
Head timely: ✕
Source timely: ✕
Target correct: ✓
Target timely: ✓`,
},
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -14,17 +14,16 @@
package attesterinclusion
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -32,41 +31,33 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
var account e2wtypes.Account
var err error
if data.account != "" {
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
}
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 {
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
// Fetch validator
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(account)
validatorIndex, err := util.ValidatorIndex(ctx, data.eth2Client, data.account, data.pubKey, data.index)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
return nil, errors.Wrap(err, "failed to obtain validator index")
}
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), []phase0.ValidatorIndex{validatorIndex})
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
validator := validators[0]
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,
@@ -78,6 +69,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}
if data.debug {
fmt.Printf("Duty is %s\n", duty.String())
}
startSlot := duty.Slot + 1
endSlot := startSlot + 32
@@ -89,20 +83,50 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if signedBlock == nil {
continue
}
if signedBlock.Message.Slot != slot {
blockSlot, err := signedBlock.Slot()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block slot")
}
if blockSlot != slot {
continue
}
if data.debug {
fmt.Printf("Fetched block for slot %d\n", slot)
}
for i, attestation := range signedBlock.Message.Body.Attestations {
attestations, err := signedBlock.Attestations()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block attestations")
}
for i, attestation := range attestations {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
headCorrect, err = calcHeadCorrect(ctx, data, attestation)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain head correct result")
}
targetCorrect, err = calcTargetCorrect(ctx, data, attestation)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain target correct result")
}
}
results.found = true
results.attestation = attestation
results.slot = slot
results.attestationIndex = uint64(i)
results.inclusionDelay = slot - duty.Slot
results.found = true
results.sourceTimely = results.inclusionDelay <= 5 // sqrt(32)
results.targetCorrect = targetCorrect
results.targetTimely = targetCorrect && results.inclusionDelay <= 32
results.headCorrect = headCorrect
results.headTimely = headCorrect && results.inclusionDelay == 1
if data.debug {
fmt.Printf("Attestation is %s\n", attestation.String())
}
return results, nil
}
}
@@ -110,9 +134,52 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
}
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
// Start with first slot of the target epoch.
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
}
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}

View File

@@ -49,6 +49,7 @@ func init() {
attesterFlags(attesterInclusionCmd)
attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch")
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
attesterInclusionCmd.Flags().Int64("index", -1, "the index of the attester")
}
func attesterInclusionBindings() {
@@ -58,4 +59,7 @@ func attesterInclusionBindings() {
if err := viper.BindPFlag("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("index", attesterInclusionCmd.Flags().Lookup("index")); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,136 @@
// 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 blockanalyze
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
"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.
blockID string
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
blocksProvider eth2client.SignedBeaconBlockProvider
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Constants.
timelySourceWeight uint64
timelyTargetWeight uint64
timelyHeadWeight uint64
syncRewardWeight uint64
proposerWeight uint64
weightDenominator uint64
// Processing.
priorAttestations map[string]*attestationData
// Head roots provides the root of the head slot at given slots.
headRoots map[phase0.Slot]phase0.Root
// Target roots provides the root of the target epoch at given slots.
targetRoots map[phase0.Slot]phase0.Root
// Block info.
// Map is slot -> committee index -> validator committee index -> votes.
votes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist
// Results.
analysis *blockAnalysis
}
type blockAnalysis struct {
Slot phase0.Slot `json:"slot"`
Attestations []*attestationAnalysis `json:"attestations"`
SyncCommitee *syncCommitteeAnalysis `json:"sync_committee"`
Value float64 `json:"value"`
}
type attestationAnalysis struct {
Head phase0.Root `json:"head"`
Target phase0.Root `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type syncCommitteeAnalysis struct {
Contributions int `json:"contributions"`
PossibleContributions int `json:"possible_contributions"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type attestationData struct {
Block phase0.Slot `json:"block"`
Index int `json:"index"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
priorAttestations: make(map[string]*attestationData),
headRoots: make(map[phase0.Slot]phase0.Root),
targetRoots: make(map[phase0.Slot]phase0.Root),
votes: make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
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.blockID = viper.GetString("blockid")
c.stream = viper.GetBool("stream")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

@@ -0,0 +1,82 @@
// 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 blockanalyze
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: "ConnectionMissing",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
},
err: "connection is required",
},
{
name: "ValidatorsZero",
vars: map[string]interface{}{
"timeout": "5s",
"validators": "0",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "validators must be at least 1",
},
{
name: "Good",
vars: map[string]interface{}{
"validators": "1",
"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)
}
})
}
}

157
cmd/block/analyze/output.go Normal file
View File

@@ -0,0 +1,157 @@
// 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 blockanalyze
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)
}
type attestationAnalysisJSON struct {
Head string `json:"head"`
Target string `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
func (a *attestationAnalysis) MarshalJSON() ([]byte, error) {
return json.Marshal(attestationAnalysisJSON{
Head: fmt.Sprintf("%#x", a.Head),
Target: fmt.Sprintf("%#x", a.Target),
Distance: a.Distance,
Duplicate: a.Duplicate,
NewVotes: a.NewVotes,
Votes: a.Votes,
PossibleVotes: a.PossibleVotes,
HeadCorrect: a.HeadCorrect,
HeadTimely: a.HeadTimely,
SourceTimely: a.SourceTimely,
TargetCorrect: a.TargetCorrect,
TargetTimely: a.TargetTimely,
Score: a.Score,
Value: a.Value,
})
}
func (c *command) outputJSON(_ context.Context) (string, error) {
data, err := json.Marshal(c.analysis)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
for i, attestation := range c.analysis.Attestations {
if c.verbose {
builder.WriteString("Attestation ")
builder.WriteString(fmt.Sprintf("%d", i))
builder.WriteString(": ")
builder.WriteString("distance ")
builder.WriteString(fmt.Sprintf("%d", attestation.Distance))
builder.WriteString(", ")
if attestation.Duplicate != nil {
builder.WriteString("duplicate of attestation ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Index))
builder.WriteString(" in block ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
builder.WriteString("\n")
continue
}
builder.WriteString(fmt.Sprintf("%d", attestation.NewVotes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.Votes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.PossibleVotes))
builder.WriteString(" new/total/possible votes")
if attestation.NewVotes == 0 {
builder.WriteString("\n")
continue
} else {
builder.WriteString(", ")
}
switch {
case !attestation.HeadCorrect:
builder.WriteString("head vote incorrect, ")
case !attestation.HeadTimely:
builder.WriteString("head vote correct but late, ")
}
if !attestation.SourceTimely {
builder.WriteString("source vote late, ")
}
switch {
case !attestation.TargetCorrect:
builder.WriteString("target vote incorrect, ")
case !attestation.TargetTimely:
builder.WriteString("target vote correct but late, ")
}
builder.WriteString("score ")
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Score))
builder.WriteString(", value ")
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Value))
builder.WriteString("\n")
}
}
if c.analysis.SyncCommitee.Contributions > 0 {
if c.verbose {
builder.WriteString("Sync committee contributions: ")
builder.WriteString(fmt.Sprintf("%d", c.analysis.SyncCommitee.Contributions))
builder.WriteString(" contributions, score ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Score))
builder.WriteString(", value ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Value))
builder.WriteString("\n")
}
}
builder.WriteString("Value for block ")
builder.WriteString(fmt.Sprintf("%d", c.analysis.Slot))
builder.WriteString(": ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.Value))
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,431 @@
// 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 blockanalyze
import (
"bytes"
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.blockID)
if err != nil {
return errors.Wrap(err, "failed to obtain beacon block")
}
if block == nil {
return errors.New("empty beacon block")
}
slot, err := block.Slot()
if err != nil {
return err
}
attestations, err := block.Attestations()
if err != nil {
return err
}
c.analysis = &blockAnalysis{
Slot: slot,
}
// Calculate how many parents we need to fetch.
minSlot := slot
for _, attestation := range attestations {
if attestation.Data.Slot < minSlot {
minSlot = attestation.Data.Slot
}
}
if c.debug {
fmt.Printf("Need to fetch blocks to slot %d\n", minSlot)
}
if err := c.fetchParents(ctx, block, minSlot); err != nil {
return err
}
return c.analyze(ctx, block)
}
func (c *command) analyze(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
if err := c.analyzeAttestations(ctx, block); err != nil {
return err
}
if err := c.analyzeSyncCommittees(ctx, block); err != nil {
return err
}
return nil
}
func (c *command) analyzeAttestations(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
slot, err := block.Slot()
if err != nil {
return err
}
c.analysis.Attestations = make([]*attestationAnalysis, len(attestations))
blockVotes := make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist)
for i, attestation := range attestations {
if c.debug {
fmt.Printf("Processing attestation %d\n", i)
}
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate head timely.
analysis.HeadTimely = attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate target timely.
analysis.TargetTimely = attestation.Data.Slot >= slot-32
}
// Calculate score and value.
if analysis.TargetCorrect && analysis.TargetTimely {
analysis.Score += float64(c.timelyTargetWeight) / float64(c.weightDenominator)
}
if analysis.SourceTimely {
analysis.Score += float64(c.timelySourceWeight) / float64(c.weightDenominator)
}
if analysis.HeadCorrect && analysis.HeadTimely {
analysis.Score += float64(c.timelyHeadWeight) / float64(c.weightDenominator)
}
analysis.Value = analysis.Score * float64(analysis.NewVotes)
c.analysis.Value += analysis.Value
c.analysis.Attestations[i] = analysis
}
return nil
}
func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedBeaconBlock, minSlot phase0.Slot) error {
parentRoot, err := block.ParentRoot()
if err != nil {
return err
}
// Obtain the parent block.
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
if err != nil {
return err
}
if parentBlock == nil {
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
}
parentSlot, err := parentBlock.Slot()
if err != nil {
return err
}
if parentSlot < minSlot {
return nil
}
if err := c.processParentBlock(ctx, parentBlock); err != nil {
return err
}
return c.fetchParents(ctx, parentBlock, minSlot)
}
func (c *command) processParentBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
slot, err := block.Slot()
if err != nil {
return err
}
if c.debug {
fmt.Printf("Processing block %d\n", slot)
}
for i, attestation := range attestations {
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
Block: slot,
Index: i,
}
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
}
}
}
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.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
// Obtain the number of active validators.
var isProvider bool
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon block information")
}
c.blockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
if !isProvider {
return errors.New("connection does not provide beacon block header information")
}
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
if !isProvider {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["TIMELY_SOURCE_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
}
var ok bool
c.timelySourceWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(26)
}
c.timelyTargetWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
}
c.timelyHeadWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
}
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(2)
}
c.syncRewardWeight, ok = tmp.(uint64)
if !ok {
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
}
tmp, exists = spec["PROPOSER_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(8)
}
c.proposerWeight, ok = tmp.(uint64)
if !ok {
return errors.New("PROPOSER_WEIGHT of unexpected type")
}
tmp, exists = spec["WEIGHT_DENOMINATOR"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(64)
}
c.weightDenominator, ok = tmp.(uint64)
if !ok {
return errors.New("WEIGHT_DENOMINATOR of unexpected type")
}
return nil
}
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
root, exists := c.headRoots[slot]
if !exists {
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
c.analysis.SyncCommitee = &syncCommitteeAnalysis{}
switch block.Version {
case spec.DataVersionPhase0:
return nil
case spec.DataVersionAltair:
c.analysis.SyncCommitee.Contributions = int(block.Altair.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Altair.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)
}
}

View File

@@ -0,0 +1,63 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockanalyze
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",
"validators": "1",
"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/block/analyze/run.go Normal file
View 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 blockanalyze
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
}

View File

@@ -32,6 +32,7 @@ type dataIn struct {
// Operation.
eth2Client eth2client.Service
jsonOutput bool
sszOutput bool
// Chain information.
blockID string
stream bool
@@ -48,13 +49,14 @@ func input(ctx context.Context) (*dataIn, error) {
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.jsonOutput = viper.GetBool("json")
data.sszOutput = viper.GetBool("ssz")
data.stream = viper.GetBool("stream")
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
return nil, err
}
if viper.GetString("blockid") == "" {

View File

@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
name: "ConnectionBad",
@@ -79,7 +79,7 @@ func TestInput(t *testing.T) {
timeout: 5 * time.Second,
blockID: "justified",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
name: "BlockIDNil",

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019, 2020, 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -18,13 +18,16 @@ import (
"context"
"encoding/hex"
"fmt"
"math/big"
"sort"
"strings"
"time"
"unicode/utf8"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
"github.com/wealdtech/go-string2eth"
@@ -47,34 +50,44 @@ func output(ctx context.Context, data *dataOut) (string, error) {
return "", nil
}
func outputBlockGeneral(ctx context.Context, verbose bool, block *spec.BeaconBlock, genesisTime time.Time, slotDuration time.Duration, slotsPerEpoch uint64) (string, error) {
bodyRoot, err := block.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate block root")
}
func outputBlockGeneral(ctx context.Context,
verbose bool,
slot phase0.Slot,
blockRoot phase0.Root,
bodyRoot phase0.Root,
parentRoot phase0.Root,
stateRoot phase0.Root,
graffiti []byte,
genesisTime time.Time,
slotDuration time.Duration,
slotsPerEpoch uint64,
) (
string,
error,
) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Slot: %d\n", block.Slot))
res.WriteString(fmt.Sprintf("Epoch: %d\n", spec.Epoch(uint64(block.Slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(block.Slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", bodyRoot))
res.WriteString(fmt.Sprintf("Slot: %d\n", slot))
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
if verbose {
res.WriteString(fmt.Sprintf("Parent root: %#x\n", block.ParentRoot))
res.WriteString(fmt.Sprintf("State root: %#x\n", block.StateRoot))
res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot))
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
}
if len(block.Body.Graffiti) > 0 && hex.EncodeToString(block.Body.Graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
if utf8.Valid(block.Body.Graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(block.Body.Graffiti)))
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
if utf8.Valid(graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", strings.TrimRight(string(graffiti), "\u0000")))
} else {
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", block.Body.Graffiti))
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti))
}
}
return res.String(), nil
}
func outputBlockETH1Data(ctx context.Context, eth1Data *spec.ETH1Data) (string, error) {
func outputBlockETH1Data(ctx context.Context, eth1Data *phase0.ETH1Data) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount))
@@ -84,10 +97,10 @@ func outputBlockETH1Data(ctx context.Context, eth1Data *spec.ETH1Data) (string,
return res.String(), nil
}
func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*spec.Attestation) (string, error) {
func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*phase0.Attestation) (string, error) {
res := strings.Builder{}
validatorCommittees := make(map[spec.Slot]map[spec.CommitteeIndex][]spec.ValidatorIndex)
validatorCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations)))
if verbose {
beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider)
@@ -100,21 +113,25 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
if !exists {
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot))
if err != nil {
return "", errors.Wrap(err, "failed to obtain beacon committees")
}
for _, beaconCommittee := range beaconCommittees {
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
validatorCommittees[beaconCommittee.Slot] = make(map[spec.CommitteeIndex][]spec.ValidatorIndex)
// Failed to get it; create an empty committee to stop us continually attempting to re-fetch.
validatorCommittees[att.Data.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
} else {
for _, beaconCommittee := range beaconCommittees {
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
}
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
}
committees = validatorCommittees[att.Data.Slot]
}
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitsToString(att.AggregationBits)))
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
if _, exists := committees[att.Data.Index]; exists {
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
}
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch))
@@ -128,7 +145,7 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
return res.String(), nil
}
func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*spec.AttesterSlashing) (string, error) {
func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*phase0.AttesterSlashing) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings)))
@@ -175,7 +192,7 @@ func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Ser
return res.String(), nil
}
func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*spec.Deposit) (string, error) {
func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
res := strings.Builder{}
// Deposits.
@@ -194,14 +211,14 @@ func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*spec.Dep
return res.String(), nil
}
func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Service, verbose bool, voluntaryExits []*spec.SignedVoluntaryExit) (string, error) {
func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Service, verbose bool, voluntaryExits []*phase0.SignedVoluntaryExit) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Voluntary exits: %d\n", len(voluntaryExits)))
if verbose {
for i, voluntaryExit := range voluntaryExits {
res.WriteString(fmt.Sprintf(" %d:\n", i))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []spec.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
} else {
@@ -214,7 +231,45 @@ func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Servic
return res.String(), nil
}
func outputBlockText(ctx context.Context, data *dataOut, signedBlock *spec.SignedBeaconBlock) (string, error) {
func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service, verbose bool, syncAggregate *altair.SyncAggregate, epoch phase0.Epoch) (string, error) {
res := strings.Builder{}
res.WriteString("Sync aggregate: ")
res.WriteString(fmt.Sprintf("%d/%d\n", syncAggregate.SyncCommitteeBits.Count(), syncAggregate.SyncCommitteeBits.Len()))
if verbose {
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
if isProvider {
config, err := specProvider.Spec(ctx)
if err == nil {
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
res.WriteString(" Contributions: ")
res.WriteString(bitvectorToString(syncAggregate.SyncCommitteeBits))
res.WriteString("\n")
syncCommitteesProvider, isProvider := eth2Client.(eth2client.SyncCommitteesProvider)
if isProvider {
syncCommittee, err := syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", uint64(epoch)*slotsPerEpoch))
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err))
} else {
res.WriteString(" Contributing validators:")
for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ {
if syncAggregate.SyncCommitteeBits.BitAt(i) {
res.WriteString(fmt.Sprintf(" %d", syncCommittee.Validators[i]))
}
}
res.WriteString("\n")
}
}
}
}
}
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
@@ -224,7 +279,199 @@ func outputBlockText(ctx context.Context, data *dataOut, signedBlock *spec.Signe
res := strings.Builder{}
// General info.
tmp, err := outputBlockGeneral(ctx, data.verbose, signedBlock.Message, data.genesisTime, data.slotDuration, data.slotsPerEpoch)
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 = outputBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *altair.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phase0.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate block root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
@@ -272,11 +519,61 @@ func outputBlockText(ctx context.Context, data *dataOut, signedBlock *spec.Signe
return res.String(), nil
}
func outputBlockExecutionPayload(ctx context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
res := strings.Builder{}
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
if verbose {
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))
}
return res.String(), nil
}
// intersection returns a list of items common between the two sets.
func intersection(set1 []uint64, set2 []uint64) []spec.ValidatorIndex {
func intersection(set1 []uint64, set2 []uint64) []phase0.ValidatorIndex {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
res := make([]spec.ValidatorIndex, 0)
res := make([]phase0.ValidatorIndex, 0)
set1Pos := 0
set2Pos := 0
@@ -287,7 +584,7 @@ func intersection(set1 []uint64, set2 []uint64) []spec.ValidatorIndex {
case set2[set2Pos] < set1[set1Pos]:
set2Pos++
default:
res = append(res, spec.ValidatorIndex(set1[set1Pos]))
res = append(res, phase0.ValidatorIndex(set1[set1Pos]))
set1Pos++
set2Pos++
}
@@ -296,7 +593,7 @@ func intersection(set1 []uint64, set2 []uint64) []spec.ValidatorIndex {
return res
}
func bitsToString(input bitfield.Bitlist) string {
func bitlistToString(input bitfield.Bitlist) string {
bits := int(input.Len())
res := ""
@@ -313,7 +610,24 @@ func bitsToString(input bitfield.Bitlist) string {
return strings.TrimSpace(res)
}
func attestingIndices(input bitfield.Bitlist, indices []spec.ValidatorIndex) string {
func bitvectorToString(input bitfield.Bitvector512) string {
bits := int(input.Len())
res := strings.Builder{}
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res.WriteString("✓")
} else {
res.WriteString("✕")
}
if i%8 == 7 && i != bits-1 {
res.WriteString(" ")
}
}
return res.String()
}
func attestingIndices(input bitfield.Bitlist, indices []phase0.ValidatorIndex) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {

View File

@@ -21,11 +21,15 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/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/phase0"
"github.com/pkg/errors"
)
var jsonOutput bool
var sszOutput bool
var results *dataOut
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -55,13 +59,29 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
if err := outputBlock(ctx, data.jsonOutput, signedBlock); err != nil {
return nil, errors.Wrap(err, "failed to output block")
if signedBlock == nil {
return nil, errors.New("empty beacon block")
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
if data.stream {
jsonOutput = data.jsonOutput
sszOutput = data.sszOutput
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
@@ -81,14 +101,48 @@ 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 {
fmt.Printf("Failed to obtain block: %v\n", err)
if !jsonOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
if err := outputBlock(context.Background(), jsonOutput, signedBlock); err != nil {
fmt.Printf("Failed to display block: %v\n", err)
if signedBlock == nil {
if !jsonOutput {
fmt.Println("Empty beacon block")
}
return
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
if !jsonOutput {
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 {
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 {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
default:
if !jsonOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
return
}
}
func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedBeaconBlock) error {
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
@@ -97,7 +151,55 @@ func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedB
}
fmt.Printf("%s\n", string(data))
default:
data, err := outputBlockText(ctx, results, signedBlock)
data, err := outputPhase0BlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
}
return nil
}
func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *altair.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
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 := outputAltairBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
}
return nil
}
func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *bellatrix.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 := outputBellatrixBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}

View File

@@ -47,7 +47,7 @@ func TestProcess(t *testing.T) {
dataIn: &dataIn{
eth2Client: eth2Client,
},
err: "failed to output block: failed to generate text: no block supplied",
err: "empty beacon block",
},
}

65
cmd/blockanalyze.go Normal file
View File

@@ -0,0 +1,65 @@
// 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"
blockanalyze "github.com/wealdtech/ethdo/cmd/block/analyze"
)
var blockAnalyzeCmd = &cobra.Command{
Use: "analyze",
Short: "Analyze a block",
Long: `Analyze the contents of a block. For example:
ethdo block analyze --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := blockanalyze.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Print(res)
}
return nil
},
}
func init() {
blockCmd.AddCommand(blockAnalyzeCmd)
blockFlags(blockAnalyzeCmd)
blockAnalyzeCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockAnalyzeCmd.Flags().Bool("json", false, "output data in JSON format")
}
func blockAnalyzeBindings() {
if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockAnalyzeCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -26,7 +26,7 @@ var blockInfoCmd = &cobra.Command{
Short: "Obtain information about a block",
Long: `Obtain information about a block. For example:
ethdo block info --slot=12345
ethdo block info --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -50,6 +50,7 @@ func init() {
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
func blockInfoBindings() {
@@ -62,4 +63,7 @@ func blockInfoBindings() {
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil {
panic(err)
}
}

81
cmd/chain/time/input.go Normal file
View File

@@ -0,0 +1,81 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
json bool
// Input
connection string
allowInsecureConnections bool
timestamp string
slot string
epoch string
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.json = viper.GetBool("json")
haveInput := false
if viper.GetString("timestamp") != "" {
data.timestamp = viper.GetString("timestamp")
haveInput = true
}
if viper.GetString("slot") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.slot = viper.GetString("slot")
haveInput = true
}
if viper.GetString("epoch") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.epoch = viper.GetString("epoch")
haveInput = true
}
if !haveInput {
return nil, errors.New("one of timestamp, slot or epoch required")
}
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
data.connection = viper.GetString("connection")
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return data, nil
}

View File

@@ -0,0 +1,97 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"slot": "1",
},
err: "connection is required",
},
{
name: "IDMissing",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "one of timestamp, slot or epoch required",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
}
})
}
}

82
cmd/chain/time/output.go Normal file
View File

@@ -0,0 +1,82 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"fmt"
"strings"
"time"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
epoch spec.Epoch
epochStart time.Time
epochEnd time.Time
slot spec.Slot
slotStart time.Time
slotEnd time.Time
syncCommitteePeriod uint64
syncCommitteePeriodStart time.Time
syncCommitteePeriodEpochStart spec.Epoch
syncCommitteePeriodEnd time.Time
syncCommitteePeriodEpochEnd spec.Epoch
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.quiet {
return "", nil
}
builder := strings.Builder{}
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d", data.epoch))
builder.WriteString("\n Epoch start ")
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Epoch end ")
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\nSlot ")
builder.WriteString(fmt.Sprintf("%d", data.slot))
builder.WriteString("\n Slot start ")
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\nSync committee period ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
builder.WriteString(")\n Sync committee period end ")
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
builder.WriteString(")\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,85 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
// import (
// "context"
// "testing"
//
// api "github.com/attestantio/go-eth2-client/api/v1"
// "github.com/stretchr/testify/require"
// "github.com/wealdtech/ethdo/testutil"
// )
//
// func TestOutput(t *testing.T) {
// tests := []struct {
// name string
// dataOut *dataOut
// res string
// err string
// }{
// {
// name: "Nil",
// err: "no data",
// },
// {
// name: "Empty",
// dataOut: &dataOut{},
// res: "No duties found",
// },
// {
// name: "Present",
// dataOut: &dataOut{
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: "Validator attesting in slot 1 committee 3",
// },
// {
// name: "JSON",
// dataOut: &dataOut{
// json: true,
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
// },
// }
//
// for _, test := range tests {
// t.Run(test.name, func(t *testing.T) {
// res, err := output(context.Background(), test.dataOut)
// if test.err != "" {
// require.EqualError(t, err, test.err)
// } else {
// require.NoError(t, err)
// require.Equal(t, test.res, res)
// }
// })
// }
// }

95
cmd/chain/time/process.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"strconv"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
epochsPerSyncCommitteePeriod := config["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"].(uint64)
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
// Calculate the slot given the input.
switch {
case data.slot != "":
slot, err := strconv.ParseUint(data.slot, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse slot")
}
results.slot = phase0.Slot(slot)
case data.epoch != "":
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse epoch")
}
results.slot = phase0.Slot(epoch * slotsPerEpoch)
case data.timestamp != "":
timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp)
if err != nil {
return nil, errors.Wrap(err, "failed to parse timestamp")
}
secs := timestamp.Sub(genesis.GenesisTime)
if secs < 0 {
return nil, errors.New("timestamp prior to genesis")
}
results.slot = phase0.Slot(secs / slotDuration)
}
// Fill in the info given the slot.
results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration)
results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration)
results.epoch = phase0.Epoch(uint64(results.slot) / slotsPerEpoch)
results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration)
results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration)
results.syncCommitteePeriod = uint64(results.epoch) / epochsPerSyncCommitteePeriod
results.syncCommitteePeriodEpochStart = phase0.Epoch(results.syncCommitteePeriod * epochsPerSyncCommitteePeriod)
results.syncCommitteePeriodEpochEnd = phase0.Epoch((results.syncCommitteePeriod+1)*epochsPerSyncCommitteePeriod) - 1
results.syncCommitteePeriodStart = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochStart)*slotsPerEpoch) * slotDuration)
results.syncCommitteePeriodEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochEnd)*slotsPerEpoch) * slotDuration)
return results, nil
}

View File

@@ -0,0 +1,120 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
dataIn *dataIn
expected *dataOut
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Slot",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
slot: "1",
},
expected: &dataOut{
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
},
},
{
name: "Epoch",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
epoch: "2",
},
expected: &dataOut{
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
},
},
{
name: "Timestamp",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
timestamp: "2021-01-01T00:00:00+0000",
},
expected: &dataOut{
epoch: 6862,
epochStart: time.Unix(1609459031, 0),
epochEnd: time.Unix(1609459415, 0),
slot: 219598,
slotStart: time.Unix(1609459199, 0),
slotEnd: time.Unix(1609459211, 0),
syncCommitteePeriod: 26,
syncCommitteePeriodStart: time.Unix(1609379927, 0),
syncCommitteePeriodEnd: time.Unix(1609477847, 0),
syncCommitteePeriodEpochStart: 6656,
syncCommitteePeriodEpochEnd: 6911,
},
},
}
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)
fmt.Printf("****** %d %d\n", res.syncCommitteePeriodStart.Unix(), res.syncCommitteePeriodEnd.Unix())
require.Equal(t, test.expected, res)
}
})
}
}

50
cmd/chain/time/run.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the wallet create data command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -0,0 +1,86 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
data string
item *altair.SignedContributionAndProof
// Data access.
eth2Client eth2client.Service
validatorsProvider eth2client.ValidatorsProvider
// Data.
spec map[string]interface{}
validator *api.Validator
syncCommittee *api.SyncCommittee
// Output.
itemStructureValid bool
validatorKnown bool
validatorInSyncCommittee bool
validatorIsAggregator bool
contributionSignatureValidFormat bool
contributionAndProofSignatureValidFormat bool
contributionAndProofSignatureValid bool
additionalInfo string
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("data") == "" {
return nil, errors.New("data is required")
}
c.data = viper.GetString("data")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return c, nil
}

View File

@@ -0,0 +1,80 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "DataMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "data is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"data": "{}",
},
err: "connection is required",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"data": "{}",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
_, err := newCommand(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,127 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
builder := strings.Builder{}
builder.WriteString("Valid data structure: ")
if c.itemStructureValid {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Validator known: ")
if c.validatorKnown {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Validator in sync committee: ")
if c.validatorInSyncCommittee {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Validator is aggregator: ")
if c.validatorIsAggregator {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Contribution signature has valid format: ")
if c.contributionSignatureValidFormat {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Contribution and proof signature has valid format: ")
if c.contributionAndProofSignatureValidFormat {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Contribution and proof signature is valid: ")
if c.contributionAndProofSignatureValid {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
return builder.String(), nil
}

View File

@@ -0,0 +1,303 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"fmt"
"os"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func (c *command) process(ctx context.Context) error {
// Parse the data.
if c.data == "" {
return errors.New("no data supplied")
}
c.item = &altair.SignedContributionAndProof{}
err := json.Unmarshal([]byte(c.data), c.item)
if err != nil {
c.additionalInfo = err.Error()
return nil
}
c.itemStructureValid = true
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
for _, validatorIndex := range c.syncCommittee.Validators {
if validatorIndex == c.item.Message.AggregatorIndex {
c.validatorInSyncCommittee = true
break
}
}
if !c.validatorInSyncCommittee {
return nil
}
// Ensure the validator is an aggregator.
isAggregator, err := c.isAggregator(ctx)
if err != nil {
return errors.Wrap(err, "failed to ascertain if sync committee member is aggregator")
}
if !isAggregator {
return nil
}
c.validatorIsAggregator = true
// Confirm the contribution signature.
if err := c.confirmContributionSignature(ctx); err != nil {
return errors.Wrap(err, "failed to confirm the contribution signature")
}
// Confirm the contribution and proof signature.
if err := c.confirmContributionAndProofSignature(ctx); err != nil {
return errors.Wrap(err, "failed to confirm the contribution and proof signature")
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
// Obtain the validator.
var isProvider bool
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validator information")
}
stateID := fmt.Sprintf("%d", c.item.Message.Contribution.Slot)
validators, err := c.validatorsProvider.Validators(ctx,
stateID,
[]phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
)
if err != nil {
return errors.Wrap(err, "failed to obtain validator information")
}
if len(validators) == 0 || validators[c.item.Message.AggregatorIndex] == nil {
return nil
}
c.validatorKnown = true
c.validator = validators[c.item.Message.AggregatorIndex]
// Obtain the sync committee
syncCommitteesProvider, isProvider := c.eth2Client.(eth2client.SyncCommitteesProvider)
if !isProvider {
return errors.New("connection does not provide sync committee information")
}
c.syncCommittee, err = syncCommitteesProvider.SyncCommittee(ctx, stateID)
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
return nil
}
// isAggregator returns true if the given
func (c *command) isAggregator(ctx context.Context) (bool, error) {
// Calculate the modulo.
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
if !isProvider {
return false, errors.New("connection does not provide spec information")
}
var err error
c.spec, err = specProvider.Spec(ctx)
if err != nil {
return false, errors.Wrap(err, "failed to obtain spec information")
}
tmp, exists := c.spec["SYNC_COMMITTEE_SIZE"]
if !exists {
return false, errors.New("spec does not contain SYNC_COMMITTEE_SIZE")
}
if _, isUint64 := tmp.(uint64); !isUint64 {
return false, errors.New("spec returned non-integer value for SYNC_COMMITTEE_SIZE")
}
syncCommitteeSize := tmp.(uint64)
if c.debug {
fmt.Fprintf(os.Stderr, "sync committee size is %d\n", syncCommitteeSize)
}
tmp, exists = c.spec["SYNC_COMMITTEE_SUBNET_COUNT"]
if !exists {
return false, errors.New("spec does not contain SYNC_COMMITTEE_SUBNET_COUNT")
}
if _, isUint64 := tmp.(uint64); !isUint64 {
return false, errors.New("spec returned non-integer value for SYNC_COMMITTEE_SUBNET_COUNT")
}
syncCommitteeSubnetCount := tmp.(uint64)
if c.debug {
fmt.Fprintf(os.Stderr, "sync committee subnet count is %d\n", syncCommitteeSubnetCount)
}
tmp, exists = c.spec["TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE"]
if !exists {
return false, errors.New("spec does not contain TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE")
}
if _, isUint64 := tmp.(uint64); !isUint64 {
return false, errors.New("spec returned non-integer value for TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE")
}
targetAggregatorsPerSyncSubcommittee := tmp.(uint64)
if c.debug {
fmt.Fprintf(os.Stderr, "target aggregators per sync subcommittee is %d\n", targetAggregatorsPerSyncSubcommittee)
}
modulo := syncCommitteeSize / syncCommitteeSubnetCount / targetAggregatorsPerSyncSubcommittee
if modulo < 1 {
modulo = 1
}
if c.debug {
fmt.Fprintf(os.Stderr, "modulo is %d\n", modulo)
}
// Hash the selection proof.
sigHash := sha256.New()
n, err := sigHash.Write(c.item.Message.SelectionProof[:])
if err != nil {
return false, errors.Wrap(err, "failed to hash the selection proof")
}
if n != len(c.item.Signature[:]) {
return false, errors.New("failed to write all bytes of the selection proof to the hash")
}
hash := sigHash.Sum(nil)
if c.debug {
fmt.Fprintf(os.Stderr, "hash of selection proof is %#x\n", hash)
}
return binary.LittleEndian.Uint64(hash[:8])%modulo == 0, nil
}
func (c *command) confirmContributionSignature(ctx context.Context) error {
sigBytes := make([]byte, 96)
copy(sigBytes, c.item.Message.Contribution.Signature[:])
_, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
return nil
}
c.contributionSignatureValidFormat = true
subCommittee := c.syncCommittee.ValidatorAggregates[c.item.Message.Contribution.SubcommitteeIndex]
includedIndices := make([]phase0.ValidatorIndex, 0, len(subCommittee))
for i := uint64(0); i < c.item.Message.Contribution.AggregationBits.Len(); i++ {
if c.item.Message.Contribution.AggregationBits.BitAt(i) {
includedIndices = append(includedIndices, subCommittee[int(i)])
}
}
if c.debug {
fmt.Fprintf(os.Stderr, "Contribution validator indices: %v (%d)\n", includedIndices, len(includedIndices))
}
includedValidators, err := c.validatorsProvider.Validators(ctx, "head", includedIndices)
if err != nil {
return errors.Wrap(err, "failed to obtain subcommittee validators")
}
if len(includedValidators) == 0 {
return errors.New("obtained empty subcommittee validator list")
}
var aggregatePubKey *e2types.BLSPublicKey
for _, v := range includedValidators {
pubKeyBytes := make([]byte, 48)
copy(pubKeyBytes, v.Validator.PublicKey[:])
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
if err != nil {
return errors.Wrap(err, "failed to aggregate public key")
}
if aggregatePubKey == nil {
aggregatePubKey = pubKey
} else {
aggregatePubKey.Aggregate(pubKey)
}
}
if c.debug {
fmt.Fprintf(os.Stderr, "Aggregate public key is %#x\n", aggregatePubKey.Marshal())
}
// Don't have the ability to carry out the batch verification at current.
return nil
}
func (c *command) confirmContributionAndProofSignature(ctx context.Context) error {
sigBytes := make([]byte, 96)
copy(sigBytes, c.item.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
return nil
}
c.contributionAndProofSignatureValidFormat = true
pubKeyBytes := make([]byte, 48)
copy(pubKeyBytes, c.validator.Validator.PublicKey[:])
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
if err != nil {
return errors.Wrap(err, "failed to configure public key")
}
objectRoot, err := c.item.Message.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to obtain signging root")
}
tmp, exists := c.spec["DOMAIN_CONTRIBUTION_AND_PROOF"]
if !exists {
return errors.New("spec does not contain DOMAIN_CONTRIBUTION_AND_PROOF")
}
if _, isUint64 := tmp.(phase0.DomainType); !isUint64 {
return errors.New("spec returned non-domain type value for DOMAIN_CONTRIBUTION_AND_PROOF")
}
contributionAndProofDomainType := tmp.(phase0.DomainType)
if c.debug {
fmt.Fprintf(os.Stderr, "contribution and proof domain type is %#x\n", contributionAndProofDomainType)
}
domain, err := c.eth2Client.(eth2client.DomainProvider).Domain(ctx, contributionAndProofDomainType, phase0.Epoch(c.item.Message.Contribution.Slot/32))
if err != nil {
return errors.Wrap(err, "failed to obtain domain")
}
container := &phase0.SigningData{
ObjectRoot: objectRoot,
Domain: domain,
}
signingRoot, err := container.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to obtain signging root")
}
c.contributionAndProofSignatureValid = sig.Verify(signingRoot[:], pubKey)
return nil
}

View File

@@ -0,0 +1,62 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
vars map[string]interface{}
err string
}{
{
name: "InvalidData",
vars: map[string]interface{}{
"timeout": "5s",
"data": "[[",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
cmd, err := newCommand(context.Background())
require.NoError(t, err)
err = cmd.process(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -46,6 +46,9 @@ In quiet mode this will return 0 if the chain information can be obtained, other
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain beacon chain genesis")
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
errCheck(err, "Failed to obtain current fork")
if quiet {
os.Exit(_exitSuccess)
}
@@ -57,7 +60,20 @@ In quiet mode this will return 0 if the chain information can be obtained, other
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix()))
}
fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot)
fmt.Printf("Genesis fork version: %x\n", config["GENESIS_FORK_VERSION"].(spec.Version))
fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].(spec.Version))
fmt.Printf("Current fork version: %#x\n", fork.CurrentVersion)
if verbose {
forkData := &spec.ForkData{
CurrentVersion: fork.CurrentVersion,
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
}
forkDataRoot, err := forkData.HashTreeRoot()
if err == nil {
var forkDigest spec.ForkDigest
copy(forkDigest[:], forkDataRoot[:])
fmt.Printf("Fork digest: %#x\n", forkDigest)
}
}
fmt.Printf("Seconds per slot: %d\n", int(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64))
@@ -69,18 +85,3 @@ func init() {
chainCmd.AddCommand(chainInfoCmd)
chainFlags(chainInfoCmd)
}
func timestampToSlot(genesis time.Time, timestamp time.Time, secondsPerSlot time.Duration) spec.Slot {
if timestamp.Unix() < genesis.Unix() {
return 0
}
return spec.Slot(uint64(timestamp.Unix()-genesis.Unix()) / uint64(secondsPerSlot.Seconds()))
}
func slotToTimestamp(genesis time.Time, slot spec.Slot, slotDuration time.Duration) int64 {
return genesis.Unix() + int64(slot)*int64(slotDuration.Seconds())
}
func epochToTimestamp(genesis time.Time, slot spec.Slot, slotDuration time.Duration, slotsPerEpoch uint64) int64 {
return genesis.Unix() + int64(slot)*int64(slotDuration.Seconds())*int64(slotsPerEpoch)
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -17,12 +17,13 @@ import (
"context"
"fmt"
"os"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "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"
)
@@ -40,46 +41,116 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
errCheck(err, "Failed to obtain beacon chain specification")
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")
finality, err := eth2Client.(eth2client.FinalityProvider).Finality(ctx, "head")
finalityProvider, isProvider := eth2Client.(eth2client.FinalityProvider)
assert(isProvider, "beacon node does not provide finality; cannot report on chain status")
finality, err := finalityProvider.Finality(ctx, "head")
errCheck(err, "Failed to obtain finality information")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
errCheck(err, "Failed to obtain genesis information")
slot := chainTime.CurrentSlot()
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
curSlot := timestampToSlot(genesis.GenesisTime, time.Now(), slotDuration)
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
curEpoch := spec.Epoch(uint64(curSlot) / slotsPerEpoch)
fmt.Printf("Current epoch: %d\n", curEpoch)
fmt.Printf("Justified epoch: %d\n", finality.Justified.Epoch)
if verbose {
distance := curEpoch - finality.Justified.Epoch
fmt.Printf("Justified epoch distance: %d\n", distance)
}
fmt.Printf("Finalized epoch: %d\n", finality.Finalized.Epoch)
if verbose {
distance := curEpoch - finality.Finalized.Epoch
fmt.Printf("Finalized epoch distance: %d\n", distance)
}
if verbose {
fmt.Printf("Prior justified epoch: %d\n", finality.PreviousJustified.Epoch)
distance := curEpoch - finality.PreviousJustified.Epoch
fmt.Printf("Prior justified epoch distance: %d\n", distance)
}
nextSlot := slot + 1
nextSlotTimestamp := chainTime.StartOfSlot(nextSlot)
epoch := chainTime.CurrentEpoch()
epochStartSlot := chainTime.FirstSlotOfEpoch(epoch)
epochEndSlot := chainTime.FirstSlotOfEpoch(epoch+1) - 1
nextEpoch := epoch + 1
nextEpochStartSlot := chainTime.FirstSlotOfEpoch(nextEpoch)
nextEpochTimestamp := chainTime.StartOfEpoch(nextEpoch)
res := strings.Builder{}
res.WriteString("Current slot: ")
res.WriteString(fmt.Sprintf("%d", slot))
res.WriteString("\n")
res.WriteString("Current epoch: ")
res.WriteString(fmt.Sprintf("%d", epoch))
res.WriteString("\n")
if verbose {
epochStartSlot := (uint64(curSlot) / slotsPerEpoch) * slotsPerEpoch
fmt.Printf("Epoch slots: %d-%d\n", epochStartSlot, epochStartSlot+slotsPerEpoch-1)
nextSlotTimestamp := slotToTimestamp(genesis.GenesisTime, curSlot+1, slotDuration)
fmt.Printf("Time until next slot: %2.1fs\n", float64(time.Until(time.Unix(nextSlotTimestamp, 0)).Milliseconds())/1000)
nextEpoch := epochToTimestamp(genesis.GenesisTime, spec.Slot(uint64(curSlot)/slotsPerEpoch+1), slotDuration, slotsPerEpoch)
fmt.Printf("Slots until next epoch: %d\n", (uint64(curSlot)/slotsPerEpoch+1)*slotsPerEpoch-uint64(curSlot))
fmt.Printf("Time until next epoch: %2.1fs\n", float64(time.Until(time.Unix(nextEpoch, 0)).Milliseconds())/1000)
res.WriteString("Epoch slots: ")
res.WriteString(fmt.Sprintf("%d", epochStartSlot))
res.WriteString("-")
res.WriteString(fmt.Sprintf("%d", epochEndSlot))
res.WriteString("\n")
}
res.WriteString("Time until next slot: ")
res.WriteString(time.Until(nextSlotTimestamp).Round(time.Second).String())
res.WriteString("\n")
res.WriteString("Time until next epoch: ")
res.WriteString(time.Until(nextEpochTimestamp).Round(time.Second).String())
res.WriteString("\n")
res.WriteString("Slots until next epoch: ")
res.WriteString(fmt.Sprintf("%d", nextEpochStartSlot-slot))
res.WriteString("\n")
res.WriteString("Justified epoch: ")
res.WriteString(fmt.Sprintf("%d", finality.Justified.Epoch))
res.WriteString("\n")
if verbose {
distance := epoch - finality.Justified.Epoch
res.WriteString("Justified epoch distance: ")
res.WriteString(fmt.Sprintf("%d", distance))
res.WriteString("\n")
}
res.WriteString("Finalized epoch: ")
res.WriteString(fmt.Sprintf("%d", finality.Finalized.Epoch))
res.WriteString("\n")
if verbose {
distance := epoch - finality.Finalized.Epoch
res.WriteString("Finalized epoch distance: ")
res.WriteString(fmt.Sprintf("%d", distance))
res.WriteString("\n")
}
if epoch >= chainTime.AltairInitialEpoch() {
period := chainTime.SlotToSyncCommitteePeriod(slot)
periodStartEpoch := chainTime.FirstEpochOfSyncPeriod(period)
periodStartSlot := chainTime.FirstSlotOfEpoch(periodStartEpoch)
nextPeriod := period + 1
nextPeriodStartEpoch := chainTime.FirstEpochOfSyncPeriod(nextPeriod)
periodEndEpoch := nextPeriodStartEpoch - 1
periodEndSlot := chainTime.FirstSlotOfEpoch(periodEndEpoch+1) - 1
nextPeriodTimestamp := chainTime.StartOfEpoch(nextPeriodStartEpoch)
res.WriteString("Sync committee period: ")
res.WriteString(fmt.Sprintf("%d", period))
res.WriteString("\n")
if verbose {
res.WriteString("Sync committee epochs: ")
res.WriteString(fmt.Sprintf("%d", periodStartEpoch))
res.WriteString("-")
res.WriteString(fmt.Sprintf("%d", periodEndEpoch))
res.WriteString("\n")
res.WriteString("Sync committee slots: ")
res.WriteString(fmt.Sprintf("%d", periodStartSlot))
res.WriteString("-")
res.WriteString(fmt.Sprintf("%d", periodEndSlot))
res.WriteString("\n")
res.WriteString("Time until next sync committee period: ")
res.WriteString(time.Until(nextPeriodTimestamp).Round(time.Second).String())
res.WriteString("\n")
}
}
fmt.Print(res.String())
os.Exit(_exitSuccess)
},
}

60
cmd/chaintime.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
chaintime "github.com/wealdtech/ethdo/cmd/chain/time"
)
var chainTimeCmd = &cobra.Command{
Use: "time",
Short: "Obtain info about the chain at a given time",
Long: `Obtain info about the chain at a given time. For example:
ethdo chain time --slot=12345`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chaintime.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Print(res)
}
return nil
},
}
func init() {
chainCmd.AddCommand(chainTimeCmd)
chainFlags(chainTimeCmd)
chainTimeCmd.Flags().String("slot", "", "The slot for which to obtain information")
chainTimeCmd.Flags().String("epoch", "", "The epoch for which to obtain information")
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS+ZZZZ)")
}
func chainTimeBindings() {
if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil {
panic(err)
}
}

45
cmd/chainverify.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// chainVerifyCmd represents the chain verify command
var chainVerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify a beacon chain signature",
Long: "Verify the signature for a given beacon chain structure is correct",
}
func init() {
chainCmd.AddCommand(chainVerifyCmd)
}
func chainVerifyFlags(cmd *cobra.Command) {
chainFlags(cmd)
cmd.Flags().String("validator", "", "The account, public key or index of the validator")
cmd.Flags().String("data", "", "The data to verify, as a JSON structure")
}
func chainVerifyBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
panic(err)
}
if err := viper.BindPFlag("data", cmd.Flags().Lookup("data")); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
chainverifysignedcontributionandproof "github.com/wealdtech/ethdo/cmd/chain/verify/signedcontributionandproof"
)
var chainVerifySignedContributionAndProofCmd = &cobra.Command{
Use: "signedcontributionandproof",
Short: "Verify a signed contribution and proof",
Long: `Verify a signed contribution and proof. For example:
ethdo chain verify signedcontributionandproof --data=... --validator=...
validator can be an account, a public key or an index.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chainverifysignedcontributionandproof.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Print(res)
}
return nil
},
}
func init() {
chainVerifyCmd.AddCommand(chainVerifySignedContributionAndProofCmd)
chainVerifyFlags(chainVerifySignedContributionAndProofCmd)
}
func chainVerifySignedContributionAndProofBindings(cmd *cobra.Command) {
chainVerifyBindings(cmd)
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019-2021 Weald Technology Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -32,6 +32,7 @@ import (
var depositVerifyData string
var depositVerifyWithdrawalPubKey string
var depositVerifyWithdrawalAddress string
var depositVerifyValidatorPubKey string
var depositVerifyDepositAmount string
var depositVerifyForkVersion string
@@ -81,7 +82,14 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis
withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
errCheck(err, "Value supplied with --withdrawalpubkey is not a valid public key")
withdrawalCredentials = eth2util.SHA256(withdrawalPubKey.Marshal())
withdrawalCredentials[0] = 0 // BLS_WITHDRAWAL_PREFIX
withdrawalCredentials[0] = 0x00 // BLS_WITHDRAWAL_PREFIX
} else if depositVerifyWithdrawalAddress != "" {
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(depositVerifyWithdrawalAddress, "0x"))
errCheck(err, "Invalid withdrawal address")
assert(len(withdrawalAddressBytes) == 20, "address should be 20 bytes")
withdrawalCredentials = make([]byte, 32)
withdrawalCredentials[0] = 0x01 // ETH1_ADDRESS_WITHDRAWAL_PREFIX
copy(withdrawalCredentials[12:], withdrawalAddressBytes)
}
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
@@ -181,10 +189,10 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, validatorPubKeys map[[48]byte]bool, amount uint64) (bool, error) {
if withdrawalCredentials == nil {
outputIf(!quiet, "Withdrawal public key not supplied; withdrawal credentials NOT checked")
outputIf(!quiet, "Withdrawal public key or address not supplied; withdrawal credentials NOT checked")
} else {
if !bytes.Equal(deposit.WithdrawalCredentials, withdrawalCredentials) {
outputIf(!quiet, "Withdrawal public key incorrect")
outputIf(!quiet, "Withdrawal credentials incorrect")
return false, nil
}
outputIf(!quiet, "Withdrawal credentials verified")
@@ -246,7 +254,7 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
if err != nil {
return false, errors.Wrap(err, "failed to decode fork version")
}
if bytes.Equal(deposit.ForkVersion, forkVersion[:]) {
if bytes.Equal(deposit.ForkVersion, forkVersion) {
outputIf(!quiet, "Fork version verified")
} else {
outputIf(!quiet, "Fork version incorrect")
@@ -263,6 +271,7 @@ func init() {
depositFlags(depositVerifyCmd)
depositVerifyCmd.Flags().StringVar(&depositVerifyData, "data", "", "JSON data, or path to JSON data")
depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalPubKey, "withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn")
depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalAddress, "withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn")
depositVerifyCmd.Flags().StringVar(&depositVerifyDepositAmount, "depositvalue", "32 Ether", "Value of the amount to be deposited")
depositVerifyCmd.Flags().StringVar(&depositVerifyValidatorPubKey, "validatorpubkey", "", "Public key(s) of the account(s) that will be carrying out validation")
depositVerifyCmd.Flags().StringVar(&depositVerifyForkVersion, "forkversion", "0x00000000", "Fork version of the chain of the deposit")

View File

@@ -70,7 +70,9 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis
}
exitRoot, err := exit.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
sig, err := e2types.BLSSignatureFromBytes(data.Exit.Signature[:])
signatureBytes := make([]byte, 96)
copy(signatureBytes, data.Exit.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
errCheck(err, "Invalid signature")
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
errCheck(err, "Failed to verify voluntary exit")

View File

@@ -52,7 +52,7 @@ func input(ctx context.Context) (*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")
return nil, err
}
return data, nil

View File

@@ -66,7 +66,7 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
name: "ConnectionBad",
@@ -75,7 +75,7 @@ func TestInput(t *testing.T) {
"connection": "localhost:1",
"topics": []string{"one", "two"},
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
name: "TopicsNil",

View File

@@ -23,8 +23,6 @@ import (
"github.com/pkg/errors"
)
var jsonOutput bool
func process(ctx context.Context, data *dataIn) error {
if data == nil {
return errors.New("no data")

View File

@@ -27,6 +27,3 @@ var proposerCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(proposerCmd)
}
func proposerFlags(cmd *cobra.Command) {
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 Weald Technology Trading
// Copyright © 2019 - 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -23,6 +23,7 @@ import (
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
@@ -56,12 +57,28 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
return nil
}
// Disable service logging.
zerolog.SetGlobalLevel(zerolog.Disabled)
// We bind viper here so that we bind to the correct command.
quiet = viper.GetBool("quiet")
verbose = viper.GetBool("verbose")
debug = viper.GetBool("debug")
// Command-specific bindings.
switch fmt.Sprintf("%s/%s", cmd.Parent().Name(), cmd.Name()) {
includeCommandBindings(cmd)
if quiet && verbose {
fmt.Println("Cannot supply both quiet and verbose flags")
}
if quiet && debug {
fmt.Println("Cannot supply both quiet and debug flags")
}
return util.SetupStore()
}
func includeCommandBindings(cmd *cobra.Command) {
switch commandPath(cmd) {
case "account/create":
accountCreateBindings()
case "account/derive":
@@ -72,14 +89,24 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
attesterDutiesBindings()
case "attester/inclusion":
attesterInclusionBindings()
case "block/analyze":
blockAnalyzeBindings()
case "block/info":
blockInfoBindings()
case "chain/time":
chainTimeBindings()
case "chain/verify/signedcontributionandproof":
chainVerifySignedContributionAndProofBindings(cmd)
case "exit/verify":
exitVerifyBindings()
case "node/events":
nodeEventsBindings()
case "slot/time":
slotTimeBindings()
case "synccommittee/inclusion":
synccommitteeInclusionBindings()
case "synccommittee/members":
synccommitteeMembersBindings()
case "validator/depositdata":
validatorDepositdataBindings()
case "validator/duties":
@@ -88,24 +115,19 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
validatorExitBindings()
case "validator/info":
validatorInfoBindings()
case "validator/keycheck":
validatorKeycheckBindings()
case "validator/expectation":
validatorExpectationBindings()
case "wallet/create":
walletCreateBindings()
case "wallet/import":
walletImportBindings()
case "wallet/sharedexport":
walletSharedExportBindings()
case "wallet/sharedimport":
walletSharedImportBindings()
}
if quiet && verbose {
fmt.Println("Cannot supply both quiet and verbose flags")
}
if quiet && debug {
fmt.Println("Cannot supply both quiet and debug flags")
}
if err := util.SetupStore(); err != nil {
return err
}
return nil
}
// Execute adds all child commands to the root command and sets flags appropriately.
@@ -187,7 +209,7 @@ func init() {
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to an Ethereum 2 node")
RootCmd.PersistentFlags().String("connection", "http://localhost:3500", "URL to an Ethereum 2 node's RET API endpoint")
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
panic(err)
}
@@ -379,3 +401,14 @@ func remotesToEndpoints(remotes []string) ([]*dirk.Endpoint, error) {
func relockAccount(locker e2wtypes.AccountLocker) {
errCheck(locker.Lock(context.Background()), "failed to re-lock account")
}
func commandPath(cmd *cobra.Command) string {
path := ""
for {
path = fmt.Sprintf("%s/%s", cmd.Name(), path)
if cmd.Parent().Name() == "ethdo" {
return strings.TrimRight(path, "/")
}
cmd = cmd.Parent()
}
}

View File

@@ -54,7 +54,7 @@ func input(ctx context.Context) (*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")
return nil, err
}
return data, nil

View File

@@ -73,7 +73,7 @@ func TestInput(t *testing.T) {
"timeout": "5s",
"slot": "1",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified",
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
}

View File

@@ -40,5 +40,5 @@ func output(ctx context.Context, data *dataOut) (string, error) {
if data.verbose {
return fmt.Sprintf("%s - %s", data.startTime, data.endTime), nil
}
return fmt.Sprintf("%s", data.startTime), nil
return data.startTime.String(), nil
}

View File

@@ -53,7 +53,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
results.startTime = genesis.GenesisTime.Add(time.Duration((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second)))
results.startTime = genesis.GenesisTime.Add((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second))
results.endTime = results.startTime.Add(slotDuration)
return results, nil

32
cmd/synccommittee.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"github.com/spf13/cobra"
)
// synccommitteeCmd represents the synccommittee command
var synccommitteeCmd = &cobra.Command{
Use: "synccommittee",
Short: "Obtain information about Ethereum 2 sync committees",
Long: "Obtain information about Ethereum 2 sync committees",
}
func init() {
RootCmd.AddCommand(synccommitteeCmd)
}
func synccommitteeFlags(cmd *cobra.Command) {
}

View 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 inclusion
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
account string
pubKey string
index string
epoch int64
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
// Output.
inCommittee bool
committeeIndex uint64
inclusions []int
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
// Validator.
c.account = viper.GetString("account")
c.pubKey = viper.GetString("pubkey")
c.index = viper.GetString("index")
if c.account == "" && c.pubKey == "" && c.index == "" {
return nil, errors.New("account, pubkey or index required")
}
// Epoch.
c.epoch = viper.GetInt64("epoch")
return c, nil
}

View File

@@ -0,0 +1,82 @@
// 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 inclusion
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: "ConnectionMissing",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
},
err: "connection is required",
},
{
name: "NoValidator",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "account, pubkey or index required",
},
{
name: "Good",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
"index": "1",
"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)
}
})
}
}

View File

@@ -0,0 +1,81 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package inclusion
import (
"context"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
builder := strings.Builder{}
if c.verbose {
builder.WriteString("Epoch: ")
builder.WriteString(fmt.Sprintf("%d\n", c.epoch))
}
if !c.inCommittee {
builder.WriteString("Validator not in sync committee")
} else {
if c.verbose {
builder.WriteString("Validator sync committee index ")
builder.WriteString(fmt.Sprintf("%d\n", c.committeeIndex))
}
noBlock := 0
included := 0
missed := 0
for _, inclusion := range c.inclusions {
switch inclusion {
case 0:
noBlock++
case 1:
included++
case 2:
missed++
}
}
builder.WriteString("Expected: ")
builder.WriteString(fmt.Sprintf("%d", len(c.inclusions)))
builder.WriteString("\nIncluded: ")
builder.WriteString(fmt.Sprintf("%d", included))
builder.WriteString("\nMissed: ")
builder.WriteString(fmt.Sprintf("%d", missed))
builder.WriteString("\nNo block: ")
builder.WriteString(fmt.Sprintf("%d", noBlock))
builder.WriteString("\nPer-slot result: ")
for i, inclusion := range c.inclusions {
switch inclusion {
case 0:
builder.WriteString("-")
case 1:
builder.WriteString("✓")
case 2:
builder.WriteString("✕")
}
if i%8 == 7 && i != len(c.inclusions)-1 {
builder.WriteString(" ")
}
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,130 @@
// 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 inclusion
import (
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
firstSlot, lastSlot := c.calculateSlots(ctx)
validatorIndex, err := util.ValidatorIndex(ctx, c.eth2Client, c.account, c.pubKey, c.index)
if err != nil {
return err
}
syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", phase0.Epoch(c.epoch))
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
if syncCommittee == nil {
return errors.New("no sync committee returned")
}
for i := range syncCommittee.Validators {
if syncCommittee.Validators[i] == validatorIndex {
c.inCommittee = true
c.committeeIndex = uint64(i)
break
}
}
if c.inCommittee {
// This validator is in the sync committee. Check blocks to see where it has been included.
c.inclusions = make([]int, 0)
if lastSlot > c.chainTime.CurrentSlot() {
lastSlot = c.chainTime.CurrentSlot()
}
for slot := firstSlot; slot < lastSlot; slot++ {
block, err := c.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return err
}
if block == nil {
c.inclusions = append(c.inclusions, 0)
continue
}
var aggregate *altair.SyncAggregate
switch block.Version {
case spec.DataVersionAltair:
aggregate = block.Altair.Message.Body.SyncAggregate
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
c.inclusions = append(c.inclusions, 1)
} else {
c.inclusions = append(c.inclusions, 2)
}
case spec.DataVersionBellatrix:
aggregate = block.Bellatrix.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)
}
}
}
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 err
}
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 {
return errors.Wrap(err, "failed to set up chaintime service")
}
return nil
}
func (c *command) calculateSlots(ctx context.Context) (phase0.Slot, phase0.Slot) {
var firstSlot phase0.Slot
var lastSlot phase0.Slot
if c.epoch == -1 {
c.epoch = int64(c.chainTime.CurrentEpoch()) - 1
}
firstSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch))
lastSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch) + 1)
return firstSlot, lastSlot
}

View File

@@ -0,0 +1,72 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package inclusion
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: "InvalidConnection",
vars: map[string]interface{}{
"timeout": "5s",
"index": "1",
"connection": "invalid",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"index": "1",
"epoch": "-1",
"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)
}
})
}
}

View 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 inclusion
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
}

View File

@@ -0,0 +1,74 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
// Operation.
eth2Client eth2client.Service
chainTime chaintime.Service
epoch int64
period string
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, err
}
// 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 {
return nil, errors.Wrap(err, "failed to configure chaintime service")
}
// Epoch
data.epoch = viper.GetInt64("epoch")
data.period = viper.GetString("period")
return data, nil
}

View File

@@ -0,0 +1,88 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method",
},
}
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)
}
})
}
}

View File

@@ -0,0 +1,61 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
json bool
validators []phase0.ValidatorIndex
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.quiet {
return "", nil
}
if data.validators == nil {
return "No sync committee validators found", nil
}
if data.json {
bytes, err := json.Marshal(data.validators)
if err != nil {
return "", errors.Wrap(err, "failed to marshal JSON")
}
return string(bytes), nil
}
validators := make([]string, len(data.validators))
for i := range data.validators {
validators[i] = fmt.Sprintf("%d", data.validators[i])
}
return strings.Join(validators, ","), nil
}

View File

@@ -0,0 +1,68 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"testing"
"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: "Empty",
dataOut: &dataOut{},
res: "No sync committee validators found",
},
{
name: "Present",
dataOut: &dataOut{
validators: []phase0.ValidatorIndex{1, 2, 3},
},
res: "1,2,3",
},
{
name: "JSON",
dataOut: &dataOut{
json: true,
validators: []phase0.ValidatorIndex{1, 2, 3},
},
res: "[1,2,3]",
},
}
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)
}
})
}
}

View File

@@ -0,0 +1,77 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"fmt"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
epoch, err := calculateEpoch(ctx, data)
if err != nil {
return nil, err
}
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", epoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain sync committee information")
}
if syncCommittee == nil {
return nil, errors.New("no sync committee returned")
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
validators: syncCommittee.Validators,
}
return results, nil
}
func calculateEpoch(ctx context.Context, data *dataIn) (phase0.Epoch, error) {
var epoch phase0.Epoch
if data.epoch != -1 {
epoch = phase0.Epoch(data.epoch)
} else {
switch strings.ToLower(data.period) {
case "", "current":
epoch = data.chainTime.CurrentEpoch()
case "next":
period := data.chainTime.SlotToSyncCommitteePeriod(data.chainTime.CurrentSlot())
nextPeriod := period + 1
epoch = data.chainTime.FirstEpochOfSyncPeriod(nextPeriod)
default:
return 0, fmt.Errorf("period %s not known", data.period)
}
}
if data.debug {
fmt.Printf("epoch is %d\n", epoch)
}
return epoch, nil
}

View File

@@ -0,0 +1,74 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"os"
"testing"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
eth2Client, err := auto.New(context.Background(),
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
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)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Good",
dataIn: &dataIn{
eth2Client: eth2Client,
chainTime: chainTime,
epoch: -1,
},
},
}
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)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the wallet create data command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -0,0 +1,67 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
synccommitteeinclusion "github.com/wealdtech/ethdo/cmd/synccommittee/inclusion"
)
var synccommitteeInclusionCmd = &cobra.Command{
Use: "inclusion",
Short: "Obtain sync committee inclusion data for a validator",
Long: `Obtain sync committee inclusion data for a validator. For example:
ethdo synccommittee inclusion --epoch=12345 --index=11111
In quiet mode this will return 0 if the validator was in the sync committee, otherwise 1.
epoch can be a specific epoch; If not supplied all slots for the current sync committee period will be provided`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := synccommitteeinclusion.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
synccommitteeCmd.AddCommand(synccommitteeInclusionCmd)
synccommitteeFlags(synccommitteeInclusionCmd)
synccommitteeInclusionCmd.Flags().Int64("epoch", -1, "the epoch for which to fetch sync committee inclusion")
synccommitteeInclusionCmd.Flags().String("pubkey", "", "validator public key for sync committee")
synccommitteeInclusionCmd.Flags().String("index", "", "validator index for sync committee")
}
func synccommitteeInclusionBindings() {
if err := viper.BindPFlag("epoch", synccommitteeInclusionCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("pubkey", synccommitteeInclusionCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("index", synccommitteeInclusionCmd.Flags().Lookup("index")); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,63 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
synccommitteemembers "github.com/wealdtech/ethdo/cmd/synccommittee/members"
)
var synccommitteeMembersCmd = &cobra.Command{
Use: "members",
Short: "Obtain information about members of a synccommittee",
Long: `Obtain information about members of a synccommittee. For example:
ethdo synccommittee members --epoch=12345
In quiet mode this will return 0 if the synccommittee members are found, otherwise 1.
epoch can be a specific epoch. period can be 'current' for the current sync period or 'next' for the next sync period`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := synccommitteemembers.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
synccommitteeCmd.AddCommand(synccommitteeMembersCmd)
synccommitteeFlags(synccommitteeMembersCmd)
synccommitteeMembersCmd.Flags().Int64("epoch", -1, "the epoch for which to fetch sync committees")
synccommitteeMembersCmd.Flags().String("period", "", "the sync committee period for which to fetch sync committees ('current', 'next')")
}
func synccommitteeMembersBindings() {
if err := viper.BindPFlag("epoch", synccommitteeMembersCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("period", synccommitteeMembersCmd.Flags().Lookup("period")); err != nil {
panic(err)
}
}

View File

@@ -17,25 +17,28 @@ import (
"context"
"encoding/hex"
"strings"
"time"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
ethdoutil "github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
string2eth "github.com/wealdtech/go-string2eth"
)
type dataIn struct {
format string
withdrawalCredentials []byte
amount spec.Gwei
validatorAccounts []e2wtypes.Account
forkVersion *spec.Version
domain *spec.Domain
passphrases []string
format string
timeout time.Duration
withdrawalAccount string
withdrawalPubKey string
withdrawalAddress string
amount spec.Gwei
validatorAccounts []e2wtypes.Account
forkVersion *spec.Version
domain *spec.Domain
passphrases []string
}
func input() (*dataIn, error) {
@@ -49,6 +52,11 @@ func input() (*dataIn, error) {
return nil, errors.New("validator account is required")
}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, data.validatorAccounts, err = ethdoutil.WalletAndAccountsFromPath(ctx, viper.GetString("validatoraccount"))
@@ -70,37 +78,25 @@ func input() (*dataIn, error) {
data.passphrases = ethdoutil.GetPassphrases()
switch {
case viper.GetString("withdrawalaccount") != "":
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, withdrawalAccount, err := ethdoutil.WalletAndAccountFromPath(ctx, viper.GetString("withdrawalaccount"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain withdrawal account")
}
pubKey, err := ethdoutil.BestPublicKey(withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for withdrawal account")
}
data.withdrawalCredentials = util.SHA256(pubKey.Marshal())
case viper.GetString("withdrawalpubkey") != "":
withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("withdrawalpubkey"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal public key")
}
if len(withdrawalPubKeyBytes) != 48 {
return nil, errors.New("withdrawal public key must be exactly 48 bytes in length")
}
withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "withdrawal public key is not valid")
}
data.withdrawalCredentials = util.SHA256(withdrawalPubKey.Marshal())
default:
return nil, errors.New("withdrawalaccount or withdrawal public key is required")
data.withdrawalAccount = viper.GetString("withdrawalaccount")
data.withdrawalPubKey = viper.GetString("withdrawalpubkey")
data.withdrawalAddress = viper.GetString("withdrawaladdress")
withdrawalDetailsPresent := 0
if data.withdrawalAccount != "" {
withdrawalDetailsPresent++
}
if data.withdrawalPubKey != "" {
withdrawalDetailsPresent++
}
if data.withdrawalAddress != "" {
withdrawalDetailsPresent++
}
if withdrawalDetailsPresent == 0 {
return nil, errors.New("withdrawal account, public key or address is required")
}
if withdrawalDetailsPresent > 1 {
return nil, errors.New("only one of withdrawal account, public key or address is allowed")
}
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
data.withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
if viper.GetString("depositvalue") == "" {
return nil, errors.New("deposit value is required")
@@ -135,7 +131,7 @@ func inputForkVersion(ctx context.Context) (*spec.Version, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to decode fork version")
}
if len(forkVersion) != 4 {
if len(data) != 4 {
return nil, errors.New("fork version must be exactly 4 bytes in length")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019-2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -84,9 +84,20 @@ func TestInput(t *testing.T) {
name: "Nil",
err: "validator account is required",
},
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "timeout is required",
},
{
name: "ValidatorAccountMissing",
vars: map[string]interface{}{
"timeout": "10s",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
@@ -96,6 +107,7 @@ func TestInput(t *testing.T) {
{
name: "ValidatorAccountUnknown",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Unknown",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
@@ -104,59 +116,74 @@ func TestInput(t *testing.T) {
err: "unknown validator account",
},
{
name: "WithdrawalAccountMissing",
name: "WithdrawalDetailsMissing",
vars: map[string]interface{}{
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "withdrawalaccount or withdrawal public key is required",
err: "withdrawal account, public key or address is required",
},
{
name: "WithdrawalAccountUnknown",
name: "WithdrawalDetailsTooMany1",
vars: map[string]interface{}{
"raw": true,
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Unknown",
"withdrawalaccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "failed to obtain withdrawal account: failed to obtain account: no account with name \"Unknown\"",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "WithdrawalPubKeyInvalid",
name: "WithdrawalDetailsTooMany2",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "invalid",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "failed to decode withdrawal public key: encoding/hex: invalid byte: U+0069 'i'",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "WithdrawalPubKeyWrongLength",
name: "WithdrawalDetailsTooMany3",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0bff",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "withdrawal public key must be exactly 48 bytes in length",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "WithdrawalPubKeyNotPubKey",
name: "WithdrawalDetailsTooMany4",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0x089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "withdrawal public key is not valid: failed to deserialize public key: err blsPublicKeyDeserialize 089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "DepositValueMissing",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"forkversion": "0x01020304",
@@ -166,6 +193,7 @@ func TestInput(t *testing.T) {
{
name: "DepositValueTooSmall",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "1000 Wei",
@@ -176,6 +204,7 @@ func TestInput(t *testing.T) {
{
name: "DepositValueInvalid",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "1 groat",
@@ -186,6 +215,7 @@ func TestInput(t *testing.T) {
{
name: "ForkVersionInvalid",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
@@ -193,54 +223,68 @@ func TestInput(t *testing.T) {
},
err: "failed to obtain fork version: failed to decode fork version: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "ForkVersionShort",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01",
},
err: "failed to obtain fork version: fork version must be exactly 4 bytes in length",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
},
res: &dataIn{
format: "json",
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: mainnetForkVersion,
domain: mainnetDomain,
format: "json",
withdrawalAccount: "Test/Interop 0",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: mainnetForkVersion,
domain: mainnetDomain,
},
},
{
name: "GoodForkVersionOverride",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
res: &dataIn{
format: "json",
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
format: "json",
withdrawalAccount: "Test/Interop 0",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
},
{
name: "GoodWithdrawalPubKey",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
res: &dataIn{
format: "json",
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
format: "json",
withdrawalPubKey: "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
},
}
@@ -258,7 +302,9 @@ func TestInput(t *testing.T) {
require.NoError(t, err)
// Cannot compare accounts directly, so need to check each element individually.
require.Equal(t, test.res.format, res.format)
require.Equal(t, test.res.withdrawalCredentials, res.withdrawalCredentials)
require.Equal(t, test.res.withdrawalAccount, res.withdrawalAccount)
require.Equal(t, test.res.withdrawalAddress, res.withdrawalAddress)
require.Equal(t, test.res.withdrawalPubKey, res.withdrawalPubKey)
require.Equal(t, test.res.amount, res.amount)
require.Equal(t, test.res.forkVersion, res.forkVersion)
require.Equal(t, test.res.domain, res.domain)

View File

@@ -109,6 +109,7 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) {
forkVersionMap := map[spec.Version]string{
[4]byte{0x00, 0x00, 0x00, 0x00}: "mainnet",
[4]byte{0x00, 0x00, 0x20, 0x09}: "pyrmont",
[4]byte{0x00, 0x00, 0x10, 0x20}: "prater",
}
if datum.validatorPubKey == nil {

View File

@@ -253,10 +253,15 @@ func TestOutputLaunchpad(t *testing.T) {
tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2")
signature = &tmp
}
var forkVersion *spec.Version
var forkVersionPyrmont *spec.Version
{
tmp := testutil.HexToVersion("0x00002009")
forkVersion = &tmp
forkVersionPyrmont = &tmp
}
var forkVersionPrater *spec.Version
{
tmp := testutil.HexToVersion("0x00001020")
forkVersionPrater = &tmp
}
var depositDataRoot *spec.Root
{
@@ -316,7 +321,7 @@ func TestOutputLaunchpad(t *testing.T) {
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: signature,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
@@ -332,7 +337,7 @@ func TestOutputLaunchpad(t *testing.T) {
validatorPubKey: validatorPubKey,
amount: 32000000000,
signature: signature,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
@@ -348,7 +353,7 @@ func TestOutputLaunchpad(t *testing.T) {
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
@@ -364,7 +369,7 @@ func TestOutputLaunchpad(t *testing.T) {
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
signature: signature,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
@@ -381,7 +386,7 @@ func TestOutputLaunchpad(t *testing.T) {
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: signature,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositMessageRoot: depositMessageRoot,
},
},
@@ -397,14 +402,14 @@ func TestOutputLaunchpad(t *testing.T) {
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: signature,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot,
},
},
err: "deposit message root required",
},
{
name: "Single",
name: "SinglePyrmont",
dataOut: []*dataOut{
{
format: "launchpad",
@@ -413,13 +418,30 @@ func TestOutputLaunchpad(t *testing.T) {
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: signature,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00002009","eth2_network_name":"pyrmont","deposit_cli_version":"1.1.0"}]`,
},
{
name: "SinglePrater",
dataOut: []*dataOut{
{
format: "launchpad",
account: "interop/00000",
validatorPubKey: validatorPubKey,
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: signature,
forkVersion: forkVersionPrater,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
},
res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"00001020","eth2_network_name":"prater","deposit_cli_version":"1.1.0"}]`,
},
{
name: "Double",
dataOut: []*dataOut{
@@ -430,7 +452,7 @@ func TestOutputLaunchpad(t *testing.T) {
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
signature: signature,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot,
depositMessageRoot: depositMessageRoot,
},
@@ -441,7 +463,7 @@ func TestOutputLaunchpad(t *testing.T) {
withdrawalCredentials: testutil.HexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"),
amount: 32000000000,
signature: signature2,
forkVersion: forkVersion,
forkVersion: forkVersionPyrmont,
depositDataRoot: depositDataRoot2,
depositMessageRoot: depositMessageRoot2,
},

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019-2021 Weald Technology Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,12 +15,16 @@ package depositdata
import (
"context"
"encoding/hex"
"fmt"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/signing"
"github.com/wealdtech/ethdo/util"
ethdoutil "github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -31,8 +35,13 @@ func process(data *dataIn) ([]*dataOut, error) {
results := make([]*dataOut, 0)
withdrawalCredentials, err := createWithdrawalCredentials(data)
if err != nil {
return nil, err
}
for _, validatorAccount := range data.validatorAccounts {
validatorPubKey, err := util.BestPublicKey(validatorAccount)
validatorPubKey, err := ethdoutil.BestPublicKey(validatorAccount)
if err != nil {
return nil, errors.Wrap(err, "validator account does not provide a public key")
}
@@ -41,7 +50,7 @@ func process(data *dataIn) ([]*dataOut, error) {
copy(pubKey[:], validatorPubKey.Marshal())
depositMessage := &spec.DepositMessage{
PublicKey: pubKey,
WithdrawalCredentials: data.withdrawalCredentials,
WithdrawalCredentials: withdrawalCredentials,
Amount: data.amount,
}
root, err := depositMessage.HashTreeRoot()
@@ -58,7 +67,7 @@ func process(data *dataIn) ([]*dataOut, error) {
depositData := &spec.DepositData{
PublicKey: pubKey,
WithdrawalCredentials: data.withdrawalCredentials,
WithdrawalCredentials: withdrawalCredentials,
Amount: data.amount,
Signature: sig,
}
@@ -75,7 +84,7 @@ func process(data *dataIn) ([]*dataOut, error) {
format: data.format,
account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()),
validatorPubKey: &pubKey,
withdrawalCredentials: data.withdrawalCredentials,
withdrawalCredentials: withdrawalCredentials,
amount: data.amount,
signature: &sig,
forkVersion: data.forkVersion,
@@ -85,3 +94,80 @@ func process(data *dataIn) ([]*dataOut, error) {
}
return results, nil
}
// createWithdrawalCredentials creates withdrawal credentials given an account, public key or Ethereum 1 address.
func createWithdrawalCredentials(data *dataIn) ([]byte, error) {
var withdrawalCredentials []byte
switch {
case data.withdrawalAccount != "":
ctx, cancel := context.WithTimeout(context.Background(), data.timeout)
defer cancel()
_, withdrawalAccount, err := ethdoutil.WalletAndAccountFromPath(ctx, data.withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain withdrawal account")
}
pubKey, err := ethdoutil.BestPublicKey(withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for withdrawal account")
}
withdrawalCredentials = util.SHA256(pubKey.Marshal())
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
case data.withdrawalPubKey != "":
withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalPubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal public key")
}
if len(withdrawalPubKeyBytes) != 48 {
return nil, errors.New("withdrawal public key must be exactly 48 bytes in length")
}
pubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "withdrawal public key is not valid")
}
withdrawalCredentials = util.SHA256(pubKey.Marshal())
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
case data.withdrawalAddress != "":
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalAddress, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal address")
}
if len(withdrawalAddressBytes) != 20 {
return nil, errors.New("withdrawal address must be exactly 20 bytes in length")
}
// Ensure the address is properly checksummed.
checksummedAddress := addressBytesToEIP55(withdrawalAddressBytes)
if checksummedAddress != data.withdrawalAddress {
return nil, fmt.Errorf("withdrawal address checksum does not match (expected %s)", checksummedAddress)
}
withdrawalCredentials = make([]byte, 32)
copy(withdrawalCredentials[12:32], withdrawalAddressBytes)
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX
default:
return nil, errors.New("withdrawal account, public key or address is required")
}
return withdrawalCredentials, 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 := util.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))
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 eald Technology Trading
// Copyright © 2019-2021 Weald Technology Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,6 +15,8 @@ package depositdata
import (
"context"
"encoding/hex"
"strings"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
@@ -49,6 +51,10 @@ func TestProcess(t *testing.T) {
)
require.NoError(t, err)
withdrawalAccount := "Test/Interop 0"
withdrawalPubKey := "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"
withdrawalAddress := "0x30C99930617B7b793beaB603ecEB08691005f2E5"
var validatorPubKey *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c")
@@ -101,6 +107,22 @@ func TestProcess(t *testing.T) {
depositMessageRoot2 = &tmp
}
var depositDataRoot3 *spec.Root
{
tmp := testutil.HexToRoot("0x489500535b03dd9deffa0f00cb38d82346111856fb58a9541fe1f01a1a97429c")
depositDataRoot3 = &tmp
}
var depositMessageRoot3 *spec.Root
{
tmp := testutil.HexToRoot("0x7b8ee5694e4338cf2bfe5a4d2f46540f0ade85ebd30713673cf5783c4e925681")
depositMessageRoot3 = &tmp
}
var signature3 *spec.BLSSignature
{
tmp := testutil.HexToSignature("0xba0019d5c421f205d845782f52a87ab95cd489fbef2911f8a1f9cf7c14b4ce59eefa82641e770a4cb405534b7776d0f801b0a8b178c1b71b718c104e89f4e633da10a398c7919a00c403d58f3f4b827af8adb263b192e7a45b0ed1926dff5f66")
signature3 = &tmp
}
tests := []struct {
name string
dataIn *dataIn
@@ -111,16 +133,119 @@ func TestProcess(t *testing.T) {
name: "Nil",
err: "no data",
},
{
name: "WithdrawalDetailsMissing",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal account, public key or address is required",
},
{
name: "WithdrawalAccountUnknown",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAccount: "Unknown",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "failed to obtain withdrawal account: failed to open wallet for account: wallet not found",
},
{
name: "WithdrawalPubKeyInvalid",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: "invalid",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "failed to decode withdrawal public key: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "WithdrawalPubKeyWrongLength",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0bff",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal public key must be exactly 48 bytes in length",
},
{
name: "WithdrawalPubKeyNotPubKey",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: "0x089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal public key is not valid: failed to deserialize public key: err blsPublicKeyDeserialize 089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
},
{
name: "WithdrawalAddressInvalid",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: "invalid",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "failed to decode withdrawal address: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "WithdrawalAddressWrongLength",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: "0x30C99930617B7b793beaB603ecEB08691005f2",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal address must be exactly 20 bytes in length",
},
{
name: "WithdrawalAddressIncorrectChecksum",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: "0x30c99930617b7b793beab603eceb08691005f2e5",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal address checksum does not match (expected 0x30C99930617B7b793beaB603ecEB08691005f2E5)",
},
{
name: "Single",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
format: "raw",
passphrases: []string{"pass"},
withdrawalAccount: withdrawalAccount,
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
@@ -139,13 +264,13 @@ func TestProcess(t *testing.T) {
{
name: "Double",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0, interop1},
forkVersion: forkVersion,
domain: domain,
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: withdrawalPubKey,
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0, interop1},
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
@@ -172,6 +297,31 @@ func TestProcess(t *testing.T) {
},
},
},
{
name: "WithdrawalAddress",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: withdrawalAddress,
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
format: "raw",
account: "Test/Interop 0",
validatorPubKey: validatorPubKey,
amount: 32000000000,
withdrawalCredentials: testutil.HexToBytes("0x01000000000000000000000030C99930617B7b793beaB603ecEB08691005f2E5"),
signature: signature3,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot3,
depositMessageRoot: depositMessageRoot3,
},
},
},
}
for _, test := range tests {
@@ -186,3 +336,18 @@ func TestProcess(t *testing.T) {
})
}
}
func TestAddressBytesToEIP55(t *testing.T) {
tests := []string{
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
}
for _, test := range tests {
bytes, err := hex.DecodeString(strings.TrimPrefix(test, "0x"))
require.NoError(t, err)
require.Equal(t, addressBytesToEIP55(bytes), test)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,10 +15,6 @@ package validatorduties
import (
"context"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
@@ -26,7 +22,6 @@ import (
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -46,7 +41,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
verbose: data.verbose,
}
validatorIndex, err := validatorIndex(ctx, eth2Client, data)
validatorIndex, err := util.ValidatorIndex(ctx, eth2Client, data.account, data.pubKey, data.index)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator index")
}
@@ -58,17 +53,20 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
thisEpochAttesterDuty, err := attesterDuty(ctx, eth2Client, validatorIndex, thisEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain this epoch duty for validator")
return nil, errors.Wrap(err, "failed to obtain this epoch attester duty for validator")
}
results.thisEpochAttesterDuty = thisEpochAttesterDuty
thisEpochProposerDuties, err := proposerDuties(ctx, eth2Client, validatorIndex, thisEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain this epoch proposer duties for validator")
}
results.thisEpochProposerDuties = thisEpochProposerDuties
nextEpoch := thisEpoch + 1
nextEpochAttesterDuty, err := attesterDuty(ctx, eth2Client, validatorIndex, nextEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain next epoch duty for validator")
return nil, errors.Wrap(err, "failed to obtain next epoch attester duty for validator")
}
results.nextEpochAttesterDuty = nextEpochAttesterDuty
@@ -129,54 +127,3 @@ func currentEpoch(ctx context.Context, eth2Client eth2client.Service) (spec.Epoc
}
return spec.Epoch(uint64(time.Since(genesis.GenesisTime).Seconds()) / (uint64(slotDuration.Seconds()) * slotsPerEpoch)), nil
}
// validatorIndex obtains the index of a validator
func validatorIndex(ctx context.Context, eth2Client eth2client.Service, data *dataIn) (spec.ValidatorIndex, error) {
switch {
case data.account != "":
ctx, cancel := context.WithTimeout(context.Background(), data.timeout)
defer cancel()
_, account, err := util.WalletAndAccountFromPath(ctx, data.account)
if err != nil {
return 0, errors.Wrap(err, "failed to obtain account")
}
return accountToIndex(ctx, account, eth2Client)
case data.pubKey != "":
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
if err != nil {
return 0, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
}
account, err := util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return 0, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
}
return accountToIndex(ctx, account, eth2Client)
case data.index != "":
val, err := strconv.ParseUint(data.index, 10, 64)
if err != nil {
return 0, err
}
return spec.ValidatorIndex(val), nil
default:
return 0, errors.New("no validator")
}
}
func accountToIndex(ctx context.Context, account e2wtypes.Account, eth2Client eth2client.Service) (spec.ValidatorIndex, error) {
pubKey, err := util.BestPublicKey(account)
if err != nil {
return 0, err
}
pubKeys := make([]spec.BLSPubKey, 1)
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", pubKeys)
if err != nil {
return 0, err
}
for index := range validators {
return index, nil
}
return 0, errors.New("validator not found")
}

View File

@@ -0,0 +1,73 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexpectation
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
validators int64
// Data access.
eth2Client eth2client.Service
validatorsProvider eth2client.ValidatorsProvider
activeValidators int
// Output.
timeBetweenProposals time.Duration
timeBetweenSyncCommittees time.Duration
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.validators = viper.GetInt64("validators")
if c.validators < 1 {
return nil, errors.New("validators must be at least 1")
}
return c, nil
}

View File

@@ -0,0 +1,82 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexpectation
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: "ConnectionMissing",
vars: map[string]interface{}{
"validators": "1",
"timeout": "5s",
},
err: "connection is required",
},
{
name: "ValidatorsZero",
vars: map[string]interface{}{
"timeout": "5s",
"validators": "0",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "validators must be at least 1",
},
{
name: "Good",
vars: map[string]interface{}{
"validators": "1",
"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)
}
})
}
}

View File

@@ -0,0 +1,39 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexpectation
import (
"context"
"strings"
"github.com/hako/durafmt"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
builder := strings.Builder{}
builder.WriteString("Expected time between block proposals: ")
builder.WriteString(durafmt.Parse(c.timeBetweenProposals).LimitFirstN(2).String())
builder.WriteString("\n")
builder.WriteString("Expected time between sync committees: ")
builder.WriteString(durafmt.Parse(c.timeBetweenSyncCommittees).LimitFirstN(2).String())
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,161 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexpectation
import (
"context"
"fmt"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
if c.debug {
fmt.Printf("Active validators: %d\n", c.activeValidators)
}
if err := c.calculateProposalChance(ctx); err != nil {
return err
}
return c.calculateSyncCommitteeChance(ctx)
}
func (c *command) calculateProposalChance(ctx context.Context) error {
// Chance of proposing a block is 1/activeValidators.
// Expectation of number of slots before proposing a block is 1/p, == activeValidators slots.
spec, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return err
}
tmp, exists := spec["SECONDS_PER_SLOT"]
if !exists {
return errors.New("spec missing SECONDS_PER_SLOT")
}
slotDuration, isType := tmp.(time.Duration)
if !isType {
return errors.New("SECONDS_PER_SLOT of incorrect type")
}
c.timeBetweenProposals = slotDuration * time.Duration(c.activeValidators) / time.Duration(c.validators)
return nil
}
func (c *command) calculateSyncCommitteeChance(ctx context.Context) error {
// Chance of being in a sync committee is SYNC_COMMITTEE_SIZE/activeValidators.
// Expectation of number of periods before being in a sync committee is 1/p, activeValidators/SYNC_COMMITTEE_SIZE periods.
spec, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return err
}
tmp, exists := spec["SECONDS_PER_SLOT"]
if !exists {
return errors.New("spec missing SECONDS_PER_SLOT")
}
slotDuration, isType := tmp.(time.Duration)
if !isType {
return errors.New("SECONDS_PER_SLOT of incorrect type")
}
tmp, exists = spec["SYNC_COMMITTEE_SIZE"]
if !exists {
return errors.New("spec missing SYNC_COMMITTEE_SIZE")
}
syncCommitteeSize, isType := tmp.(uint64)
if !isType {
return errors.New("SYNC_COMMITTEE_SIZE of incorrect type")
}
tmp, exists = spec["SLOTS_PER_EPOCH"]
if !exists {
return errors.New("spec missing SLOTS_PER_EPOCH")
}
slotsPerEpoch, isType := tmp.(uint64)
if !isType {
return errors.New("SLOTS_PER_EPOCH of incorrect type")
}
tmp, exists = spec["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"]
if !exists {
return errors.New("spec missing EPOCHS_PER_SYNC_COMMITTEE_PERIOD")
}
epochsPerPeriod, isType := tmp.(uint64)
if !isType {
return errors.New("EPOCHS_PER_SYNC_COMMITTEE_PERIOD of incorrect type")
}
periodsBetweenSyncCommittees := uint64(c.activeValidators) / syncCommitteeSize
if c.debug {
fmt.Printf("Sync committee periods between inclusion: %d\n", periodsBetweenSyncCommittees)
}
c.timeBetweenSyncCommittees = slotDuration * time.Duration(slotsPerEpoch*epochsPerPeriod) * time.Duration(periodsBetweenSyncCommittees) / time.Duration(c.validators)
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")
}
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 {
return errors.Wrap(err, "failed to set up chaintime service")
}
// Obtain the number of active validators.
var isProvider bool
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validator information")
}
validators, err := c.validatorsProvider.Validators(ctx, "head", nil)
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
currentEpoch := chainTime.CurrentEpoch()
for _, validator := range validators {
if validator.Validator.ActivationEpoch <= currentEpoch &&
validator.Validator.ExitEpoch > currentEpoch {
c.activeValidators++
}
}
return nil
}

View File

@@ -0,0 +1,63 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexpectation
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",
"validators": "1",
"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)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorexpectation
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
}

View File

@@ -0,0 +1,55 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorkeycheck
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
quiet bool
verbose bool
debug bool
// Withdrawal credentials.
withdrawalCredentials string
// Operation.
mnemonic string
privKey string
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Withdrawal credentials.
data.withdrawalCredentials = viper.GetString("withdrawal-credentials")
if data.withdrawalCredentials == "" {
return nil, errors.New("withdrawal credentials are required")
}
data.mnemonic = viper.GetString("mnemonic")
data.privKey = viper.GetString("privkey")
if data.mnemonic == "" && data.privKey == "" {
return nil, errors.New("mnemonic or privkey is required")
}
return data, nil
}

View File

@@ -0,0 +1,71 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorkeycheck
import (
"context"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "WithdrawalCredentialsMissing",
vars: map[string]interface{}{},
err: "withdrawal credentials are required",
},
{
name: "MnemonicAndPrivateKeyMissing",
vars: map[string]interface{}{
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
},
err: "mnemonic or privkey is required",
},
{
name: "GoodWithMnemonic",
vars: map[string]interface{}{
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
"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",
},
res: &dataIn{
withdrawalCredentials: "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
},
},
}
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.withdrawalCredentials, res.withdrawalCredentials)
}
})
}
}

View File

@@ -0,0 +1,52 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorkeycheck
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
match bool
path string
}
func output(ctx context.Context, data *dataOut) (string, int, error) {
if data == nil {
return "", 1, errors.New("no data")
}
if data.quiet {
if !data.match {
os.Exit(1)
}
return "", 1, nil
}
if data.match {
if data.path == "" {
return "Withdrawal credentials confirmed", 0, nil
}
return fmt.Sprintf("Withdrawal credentials confirmed at path %s", data.path), 0, nil
}
return "Could not confirm withdrawal credentials with given information", 1, nil
}

View File

@@ -0,0 +1,81 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorkeycheck
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
exitCode int
expected []string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Not found",
dataOut: &dataOut{
match: false,
},
exitCode: 1,
expected: []string{
"Could not confirm withdrawal credentials with given information",
},
},
{
name: "Found",
dataOut: &dataOut{
match: true,
},
expected: []string{
"Withdrawal credentials confirmed",
},
},
{
name: "FoundWithPath",
dataOut: &dataOut{
match: true,
path: "m/12381/3600/10/0",
},
expected: []string{
"Withdrawal credentials confirmed at path m/12381/3600/10/0",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, exitCode, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.exitCode, exitCode)
for _, expected := range test.expected {
require.True(t, strings.Contains(res, expected))
}
}
})
}
}

View File

@@ -0,0 +1,123 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorkeycheck
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
"golang.org/x/text/unicode/norm"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
validatorWithdrawalCredentials, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalCredentials, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to parse withdrawal credentials")
}
match := false
path := ""
if data.privKey != "" {
// Single private key to check.
keyBytes, err := hex.DecodeString(strings.TrimPrefix(data.privKey, "0x"))
if err != nil {
return nil, err
}
key, err := e2types.BLSPrivateKeyFromBytes(keyBytes)
if err != nil {
return nil, err
}
match, err = checkPrivKey(ctx, data.debug, validatorWithdrawalCredentials, key)
if err != nil {
return nil, err
}
} else {
// Mnemonic to check.
match, path, err = checkMnemonic(ctx, data.debug, validatorWithdrawalCredentials, data.mnemonic)
if err != nil {
return nil, err
}
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
match: match,
path: path,
}
return results, nil
}
func checkPrivKey(ctx context.Context, debug bool, validatorWithdrawalCredentials []byte, key *e2types.BLSPrivateKey) (bool, error) {
pubKey := key.PublicKey()
withdrawalCredentials := util.SHA256(pubKey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
return bytes.Equal(withdrawalCredentials, validatorWithdrawalCredentials), nil
}
func checkMnemonic(ctx context.Context, debug bool, validatorWithdrawalCredentials []byte, mnemonic string) (bool, string, error) {
// If there are more than 24 words we treat the additional characters as the passphrase.
mnemonicParts := strings.Split(mnemonic, " ")
mnemonicPassphrase := ""
if len(mnemonicParts) > 24 {
mnemonic = strings.Join(mnemonicParts[:24], " ")
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
}
// Normalise the input.
mnemonic = string(norm.NFKD.Bytes([]byte(mnemonic)))
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
if !bip39.IsMnemonicValid(mnemonic) {
return false, "", errors.New("mnemonic is invalid")
}
// Create seed from mnemonic and passphrase.
seed := bip39.NewSeed(mnemonic, mnemonicPassphrase)
// Check first 1024 indices.
for i := 0; i < 1024; i++ {
path := fmt.Sprintf("m/12381/3600/%d/0", i)
if debug {
fmt.Printf("Checking path %s\n", path)
}
key, err := util.PrivateKeyFromSeedAndPath(seed, path)
if err != nil {
return false, "", errors.Wrap(err, "failed to generate key")
}
match, err := checkPrivKey(ctx, debug, validatorWithdrawalCredentials, key)
if err != nil {
return false, "", errors.Wrap(err, "failed to match key")
}
if match {
return true, path, nil
}
}
return false, "", nil
}

View File

@@ -0,0 +1,51 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorkeycheck
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// Run runs the wallet create data command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", err
}
results, exitCode, err := output(ctx, dataOut)
if err != nil {
return "", err
}
if exitCode != 0 {
fmt.Println(results)
os.Exit(exitCode)
}
return results, nil
}

View File

@@ -49,9 +49,10 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
func init() {
validatorCmd.AddCommand(validatorDepositDataCmd)
validatorFlags(validatorDepositDataCmd)
validatorDepositDataCmd.Flags().String("validatoraccount", "", "Account of the account carrying out the validation")
validatorDepositDataCmd.Flags().String("withdrawalaccount", "", "Account of the account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("validatoraccount", "", "Account carrying out the validation")
validatorDepositDataCmd.Flags().String("withdrawalaccount", "", "Account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("depositvalue", "", "Value of the amount to be deposited")
validatorDepositDataCmd.Flags().Bool("raw", false, "Print raw deposit data transaction data")
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to fetch it from the node)")
@@ -68,6 +69,9 @@ func validatorDepositdataBindings() {
if err := viper.BindPFlag("withdrawalpubkey", validatorDepositDataCmd.Flags().Lookup("withdrawalpubkey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("withdrawaladdress", validatorDepositDataCmd.Flags().Lookup("withdrawaladdress")); err != nil {
panic(err)
}
if err := viper.BindPFlag("depositvalue", validatorDepositDataCmd.Flags().Lookup("depositvalue")); err != nil {
panic(err)
}

View File

@@ -39,7 +39,7 @@ In quiet mode this will return 0 if the the duties have been obtained, otherwise
if viper.GetBool("quiet") {
return nil
}
fmt.Printf(res)
fmt.Print(res)
return nil
},
}

View File

@@ -0,0 +1,55 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
validatorexpectation "github.com/wealdtech/ethdo/cmd/validator/expectation"
)
var validatorExpectationCmd = &cobra.Command{
Use: "expectation",
Short: "Calculate expectation for individual validators",
Long: `Calculate expectation for individual validators. For example:
ethdo validator expectation`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorexpectation.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
res = strings.TrimRight(res, "\n")
fmt.Println(res)
return nil
},
}
func init() {
validatorCmd.AddCommand(validatorExpectationCmd)
validatorFlags(validatorExpectationCmd)
validatorExpectationCmd.Flags().Int64("validators", 1, "Number of validators")
}
func validatorExpectationBindings() {
if err := viper.BindPFlag("validators", validatorExpectationCmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -54,7 +54,7 @@ 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()
account, err := validatorInfoAccount(ctx, eth2Client)
errCheck(err, "Failed to obtain validator account")
pubKeys := make([]spec.BLSPubKey, 1)
@@ -99,6 +99,12 @@ In quiet mode this will return 0 if the validator information can be obtained, o
fmt.Printf("Public key: %#x\n", validator.Validator.PublicKey)
}
fmt.Printf("Status: %v\n", validator.Status)
switch validator.Status {
case api.ValidatorStateActiveExiting, api.ValidatorStateActiveSlashed:
fmt.Printf("Exit epoch: %d\n", validator.Validator.ExitEpoch)
case api.ValidatorStateExitedUnslashed, api.ValidatorStateExitedSlashed:
fmt.Printf("Withdrawable epoch: %d\n", validator.Validator.WithdrawableEpoch)
}
fmt.Printf("Balance: %s\n", string2eth.GWeiToString(uint64(validator.Balance), true))
if validator.Status.IsActive() {
fmt.Printf("Effective balance: %s\n", string2eth.GWeiToString(uint64(validator.Validator.EffectiveBalance), true))
@@ -107,31 +113,12 @@ In quiet mode this will return 0 if the validator information can be obtained, o
fmt.Printf("Withdrawal credentials: %#x\n", validator.Validator.WithdrawalCredentials)
}
// transition := time.Unix(int64(validatorInfo.TransitionTimestamp), 0)
// transitionPassed := int64(validatorInfo.TransitionTimestamp) <= time.Now().Unix()
// switch validatorInfo.Status {
// case ethpb.ValidatorStatus_DEPOSITED:
// if validatorInfo.TransitionTimestamp != 0 {
// fmt.Printf("Inclusion in chain: %s\n", transition)
// }
// case ethpb.ValidatorStatus_PENDING:
// fmt.Printf("Activation: %s\n", transition)
// case ethpb.ValidatorStatus_EXITING, ethpb.ValidatorStatus_SLASHING:
// fmt.Printf("Attesting finishes: %s\n", transition)
// case ethpb.ValidatorStatus_EXITED:
// if transitionPassed {
// fmt.Printf("Funds withdrawable: Now\n")
// } else {
// fmt.Printf("Funds withdrawable: %s\n", transition)
// }
// }
os.Exit(_exitSuccess)
},
}
// validatorInfoAccount obtains the account for the validator info command.
func validatorInfoAccount() (e2wtypes.Account, error) {
func validatorInfoAccount(ctx context.Context, eth2Client eth2client.Service) (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
switch {
@@ -151,6 +138,27 @@ func validatorInfoAccount() (e2wtypes.Account, error) {
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")
}
@@ -212,6 +220,7 @@ 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")
validatorFlags(validatorInfoCmd)
}
@@ -219,4 +228,7 @@ 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 {
panic(err)
}
}

67
cmd/validatorkeycheck.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
validatorkeycheck "github.com/wealdtech/ethdo/cmd/validator/keycheck"
)
var validatorKeycheckCmd = &cobra.Command{
Use: "keycheck",
Short: "Check that the withdrawal credentials for a validator matches the given key.",
Long: `Check that the withdrawal credentials for a validator matches the given key. For example:
ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --privkey=0x1b46e61babc7a6a0fbfe8e416de3c71f85e367f24e0bfcb12e57adb11117662c
A mnemonic can be used in place of a private key, in which case the first 1,024 indices of the standard withdrawal key path will be scanned for a matching key.
In quiet mode this will return 0 if the withdrawal credentials match the key, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorkeycheck.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
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")
}
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)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2021 Weald Technology Trading
// Copyright © 2019 - 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -23,8 +23,8 @@ import (
)
// ReleaseVersion is the release version of the codebase.
// Usually overrideen by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.7.5)"
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.18.0)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

View File

@@ -0,0 +1,83 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
// System.
timeout time.Duration
verbose bool
debug bool
wallet e2wtypes.Wallet
file string
participants uint32
threshold uint32
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetString("remote") != "" {
return nil, errors.New("wallet export not available for remote wallets")
}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
// Quiet is not allowed.
if viper.GetBool("quiet") {
return nil, errors.New("quiet not allowed")
}
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Wallet.
wallet, err := util.WalletFromInput(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to access wallet")
}
data.wallet = wallet
// File.
data.file = viper.GetString("file")
if data.file == "" {
return nil, errors.New("file is required")
}
// Participants
data.participants = viper.GetUint32("participants")
if data.participants == 0 {
return nil, errors.New("participants is required")
}
data.threshold = viper.GetUint32("threshold")
if data.threshold == 0 {
return nil, errors.New("threshold is required")
}
if data.threshold > data.participants {
return nil, errors.New("threshold cannot be more than participants")
}
return data, nil
}

View File

@@ -0,0 +1,153 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
wallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"wallet": "Test wallet",
},
err: "timeout is required",
},
{
name: "Quiet",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"quiet": "true",
},
err: "quiet not allowed",
},
{
name: "WalletMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to access wallet: cannot determine wallet",
},
{
name: "WalletUnknown",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "unknown",
},
err: "failed to access wallet: wallet not found",
},
{
name: "Remote",
vars: map[string]interface{}{
"timeout": "5s",
"remote": "remoteaddress",
},
err: "wallet export not available for remote wallets",
},
{
name: "FileMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
},
err: "file is required",
},
{
name: "ParticipantsMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
},
err: "participants is required",
},
{
name: "ThresholdMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
},
err: "threshold is required",
},
{
name: "ThresholdTooHigh",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
"threshold": "6",
},
err: "threshold cannot be more than participants",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
"threshold": "3",
},
res: &dataIn{
timeout: 5 * time.Second,
wallet: wallet,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
require.Equal(t, test.vars["wallet"], res.wallet.Name())
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More