Compare commits

...

59 Commits

Author SHA1 Message Date
Jim McDonald
e6d3c67e39 Bump version. 2022-03-16 11:19:52 +00:00
Jim McDonald
6d0a0225c2 Tidy up graffiti and execution output for block info. 2022-03-16 08:52:28 +00:00
Jim McDonald
d60d8beb0b Bump version. 2022-03-11 14:16:49 +00:00
Jim McDonald
a657b3bc24 Do not show execution payload if empty. 2022-03-11 14:16:09 +00:00
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
84 changed files with 5896 additions and 807 deletions

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,42 @@
1.18.2:
- tidy up output of "block info"
1.18.1:
- do not show execution payload if empty
1.18.0:
- add "-ssz" option to "block info"
- add "block analyze" command
- 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)

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

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

@@ -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,34 +31,23 @@ 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")
}
@@ -81,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
@@ -92,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
}
}
@@ -113,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,45 @@ 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" {
graffiti = bytes.TrimRight(graffiti, "\u0000")
if utf8.Valid(graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
} 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 +98,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 +114,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 +146,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 +193,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 +212,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 +232,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 +280,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 +520,69 @@ 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
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(baseFeePerGas, true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
}
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 +593,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 +602,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 +619,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)
}
}

View File

@@ -28,12 +28,17 @@ type dataOut struct {
quiet bool
verbose bool
epoch spec.Epoch
epochStart time.Time
epochEnd time.Time
slot spec.Slot
slotStart time.Time
slotEnd time.Time
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) {
@@ -60,7 +65,18 @@ func output(ctx context.Context, data *dataOut) (string, error) {
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\n")
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

@@ -19,7 +19,7 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
@@ -41,6 +41,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
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")
@@ -59,13 +60,13 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to parse slot")
}
results.slot = spec.Slot(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 = spec.Slot(epoch * slotsPerEpoch)
results.slot = phase0.Slot(epoch * slotsPerEpoch)
case data.timestamp != "":
timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp)
if err != nil {
@@ -75,15 +76,20 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if secs < 0 {
return nil, errors.New("timestamp prior to genesis")
}
results.slot = spec.Slot(secs / slotDuration)
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 = spec.Epoch(uint64(results.slot) / slotsPerEpoch)
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

@@ -15,6 +15,7 @@ package chaintime
import (
"context"
"fmt"
"os"
"testing"
"time"
@@ -46,11 +47,16 @@ func TestProcess(t *testing.T) {
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),
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,
},
},
{
@@ -62,12 +68,17 @@ func TestProcess(t *testing.T) {
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),
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,
},
},
{
@@ -76,15 +87,20 @@ func TestProcess(t *testing.T) {
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
timestamp: "2021-01-01T00:00:00",
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),
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,
},
},
}
@@ -96,6 +112,7 @@ func TestProcess(t *testing.T) {
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)
}
})

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,53 +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")
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
assert(isProvider, "beacon node does not provide spec; cannot report on chain status")
config, err := 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")
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")
genesisProvider, isProvider := eth2Client.(eth2client.GenesisProvider)
assert(isProvider, "beacon node does not provide genesis; cannot report on chain status")
genesis, err := 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)
outputIf(verbose, fmt.Sprintf("Current slot: %d", curSlot))
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)
},
}

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

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

@@ -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,16 +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":
@@ -92,6 +117,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
validatorInfoBindings()
case "validator/keycheck":
validatorKeycheckBindings()
case "validator/expectation":
validatorExpectationBindings()
case "wallet/create":
walletCreateBindings()
case "wallet/import":
@@ -101,19 +128,6 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
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.
@@ -195,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)
}
@@ -387,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",
},
}

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

@@ -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 - 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")
}
@@ -132,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 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)
}
}

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
@@ -24,7 +24,7 @@ import (
// ReleaseVersion is the release version of the codebase.
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.10.2)"
var ReleaseVersion = "local build (latest release 1.18.2)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

View File

