// 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 // // 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 ( "context" "fmt" "os" "strconv" "strings" "time" eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" apiv1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/spf13/cobra" "github.com/spf13/viper" standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" "github.com/wealdtech/ethdo/util" string2eth "github.com/wealdtech/go-string2eth" ) var chainStatusCmd = &cobra.Command{ Use: "status", Short: "Obtain status about a chain", Long: `Obtain status about a chain. For example: ethdo chain status In quiet mode this will return 0 if the chain status can be obtained, otherwise 1.`, Run: func(_ *cobra.Command, _ []string) { ctx := context.Background() eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ Address: viper.GetString("connection"), Timeout: viper.GetDuration("timeout"), AllowInsecure: viper.GetBool("allow-insecure-connections"), LogFallback: !viper.GetBool("quiet"), }) errCheck(err, "Failed to connect to Ethereum 2 beacon node") chainTime, err := standardchaintime.New(ctx, standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)), 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") finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{ State: "head", }) errCheck(err, "Failed to obtain finality information") finality := finalityResponse.Data blockProvider, isProvider := eth2Client.(eth2client.SignedBeaconBlockProvider) assert(isProvider, "beacon node does not provide signed beacon blocks; cannot report on chain status") blockResponse, err := blockProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{Block: "head"}) errCheck(err, "Failed to obtain block information") block := blockResponse.Data slot := chainTime.CurrentSlot() 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) headSlot, err := block.Slot() errCheck(err, "Failed to obtain block slot") res := strings.Builder{} res.WriteString("Current slot: ") res.WriteString(fmt.Sprintf("%d", slot)) res.WriteString("\n") res.WriteString("Head slot: ") res.WriteString(fmt.Sprintf("%d", headSlot)) if headSlot != slot { if slot-headSlot == 1 { res.WriteString(" (1 slot behind)") } else { res.WriteString(" (") res.WriteString(fmt.Sprintf("%d", slot-headSlot)) res.WriteString(" slots behind)") } } res.WriteString("\n") res.WriteString("Current epoch: ") res.WriteString(fmt.Sprintf("%d", epoch)) res.WriteString("\n") if viper.GetBool("verbose") { 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 viper.GetBool("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 viper.GetBool("verbose") { distance := epoch - finality.Finalized.Epoch res.WriteString("Finalized epoch distance: ") res.WriteString(fmt.Sprintf("%d", distance)) res.WriteString("\n") } if viper.GetBool("verbose") { validatorsProvider, isProvider := eth2Client.(eth2client.ValidatorsProvider) if isProvider { validatorsResponse, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{State: "head"}) errCheck(err, "Failed to obtain validators information") // Stats of inteest. totalBalance := phase0.Gwei(0) activeEffectiveBalance := phase0.Gwei(0) validatorCount := make(map[apiv1.ValidatorState]int) for _, validator := range validatorsResponse.Data { validatorCount[validator.Status]++ totalBalance += validator.Balance if validator.Status.IsActive() { activeEffectiveBalance += validator.Validator.EffectiveBalance } } res.WriteString(fmt.Sprintf("Total balance: %s\n", string2eth.GWeiToString(uint64(totalBalance), true))) res.WriteString(fmt.Sprintf("Active effective balance: %s\n", string2eth.GWeiToString(uint64(activeEffectiveBalance), true))) res.WriteString("Validator states:\n") res.WriteString(fmt.Sprintf(" Pending: %d\n", validatorCount[apiv1.ValidatorStatePendingInitialized])) res.WriteString(fmt.Sprintf(" Activating: %d\n", validatorCount[apiv1.ValidatorStatePendingQueued])) res.WriteString(fmt.Sprintf(" Active: %d\n", validatorCount[apiv1.ValidatorStateActiveOngoing]+validatorCount[apiv1.ValidatorStateActiveSlashed])) res.WriteString(fmt.Sprintf(" Exiting: %d\n", validatorCount[apiv1.ValidatorStateActiveExiting])) res.WriteString(fmt.Sprintf(" Exited: %d\n", validatorCount[apiv1.ValidatorStateExitedUnslashed]+validatorCount[apiv1.ValidatorStateExitedSlashed]+validatorCount[apiv1.ValidatorStateWithdrawalPossible]+validatorCount[apiv1.ValidatorStateWithdrawalDone])) res.WriteString(fmt.Sprintf(" Unknown: %d\n", validatorCount[apiv1.ValidatorStateUnknown])) } } if epoch >= chainTime.AltairInitialEpoch() { period := chainTime.SlotToSyncCommitteePeriod(slot) periodStartEpoch := chainTime.FirstEpochOfSyncPeriod(period) 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(strconv.FormatUint(period, 10)) res.WriteString("\n") if viper.GetBool("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) }, } func init() { chainCmd.AddCommand(chainStatusCmd) chainFlags(chainStatusCmd) }