Compare commits

...

42 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
74 changed files with 4173 additions and 629 deletions

View File

@@ -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
# sha256sum ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
- name: Create release
id: create_release

View File

@@ -1,3 +1,35 @@
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"

View File

@@ -1,4 +1,4 @@
FROM golang:1.16-buster as builder
FROM golang:1.17-bullseye as builder
WORKDIR /app
@@ -10,7 +10,9 @@ 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

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"
"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([]phase0.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
@@ -110,10 +101,32 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
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
}
}
@@ -121,6 +134,49 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
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, []phase0.ValidatorIndex{validator.Index})

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

@@ -18,6 +18,7 @@ import (
"context"
"encoding/hex"
"fmt"
"math/big"
"sort"
"strings"
"time"
@@ -25,6 +26,7 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
@@ -51,6 +53,7 @@ func output(ctx context.Context, data *dataOut) (string, error) {
func outputBlockGeneral(ctx context.Context,
verbose bool,
slot phase0.Slot,
blockRoot phase0.Root,
bodyRoot phase0.Root,
parentRoot phase0.Root,
stateRoot phase0.Root,
@@ -67,12 +70,14 @@ func outputBlockGeneral(ctx context.Context,
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", bodyRoot))
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
if verbose {
res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot))
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
}
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
graffiti = bytes.TrimRight(graffiti, "\u0000")
if utf8.Valid(graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
} else {
@@ -109,13 +114,15 @@ 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[phase0.CommitteeIndex][]phase0.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]
}
@@ -123,7 +130,9 @@ func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service,
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
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))
@@ -261,6 +270,96 @@ func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = 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")
@@ -271,13 +370,19 @@ func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *alta
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")
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,
@@ -349,6 +454,10 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
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")
@@ -356,6 +465,7 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
@@ -410,6 +520,64 @@ func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phas
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) []phase0.ValidatorIndex {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })

View File

@@ -23,11 +23,13 @@ import (
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/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) {
@@ -66,7 +68,11 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, signedBlock.Altair); err != nil {
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:
@@ -75,6 +81,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
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")
@@ -94,26 +101,43 @@ 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 signedBlock == nil {
fmt.Println("Empty beacon block")
if !jsonOutput {
fmt.Println("Empty beacon block")
}
return
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
fmt.Printf("Failed to output block: %v\n", err)
if !jsonOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, signedBlock.Altair); err != nil {
fmt.Printf("Failed to output block: %v\n", err)
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:
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
if !jsonOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
return
}
}
@@ -136,7 +160,7 @@ func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0
return nil
}
func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair.SignedBeaconBlock) error {
func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *altair.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
@@ -144,6 +168,12 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair
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 {
@@ -153,3 +183,27 @@ func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair
}
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")
}
fmt.Printf("%s\n", data)
}
return nil
}

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

@@ -85,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
@@ -64,8 +64,21 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
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":
@@ -76,16 +89,22 @@ 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":
@@ -98,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":
@@ -107,15 +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")
}
return util.SetupStore()
}
// Execute adds all child commands to the root command and sets flags appropriately.
@@ -197,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)
}
@@ -389,3 +401,14 @@ func remotesToEndpoints(remotes []string) ([]*dirk.Endpoint, error) {
func relockAccount(locker e2wtypes.AccountLocker) {
errCheck(locker.Lock(context.Background()), "failed to re-lock account")
}
func commandPath(cmd *cobra.Command) string {
path := ""
for {
path = fmt.Sprintf("%s/%s", cmd.Name(), path)
if cmd.Parent().Name() == "ethdo" {
return strings.TrimRight(path, "/")
}
cmd = cmd.Parent()
}
}

View File

@@ -54,7 +54,7 @@ func input(ctx context.Context) (*dataIn, error) {
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
return nil, err
}
return data, nil

View File

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

View File

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

@@ -18,7 +18,6 @@ import (
"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"
@@ -35,7 +34,8 @@ type dataIn struct {
// Operation.
eth2Client eth2client.Service
chainTime chaintime.Service
epoch spec.Epoch
epoch int64
period string
}
func input(ctx context.Context) (*dataIn, error) {
@@ -53,7 +53,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
}
// Chain time.
@@ -67,12 +67,8 @@ func input(ctx context.Context) (*dataIn, error) {
}
// Epoch
epoch := viper.GetInt64("epoch")
if epoch == -1 {
data.epoch = data.chainTime.CurrentEpoch()
} else {
data.epoch = spec.Epoch(epoch)
}
data.epoch = viper.GetInt64("epoch")
data.period = viper.GetString("period")
return data, nil
}

View File

@@ -65,7 +65,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",
},
}

View File

@@ -16,8 +16,10 @@ 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"
)
@@ -26,11 +28,12 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
if data.epoch < data.chainTime.AltairInitialEpoch() {
return nil, errors.New("not an Altair epoch")
epoch, err := calculateEpoch(ctx, data)
if err != nil {
return nil, err
}
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, fmt.Sprintf("%d", data.chainTime.FirstSlotOfEpoch(data.epoch)))
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", epoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain sync committee information")
}
@@ -48,3 +51,27 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
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

@@ -56,7 +56,7 @@ func TestProcess(t *testing.T) {
dataIn: &dataIn{
eth2Client: eth2Client,
chainTime: chainTime,
epoch: 61650,
epoch: -1,
},
},
}

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

@@ -28,7 +28,9 @@ var synccommitteeMembersCmd = &cobra.Command{
ethdo synccommittee members --epoch=12345
In quiet mode this will return 0 if the synccommittee members are found, otherwise 1.`,
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 {
@@ -48,10 +50,14 @@ 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.12.2)"
var ReleaseVersion = "local build (latest release 1.18.2)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

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.

51
go.mod
View File

@@ -4,45 +4,48 @@ go 1.16
require (
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/attestantio/dirk v1.0.3
github.com/attestantio/go-eth2-client v0.7.2
github.com/ferranbt/fastssz v0.0.0-20210905181407-59cf6761a7d5
github.com/gofrs/uuid v4.0.0+incompatible
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/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-20210902234237-7763804ee078
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/mitchellh/mapstructure v1.4.2 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
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-20210809151128-385d8c5e3fb7
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388
github.com/rs/zerolog v1.23.0
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.2
github.com/wealdtech/go-eth2-types/v2 v2.5.6
github.com/wealdtech/go-eth2-util v1.6.5
github.com/wealdtech/go-eth2-wallet v1.14.6
github.com/wealdtech/go-eth2-wallet-dirk v1.1.7
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.1.6
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.5.5
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.4
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.15
github.com/wealdtech/go-eth2-wallet-store-s3 v1.9.5
github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.3
github.com/wealdtech/go-eth2-wallet-types/v2 v2.8.5
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-20220128200615-198e4374d7ed // indirect
golang.org/x/text v0.3.7
google.golang.org/grpc v1.40.0
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
)
replace github.com/fsnotify/fsnotify v1.5.0 => github.com/fsnotify/fsnotify v1.5.1

498
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@ type Service struct {
slotsPerEpoch uint64
epochsPerSyncCommitteePeriod uint64
altairForkEpoch phase0.Epoch
bellatrixForkEpoch phase0.Epoch
}
// module-wide log.
@@ -92,12 +93,20 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
}
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
@@ -213,3 +222,28 @@ func fetchAltairForkEpoch(ctx context.Context, provider eth2client.ForkScheduleP
}
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

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