@@ -26,7 +26,7 @@ var walletImportCmd = &cobra.Command{
Short: "Import a wallet",
Long: `Import a wallet. For example:
ethdo wallet import --importdata=primary --importpassphrase="my export secret"
ethdo wallet import --data=primary --passphrase="my export secret"
In quiet mode this will return 0 if the wallet is imported successfully, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {

View File

@@ -37,7 +37,7 @@ ethdo wallet create --wallet="Recreated wallet" --type=hd --wallet-passphrase="s
A wallet can be backed up with the `ethdo wallet export` command. This creates an encrypted backup of the wallet, for example:
```sh
ethdo wallet export --wallet="My wallet" --exportpassphrase="export secret" >export.dat
ethdo wallet export --wallet="My wallet" --passphrase="export secret" >export.dat
```
Note that by default the wallet backup is printed to the console, hence the `>export.dat` to redirect it to a file.
@@ -47,7 +47,7 @@ Note that by default the wallet backup is printed to the console, hence the `>ex
A backed up wallet can be restored with the `ethdo wallet import` command, for example:
```sh
ethdo wallet import --importdata=export.dat --importpassphrase="export secret"
ethdo wallet import --data=export.dat --passphrase="export secret"
```
In this example the wallet to be imported is being read from the `export.dat` file.

View File

@@ -266,13 +266,35 @@ $ ethdo version
### `block` commands
Block commands focus on providing information about Ethereum 2 blocks.
#### `analyze`
`ethdo block info` obtains information about a block in Ethereum 2. Options include:
- `blockid`: the ID (slot, root, 'head') of the block to obtain
```sh
$ ethdo block analyze --blockid=80
Value for block 80: 488.531
```
Additional information is supplied when using `--verbose`
```
$ ethdo block analyze --blockid=80 --verbose
Attestation 0: distance 1, 119/119/132 new/total/possible votes, score 0.844, value 100.406
Attestation 1: distance 1, 116/116/131 new/total/possible votes, score 0.844, value 97.875
Attestation 2: distance 1, 115/115/131 new/total/possible votes, score 0.844, value 97.031
Attestation 3: distance 1, 114/114/132 new/total/possible votes, score 0.844, value 96.188
Attestation 4: distance 1, 113/113/132 new/total/possible votes, score 0.844, value 95.344
Attestation 5: distance 1, 2/22/132 new/total/possible votes, score 0.844, value 1.688
Value for block 80: 488.531
```
#### `info`
`ethdo block info` obtains information about a block in Ethereum 2. Options include:
- `slot`: the slot at which to attempt to fetch the block
- `blockid`: the ID (slot, root, 'head') of the block to obtain
```sh
$ ethdo block info --slot=80
$ ethdo block info --blockid=80
Attestations: 1
Attester slashings: 0
Deposits: 0
@@ -451,6 +473,40 @@ $ ethdo slot time --slot=5
2020-12-01 12:01:23 +0000 GMT
```
### `synccommittee` commands
Sync committee commands focus on information about sync committees.
#### `inclusion`
`ethdo synccommittee inclusion` provides information about the inclusion, or not, of a validator's sync committee messages. Options include:
- `account` the account of the validator for which to print sync committee contributions
- `index` the index of the validator for which to print sync committee contributions
- `pubkey` the public key of the validator for which to print sync committee contributions
- `epoch` the specific epoch for which to print sync committee contributions. Defaults to the last complete epoch
```sh
$ ethdo synccommittee inclusion --index=274946 --epoch=91592
Epoch: 91593
Expected: 32
Included: 30
Missed: 1
No block: 1
Per-slot result: ✓✓✓✓✓✓✓✓ ✓✓✕✓✓✓✓✓ ✓✓✓✓-✓✓✓ ✓✓✓✓✓✓✓✓
```
#### `members`
`ethdo synccommittee members` provides information about the members of a sync committee. Options include:
- `epoch` the specific epoch for which to provide sync committee members.
- `period` the period for which to provide sync committee members. Can be 'current' or 'next'; dfeaults to 'current'
```sh
$ ethdo synccommittee members
138334,116317,231736,65706,60046,148162,274946,34724,18051,122841,269578,121110,89733,154887,202118,243459,267543,82793,59504,238929,55360,272874,93917,83116,264342,244312,264907,79193,15443,27997,127175,140965,64416,66399,173906,268885,67779,48139,215005,191435,107954,225228,148630,169357,61091,223319,40668,184307,95903,81179,237461,41723,119710,243333,248243,42757,228686,252749,17546,231625,132030,15934,108465,104302,93026,191946,63738,80996,90679,227542,75463,64581,242030,5429,61623,157314,145363,224733,232492,45357,80674,198583,221422,48665,154803,128608,172512,261074,102835,129935,255726,40846,218932,139874,194575,17346,171565,76413,237859,103170,95661,83018,73902,246680,35795,257792,23836,136624,45745,190990,124229,37281,23818,233435,253903,37502,8669,31151,267179,27954,181019,145719,112270,1899,184844,175014,121769,41717,218760,44813,255860,64865,31985,231664,134296,88114,185542,27557,1698,62470,79182,184325,80380,8865,218456,178979,243886,9466,221389,131476,160857,62916,195389,160182,99293,100263,242371,144594,227527,275978,65714,74350,60121,46642,219334,157142,99379,203508,84367,251808,276456,92563,199831,215312,193875,129690,104234,44290,227725,194780,163061,162328,176517,278620,137355,212826,131615,125734,151873,18977,147927,272759,160537,210675,180411,24203,37266,247527,128678,270287,90352,23043,169645,5304,183412,237387,79751,37635,275139,95857,185990,235565,49425,255836,254314,77582,104172,168556,143653,64173,64504,130363,216602,218107,181130,191845,56454,2040,270365,161952,222409,45097,51611,219190,154903,162311,257460,106337,110775,42928,275709,202352,54724,272295,274470,35220,19694,10347,169585,104938,35121,212982,190582,77999,110201,141519,239881,81263,84314,148883,254649,256309,270013,254179,134009,149660,177127,201926,30533,164789,154343,57437,28958,135169,186415,218514,171355,165247,213526,100044,184264,93278,269329,159634,4092,224671,217236,123946,80703,85444,247742,17959,146473,128231,167559,133899,181532,33378,79060,119785,249443,180469,43692,169679,154421,114047,87877,28337,59072,19807,204598,220293,99461,55272,227923,4503,12580,27044,68955,157373,61321,265034,106833,31534,69137,264783,129588,70433,88338,113528,226211,123003,118982,131549,60350,78896,165715,119736,52639,93274,164295,278837,186453,69910,36768,249533,106205,184057,253232,88155,121377,242589,148236,250065,191526,277249,157463,226527,93000,64784,176880,176380,144301,52061,169803,134291,96648,211716,223000,157911,256737,100938,50434,41075,114894,259888,116872,218201,83617,76348,256832,17113,50270,96468,128448,36987,127511,42397,10154,49234,193346,126352,57719,17029,213127,157942,187829,2353,62462,73637,29053,120324,108515,254684,35982,188131,217092,256206,85802,105907,21204,147562,188961,154541,131147,16000,225112,58362,170375,42239,188309,60280,125472,220119,268946,65736,274053,223569,60454,239552,4401,139357,279634,162711,112016,90295,170641,239770,212067,213770,78311,49057,256295,28666,167207,166783,213148,30689,72118,55912,197733,205116,106169,40570,225057,122079,126423,217781,212897,147499,201774,10616,157826,155954,258431,212151,255318,97138,151907,181491,40236,272993,104430,178068,56089,10067,185066,93669,124108,12785,230215,67995,196282,248285,215370,167715,186183,238147,164161,15068,127990,166146,244578,195912,199812,248435,135597,143024,225304,27045,238140,87008,272550,165234,218128,160038,17697,25332,23446,265921,201045,241106
```
### `validator` commands
Validator commands focus on interaction with Ethereum 2 validators.
@@ -527,6 +583,16 @@ $ ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01
Withdrawal credentials confirmed at path m/12381/3600/10/0
```
#### `expectation`
`ethdo validator expectation` calculates the times between expected actions.
```sh
$ ethdo validator expectation
Expected time between block proposals: 4 weeks 6 days
Expected time between sync committees: 1 year 27 weeks
```
### `attester` commands
Attester commands focus on Ethereum 2 validators' actions as attesters.

80
go.mod
View File

@@ -1,67 +1,51 @@
module github.com/wealdtech/ethdo
go 1.13
go 1.16
require (
github.com/DataDog/zstd v1.4.8 // indirect
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/attestantio/dirk v1.0.2
github.com/attestantio/go-eth2-client v0.6.30
github.com/aws/aws-sdk-go v1.40.14 // indirect
github.com/dgraph-io/badger/v2 v2.2007.3 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/ferranbt/fastssz v0.0.0-20210719200358-90640294cb9c
github.com/goccy/go-yaml v1.9.2 // indirect
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/attestantio/dirk v1.1.0
github.com/attestantio/go-eth2-client v0.11.0
github.com/aws/aws-sdk-go v1.42.44 // indirect
github.com/ferranbt/fastssz v0.0.0-20220103083642-bc5fefefa28b
github.com/gofrs/uuid v4.2.0+incompatible
github.com/google/uuid v1.3.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/hashicorp/hcl v1.0.1-vault-3 // indirect
github.com/herumi/bls-eth-go-binary v0.0.0-20210520070601-31246bfa8ac4
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/herumi/bls-eth-go-binary v0.0.0-20220103074059-01b0ca9e9ef7
github.com/jackc/puddle v1.2.1 // indirect
github.com/minio/highwayhash v1.0.2 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.1 // indirect
github.com/protolambda/zssz v0.1.5 // indirect
github.com/prysmaticlabs/ethereumapis v0.0.0-20210201130911-92b2a467c108 // indirect
github.com/prysmaticlabs/go-bitfield v0.0.0-20210706153858-5cb5ce8bdbfe
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388
github.com/rs/zerolog v1.23.0
github.com/spf13/cast v1.4.0 // indirect
github.com/spf13/cobra v1.2.1
github.com/rs/zerolog v1.26.1
github.com/spf13/afero v1.8.0 // indirect
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tyler-smith/go-bip39 v1.1.0
github.com/wealdtech/go-bytesutil v1.1.1
github.com/wealdtech/go-ecodec v1.1.1
github.com/wealdtech/go-eth2-types/v2 v2.5.5
github.com/wealdtech/go-eth2-util v1.6.4
github.com/wealdtech/go-eth2-wallet v1.14.4
github.com/wealdtech/go-eth2-wallet-dirk v1.1.6
github.com/wealdtech/go-eth2-wallet-distributed v1.1.3
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.5
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.5.4
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.3
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.14
github.com/wealdtech/go-eth2-wallet-store-s3 v1.9.4
github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.2
github.com/wealdtech/go-eth2-wallet-types/v2 v2.8.4
github.com/wealdtech/go-ecodec v1.1.2
github.com/wealdtech/go-eth2-types/v2 v2.6.0
github.com/wealdtech/go-eth2-util v1.7.0
github.com/wealdtech/go-eth2-wallet v1.15.0
github.com/wealdtech/go-eth2-wallet-dirk v1.2.0
github.com/wealdtech/go-eth2-wallet-distributed v1.1.4
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.2.0
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.6.0
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.4.0
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.17.0
github.com/wealdtech/go-eth2-wallet-store-s3 v1.10.0
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0
github.com/wealdtech/go-eth2-wallet-types/v2 v2.9.0
github.com/wealdtech/go-string2eth v1.1.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/text v0.3.6
google.golang.org/genproto v0.0.0-20210803142424-70bd63adacf2 // indirect
google.golang.org/grpc v1.39.0
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect
golang.org/x/text v0.3.7
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect
google.golang.org/grpc v1.44.0
gopkg.in/ini.v1 v1.66.3 // indirect
)

689
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
// 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 (
"time"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
// Service provides a number of functions for calculating chain-related times.
type Service interface {
// GenesisTime provides the time of the chain's genesis.
GenesisTime() time.Time
// SlotsPerEpoch provides the number of slots in the chain's epoch.
SlotsPerEpoch() uint64
// SlotDuration provides the duration of the chain's slot.
SlotDuration() time.Duration
// StartOfSlot provides the time at which a given slot starts.
StartOfSlot(slot phase0.Slot) time.Time
// StartOfEpoch provides the time at which a given epoch starts.
StartOfEpoch(epoch phase0.Epoch) time.Time
// CurrentSlot provides the current slot.
CurrentSlot() phase0.Slot
// CurrentEpoch provides the current epoch.
CurrentEpoch() phase0.Epoch
// CurrentSyncCommitteePeriod provides the current sync committee period.
CurrentSyncCommitteePeriod() uint64
// SlotToEpoch provides the epoch of the given slot.
SlotToEpoch(slot phase0.Slot) phase0.Epoch
// SlotToSyncCommitteePeriod provides the sync committee period of the given slot.
SlotToSyncCommitteePeriod(slot phase0.Slot) uint64
// FirstSlotOfEpoch provides the first slot of the given epoch.
FirstSlotOfEpoch(epoch phase0.Epoch) phase0.Slot
// TimestampToSlot provides the slot of the given timestamp.
TimestampToSlot(timestamp time.Time) phase0.Slot
// TimestampToEpoch provides the epoch of the given timestamp.
TimestampToEpoch(timestamp time.Time) phase0.Epoch
// FirstEpochOfSyncPeriod provides the first epoch of the given sync period.
FirstEpochOfSyncPeriod(period uint64) phase0.Epoch
// AltairInitialEpoch provides the epoch at which the Altair hard fork takes place.
AltairInitialEpoch() phase0.Epoch
// AltairInitialSyncCommitteePeriod provides the sync committee period in which the Altair hard fork takes place.
AltairInitialSyncCommitteePeriod() uint64
}

View File

@@ -0,0 +1,90 @@
// 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 standard
import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
type parameters struct {
logLevel zerolog.Level
genesisTimeProvider eth2client.GenesisTimeProvider
specProvider eth2client.SpecProvider
forkScheduleProvider eth2client.ForkScheduleProvider
}
// Parameter is the interface for service parameters.
type Parameter interface {
apply(*parameters)
}
type parameterFunc func(*parameters)
func (f parameterFunc) apply(p *parameters) {
f(p)
}
// WithLogLevel sets the log level for the module.
func WithLogLevel(logLevel zerolog.Level) Parameter {
return parameterFunc(func(p *parameters) {
p.logLevel = logLevel
})
}
// WithGenesisTimeProvider sets the genesis time provider.
func WithGenesisTimeProvider(provider eth2client.GenesisTimeProvider) Parameter {
return parameterFunc(func(p *parameters) {
p.genesisTimeProvider = provider
})
}
// WithSpecProvider sets the spec provider.
func WithSpecProvider(provider eth2client.SpecProvider) Parameter {
return parameterFunc(func(p *parameters) {
p.specProvider = provider
})
}
// WithForkScheduleProvider sets the fork schedule provider.
func WithForkScheduleProvider(provider eth2client.ForkScheduleProvider) Parameter {
return parameterFunc(func(p *parameters) {
p.forkScheduleProvider = provider
})
}
// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct.
func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
parameters := parameters{
logLevel: zerolog.GlobalLevel(),
}
for _, p := range params {
if params != nil {
p.apply(&parameters)
}
}
if parameters.specProvider == nil {
return nil, errors.New("no spec provider specified")
}
if parameters.genesisTimeProvider == nil {
return nil, errors.New("no genesis time provider specified")
}
if parameters.forkScheduleProvider == nil {
return nil, errors.New("no fork schedule provider specified")
}
return &parameters, nil
}

View File

@@ -0,0 +1,249 @@
// 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 standard
import (
"bytes"
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/rs/zerolog"
zerologger "github.com/rs/zerolog/log"
)
// Service provides chain time services.
type Service struct {
genesisTime time.Time
slotDuration time.Duration
slotsPerEpoch uint64
epochsPerSyncCommitteePeriod uint64
altairForkEpoch phase0.Epoch
bellatrixForkEpoch phase0.Epoch
}
// module-wide log.
var log zerolog.Logger
// New creates a new controller.
func New(ctx context.Context, params ...Parameter) (*Service, error) {
parameters, err := parseAndCheckParameters(params...)
if err != nil {
return nil, errors.Wrap(err, "problem with parameters")
}
// Set logging.
log = zerologger.With().Str("service", "chaintime").Str("impl", "standard").Logger().Level(parameters.logLevel)
genesisTime, err := parameters.genesisTimeProvider.GenesisTime(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis time")
}
log.Trace().Time("genesis_time", genesisTime).Msg("Obtained genesis time")
spec, err := parameters.specProvider.Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["SECONDS_PER_SLOT"]
if !exists {
return nil, errors.New("SECONDS_PER_SLOT not found in spec")
}
slotDuration, ok := tmp.(time.Duration)
if !ok {
return nil, errors.New("SECONDS_PER_SLOT of unexpected type")
}
tmp, exists = spec["SLOTS_PER_EPOCH"]
if !exists {
return nil, errors.New("SLOTS_PER_EPOCH not found in spec")
}
slotsPerEpoch, ok := tmp.(uint64)
if !ok {
return nil, errors.New("SLOTS_PER_EPOCH of unexpected type")
}
var epochsPerSyncCommitteePeriod uint64
if tmp, exists := spec["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"]; exists {
tmp2, ok := tmp.(uint64)
if !ok {
return nil, errors.New("EPOCHS_PER_SYNC_COMMITTEE_PERIOD of unexpected type")
}
epochsPerSyncCommitteePeriod = tmp2
}
altairForkEpoch, err := fetchAltairForkEpoch(ctx, parameters.forkScheduleProvider)
if err != nil {
// Set to far future epoch.
altairForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(altairForkEpoch)).Msg("Obtained Altair fork epoch")
bellatrixForkEpoch, err := fetchBellatrixForkEpoch(ctx, parameters.forkScheduleProvider)
if err != nil {
// Set to far future epoch.
bellatrixForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(bellatrixForkEpoch)).Msg("Obtained Bellatrix fork epoch")
s := &Service{
genesisTime: genesisTime,
slotDuration: slotDuration,
slotsPerEpoch: slotsPerEpoch,
epochsPerSyncCommitteePeriod: epochsPerSyncCommitteePeriod,
altairForkEpoch: altairForkEpoch,
bellatrixForkEpoch: bellatrixForkEpoch,
}
return s, nil
}
// GenesisTime provides the time of the chain's genesis.
func (s *Service) GenesisTime() time.Time {
return s.genesisTime
}
// SlotsPerEpoch provides the number of slots in the chain's epoch.
func (s *Service) SlotsPerEpoch() uint64 {
return s.slotsPerEpoch
}
// SlotDuration provides the duration of the chain's slot.
func (s *Service) SlotDuration() time.Duration {
return s.slotDuration
}
// StartOfSlot provides the time at which a given slot starts.
func (s *Service) StartOfSlot(slot phase0.Slot) time.Time {
return s.genesisTime.Add(time.Duration(slot) * s.slotDuration)
}
// StartOfEpoch provides the time at which a given epoch starts.
func (s *Service) StartOfEpoch(epoch phase0.Epoch) time.Time {
return s.genesisTime.Add(time.Duration(uint64(epoch)*s.slotsPerEpoch) * s.slotDuration)
}
// CurrentSlot provides the current slot.
func (s *Service) CurrentSlot() phase0.Slot {
if s.genesisTime.After(time.Now()) {
return 0
}
return phase0.Slot(uint64(time.Since(s.genesisTime).Seconds()) / uint64(s.slotDuration.Seconds()))
}
// CurrentEpoch provides the current epoch.
func (s *Service) CurrentEpoch() phase0.Epoch {
return phase0.Epoch(uint64(s.CurrentSlot()) / s.slotsPerEpoch)
}
// CurrentSyncCommitteePeriod provides the current sync committee period.
func (s *Service) CurrentSyncCommitteePeriod() uint64 {
return uint64(s.CurrentEpoch()) / s.epochsPerSyncCommitteePeriod
}
// SlotToEpoch provides the epoch of a given slot.
func (s *Service) SlotToEpoch(slot phase0.Slot) phase0.Epoch {
return phase0.Epoch(uint64(slot) / s.slotsPerEpoch)
}
// SlotToSyncCommitteePeriod provides the sync committee period of the given slot.
func (s *Service) SlotToSyncCommitteePeriod(slot phase0.Slot) uint64 {
return uint64(s.SlotToEpoch(slot)) / s.epochsPerSyncCommitteePeriod
}
// FirstSlotOfEpoch provides the first slot of the given epoch.
func (s *Service) FirstSlotOfEpoch(epoch phase0.Epoch) phase0.Slot {
return phase0.Slot(uint64(epoch) * s.slotsPerEpoch)
}
// TimestampToSlot provides the slot of the given timestamp.
func (s *Service) TimestampToSlot(timestamp time.Time) phase0.Slot {
if timestamp.Before(s.genesisTime) {
return 0
}
secondsSinceGenesis := uint64(timestamp.Sub(s.genesisTime).Seconds())
return phase0.Slot(secondsSinceGenesis / uint64(s.slotDuration.Seconds()))
}
// TimestampToEpoch provides the epoch of the given timestamp.
func (s *Service) TimestampToEpoch(timestamp time.Time) phase0.Epoch {
if timestamp.Before(s.genesisTime) {
return 0
}
secondsSinceGenesis := uint64(timestamp.Sub(s.genesisTime).Seconds())
return phase0.Epoch(secondsSinceGenesis / uint64(s.slotDuration.Seconds()) / s.slotsPerEpoch)
}
// FirstEpochOfSyncPeriod provides the first epoch of the given sync period.
// Note that epochs before the sync committee period will provide the Altair hard fork epoch.
func (s *Service) FirstEpochOfSyncPeriod(period uint64) phase0.Epoch {
epoch := phase0.Epoch(period * s.epochsPerSyncCommitteePeriod)
if epoch < s.altairForkEpoch {
epoch = s.altairForkEpoch
}
return epoch
}
// AltairInitialEpoch provides the epoch at which the Altair hard fork takes place.
func (s *Service) AltairInitialEpoch() phase0.Epoch {
return s.altairForkEpoch
}
// AltairInitialSyncCommitteePeriod provides the sync committee period in which the Altair hard fork takes place.
func (s *Service) AltairInitialSyncCommitteePeriod() uint64 {
return uint64(s.altairForkEpoch) / s.epochsPerSyncCommitteePeriod
}
func fetchAltairForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
forkSchedule, err := provider.ForkSchedule(ctx)
if err != nil {
return 0, err
}
for i := range forkSchedule {
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
// This is the genesis fork; ignore it.
continue
}
return forkSchedule[i].Epoch, nil
}
return 0, errors.New("no altair fork obtained")
}
// BellatrixInitialEpoch provides the epoch at which the Bellatrix hard fork takes place.
func (s *Service) BellatrixInitialEpoch() phase0.Epoch {
return s.bellatrixForkEpoch
}
func fetchBellatrixForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
forkSchedule, err := provider.ForkSchedule(ctx)
if err != nil {
return 0, err
}
count := 0
for i := range forkSchedule {
count++
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
// This is the genesis fork; ignore it.
continue
}
if count == 1 {
return forkSchedule[i].Epoch, nil
}
count++
}
return 0, errors.New("no bellatrix fork obtained")
}

View File

@@ -0,0 +1,258 @@
// 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 standard_test
import (
"context"
"testing"
"time"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/testing/mock"
)
func TestService(t *testing.T) {
genesisTime := time.Now()
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
forkSchedule := []*phase0.Fork{
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
Epoch: 0,
},
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x05, 0x06, 0x07, 0x08},
Epoch: 10,
},
}
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
tests := []struct {
name string
params []standard.Parameter
err string
}{
{
name: "GenesisTimeProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
err: "problem with parameters: no genesis time provider specified",
},
{
name: "SpecProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
err: "problem with parameters: no spec provider specified",
},
{
name: "ForkScheduleProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
},
err: "problem with parameters: no fork schedule provider specified",
},
{
name: "Good",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := standard.New(context.Background(), test.params...)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}
// createService is a helper that creates a mock chaintime service.
func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uint64, uint64, []*phase0.Fork, error) {
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
forkSchedule := []*phase0.Fork{
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
Epoch: 0,
},
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x05, 0x06, 0x07, 0x08},
Epoch: 10,
},
}
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
s, err := standard.New(context.Background(),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
)
return s, slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
}
func TestGenesisTime(t *testing.T) {
genesisTime := time.Now()
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, genesisTime, s.GenesisTime())
}
func TestStartOfSlot(t *testing.T) {
genesisTime := time.Now()
s, slotDuration, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, genesisTime, s.StartOfSlot(0))
require.Equal(t, genesisTime.Add(1000*slotDuration), s.StartOfSlot(1000))
}
func TestStartOfEpoch(t *testing.T) {
genesisTime := time.Now()
s, slotDuration, slotsPerEpoch, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, genesisTime, s.StartOfEpoch(0))
require.Equal(t, genesisTime.Add(time.Duration(1000*slotsPerEpoch)*slotDuration), s.StartOfEpoch(1000))
}
func TestCurrentSlot(t *testing.T) {
genesisTime := time.Now().Add(-60 * time.Second)
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, phase0.Slot(5), s.CurrentSlot())
}
func TestCurrentEpoch(t *testing.T) {
genesisTime := time.Now().Add(-1000 * time.Second)
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, phase0.Epoch(2), s.CurrentEpoch())
}
func TestTimestampToSlot(t *testing.T) {
genesisTime := time.Now()
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
tests := []struct {
name string
timestamp time.Time
slot phase0.Slot
}{
{
name: "PreGenesis",
timestamp: genesisTime.AddDate(0, 0, -1),
slot: 0,
},
{
name: "Genesis",
timestamp: genesisTime,
slot: 0,
},
{
name: "Slot1",
timestamp: genesisTime.Add(12 * time.Second),
slot: 1,
},
{
name: "Slot999",
timestamp: genesisTime.Add(999 * 12 * time.Second),
slot: 999,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.slot, s.TimestampToSlot(test.timestamp))
})
}
}
func TestTimestampToEpoch(t *testing.T) {
genesisTime := time.Now()
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
tests := []struct {
name string
timestamp time.Time
epoch phase0.Epoch
}{
{
name: "PreGenesis",
timestamp: genesisTime.AddDate(0, 0, -1),
epoch: 0,
},
{
name: "Genesis",
timestamp: genesisTime,
epoch: 0,
},
{
name: "Epoch1",
timestamp: genesisTime.Add(32 * 12 * time.Second),
epoch: 1,
},
{
name: "Epoch1Boundary",
timestamp: genesisTime.Add(64 * 12 * time.Second).Add(-1 * time.Millisecond),
epoch: 1,
},
{
name: "Epoch999",
timestamp: genesisTime.Add(999 * 32 * 12 * time.Second),
epoch: 999,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.epoch, s.TimestampToEpoch(test.timestamp))
})
}
}

151
testing/mock/eth2client.go Normal file
View File

@@ -0,0 +1,151 @@
// 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 mock
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"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
// GenesisTimeProvider is a mock for eth2client.GenesisTimeProvider.
type GenesisTimeProvider struct {
genesisTime time.Time
}
// NewGenesisTimeProvider returns a mock genesis time provider with the provided value.
func NewGenesisTimeProvider(genesisTime time.Time) eth2client.GenesisTimeProvider {
return &GenesisTimeProvider{
genesisTime: genesisTime,
}
}
// GenesisTime is a mock.
func (m *GenesisTimeProvider) GenesisTime(ctx context.Context) (time.Time, error) {
return m.genesisTime, nil
}
// SpecProvider is a mock for eth2client.SpecProvider.
type SpecProvider struct {
spec map[string]interface{}
}
// NewSpecProvider returns a mock spec provider with the provided values.
func NewSpecProvider(slotDuration time.Duration,
slotsPerEpoch uint64,
epochsPerSyncCommitteePeriod uint64,
) eth2client.SpecProvider {
return &SpecProvider{
spec: map[string]interface{}{
"SECONDS_PER_SLOT": slotDuration,
"SLOTS_PER_EPOCH": slotsPerEpoch,
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": epochsPerSyncCommitteePeriod,
},
}
}
// Spec is a mock.
func (m *SpecProvider) Spec(ctx context.Context) (map[string]interface{}, error) {
return m.spec, nil
}
// ForkScheduleProvider is a mock for eth2client.ForkScheduleProvider.
type ForkScheduleProvider struct {
schedule []*phase0.Fork
}
// NewForkScheduleProvider returns a mock spec provider with the provided values.
func NewForkScheduleProvider(schedule []*phase0.Fork) eth2client.ForkScheduleProvider {
return &ForkScheduleProvider{
schedule: schedule,
}
}
// ForkSchedule is a mock.
func (m *ForkScheduleProvider) ForkSchedule(ctx context.Context) ([]*phase0.Fork, error) {
return m.schedule, nil
}
// SlotsPerEpochProvider is a mock for eth2client.SlotsPerEpochProvider.
type SlotsPerEpochProvider struct {
slotsPerEpoch uint64
}
// NewSlotsPerEpochProvider returns a mock slots per epoch provider with the provided value.
func NewSlotsPerEpochProvider(slotsPerEpoch uint64) eth2client.SlotsPerEpochProvider {
return &SlotsPerEpochProvider{
slotsPerEpoch: slotsPerEpoch,
}
}
// SlotsPerEpoch is a mock.
func (m *SlotsPerEpochProvider) SlotsPerEpoch(ctx context.Context) (uint64, error) {
return m.slotsPerEpoch, nil
}
// AttestationsSubmitter is a mock for eth2client.AttestationsSubmitter.
type AttestationsSubmitter struct{}
// NewAttestationSubmitter returns a mock attestations submitter with the provided value.
func NewAttestationSubmitter() eth2client.AttestationsSubmitter {
return &AttestationsSubmitter{}
}
// SubmitAttestations is a mock.
func (m *AttestationsSubmitter) SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error {
return nil
}
// BeaconBlockSubmitter is a mock for eth2client.BeaconBlockSubmitter.
type BeaconBlockSubmitter struct{}
// NewBeaconBlockSubmitter returns a mock beacon block submitter with the provided value.
func NewBeaconBlockSubmitter() eth2client.BeaconBlockSubmitter {
return &BeaconBlockSubmitter{}
}
// SubmitBeaconBlock is a mock.
func (m *BeaconBlockSubmitter) SubmitBeaconBlock(ctx context.Context, bloc *spec.VersionedSignedBeaconBlock) error {
return nil
}
// AggregateAttestationsSubmitter is a mock for eth2client.AggregateAttestationsSubmitter.
type AggregateAttestationsSubmitter struct{}
// NewAggregateAttestationsSubmitter returns a mock aggregate attestation submitter with the provided value.
func NewAggregateAttestationsSubmitter() eth2client.AggregateAttestationsSubmitter {
return &AggregateAttestationsSubmitter{}
}
// SubmitAggregateAttestations is a mock.
func (m *AggregateAttestationsSubmitter) SubmitAggregateAttestations(ctx context.Context, aggregates []*phase0.SignedAggregateAndProof) error {
return nil
}
// BeaconCommitteeSubscriptionsSubmitter is a mock for eth2client.BeaconCommitteeSubscriptionsSubmitter.
type BeaconCommitteeSubscriptionsSubmitter struct{}
// NewBeaconCommitteeSubscriptionsSubmitter returns a mock beacon committee subscription submitter with the provided value.
func NewBeaconCommitteeSubscriptionsSubmitter() eth2client.BeaconCommitteeSubscriptionsSubmitter {
return &BeaconCommitteeSubscriptionsSubmitter{}
}
// SubmitBeaconCommitteeSubscriptions is a mock.
func (m *BeaconCommitteeSubscriptionsSubmitter) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*api.BeaconCommitteeSubscription) error {
return nil
}

View File

@@ -17,6 +17,7 @@ import (
"context"
"fmt"
"net/url"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
@@ -31,6 +32,9 @@ func ConnectToBeaconNode(ctx context.Context, address string, timeout time.Durat
return nil, errors.New("no timeout specified")
}
if !strings.HasPrefix(address, "http") {
address = fmt.Sprintf("http://%s", address)
}
if !allowInsecure {
// Ensure the connection is either secure or local.
connectionURL, err := url.Parse(address)
@@ -39,7 +43,9 @@ func ConnectToBeaconNode(ctx context.Context, address string, timeout time.Durat
}
if connectionURL.Scheme == "http" &&
connectionURL.Host != "localhost" &&
connectionURL.Host != "127.0.0.1" {
!strings.HasPrefix(connectionURL.Host, "localhost:") &&
connectionURL.Host != "127.0.0.1" &&
!strings.HasPrefix(connectionURL.Host, "127.0.0.1:") {
fmt.Println("Connections to remote beacon nodes should be secure. This warning can be silenced with --allow-insecure-connections")
}
}

View File

@@ -26,35 +26,32 @@ var networks = map[string]string{
"00000000219ab540356cbb839cbe05303d7705fa": "Mainnet",
"07b39f4fde4a38bace212b546dac87c58dfe3fdc": "Medalla",
"8c5fecdc472e27bc447696f431e425d02dd46a8c": "Pyrmont",
"ff50ed3d0ec03ac01d4c79aad74928bff48a7b2b": "Prater",
}
// Network returns the name of the network., calculated from the deposit contract information.
// If not known, returns "Unknown".
func Network(ctx context.Context, eth2Client eth2client.Service) (string, error) {
var address []byte
var err error
if eth2Client == nil {
return "", errors.New("no Ethereum 2 client supplied")
}
if provider, isProvider := eth2Client.(eth2client.DepositContractProvider); isProvider {
address, err = provider.DepositContractAddress(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain deposit contract address")
}
} else if provider, isProvider := eth2Client.(eth2client.SpecProvider); isProvider {
config, err := provider.Spec(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain chain specification")
}
if config == nil {
return "", errors.New("failed to return chain specification")
}
depositContractAddress, exists := config["DEPOSIT_CONTRACT_ADDRESS"]
if exists {
address = depositContractAddress.([]byte)
}
provider, isProvider := eth2Client.(eth2client.SpecProvider)
if !isProvider {
return "", errors.New("client does not provide deposit contract address")
}
config, err := provider.Spec(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain chain specification")
}
if config == nil {
return "", errors.New("failed to return chain specification")
}
depositContractAddress, exists := config["DEPOSIT_CONTRACT_ADDRESS"]
if exists {
address = depositContractAddress.([]byte)
}
return network(address), nil

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
@@ -23,38 +23,6 @@ import (
"github.com/wealdtech/ethdo/util"
)
// A mock Ethereum 2 client service that returns supplied deposit information.
type depositETH2Client struct {
address []byte
chainID uint64
networkID uint64
}
// Name returns the name of the client implementation.
func (c *depositETH2Client) Name() string {
return "deposit mock"
}
// Address returns the address of the client.
func (c *depositETH2Client) Address() string {
return "mock"
}
// DepositContractAddress provides the Ethereum 1 address of the deposit contract.
func (c *depositETH2Client) DepositContractAddress(ctx context.Context) ([]byte, error) {
return c.address, nil
}
// DepositContractChainID provides the Ethereum 1 chain ID of the deposit contract.
func (c *depositETH2Client) DepositContractChainID(ctx context.Context) (uint64, error) {
return c.chainID, nil
}
// DepositContractNetworkID provides the Ethereum 1 network ID of the deposit contract.
func (c *depositETH2Client) DepositContractNetworkID(ctx context.Context) (uint64, error) {
return c.networkID, nil
}
// A mock Ethereum 2 client service that returns spec information.
type specETH2Client struct {
address []byte
@@ -88,15 +56,6 @@ func TestNetworks(t *testing.T) {
name: "Nil",
err: "no Ethereum 2 client supplied",
},
{
name: "MainnetDeposit",
service: &depositETH2Client{
address: testutil.HexToBytes("0x00000000219ab540356cbb839cbe05303d7705fa"),
chainID: 0,
networkID: 0,
},
network: "Mainnet",
},
{
name: "MainnetSpec",
service: &specETH2Client{
@@ -104,15 +63,6 @@ func TestNetworks(t *testing.T) {
},
network: "Mainnet",
},
{
name: "UnknownDeposit",
service: &depositETH2Client{
address: testutil.HexToBytes("0x1111111111111111111111111111111111111111"),
chainID: 0,
networkID: 0,
},
network: "Unknown",
},
{
name: "UnknownSpec",
service: &specETH2Client{

79
util/validator.go Normal file
View File

@@ -0,0 +1,79 @@
// 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 util
import (
"context"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// ValidatorIndex obtains the index of a validator.
func ValidatorIndex(ctx context.Context, client consensusclient.Service, account string, pubKey string, index string) (phase0.ValidatorIndex, error) {
switch {
case account != "":
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, account, err := WalletAndAccountFromPath(ctx, account)
if err != nil {
return 0, errors.Wrap(err, "failed to obtain account")
}
return accountToIndex(ctx, account, client)
case pubKey != "":
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x"))
if err != nil {
return 0, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey))
}
account, err := NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return 0, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey))
}
return accountToIndex(ctx, account, client)
case index != "":
val, err := strconv.ParseUint(index, 10, 64)
if err != nil {
return 0, err
}
return phase0.ValidatorIndex(val), nil
default:
return 0, errors.New("no validator")
}
}
func accountToIndex(ctx context.Context, account e2wtypes.Account, client consensusclient.Service) (phase0.ValidatorIndex, error) {
pubKey, err := BestPublicKey(account)
if err != nil {
return 0, err
}
pubKeys := make([]phase0.BLSPubKey, 1)
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := client.(consensusclient.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")
}