// Copyright © 2019 - 2023 Weald Technology Trading. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package blockinfo import ( "bytes" "context" "encoding/hex" "fmt" "math/big" "regexp" "sort" "strconv" "strings" "time" "unicode/utf8" eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/prysmaticlabs/go-bitfield" "github.com/wealdtech/go-string2eth" ) type dataOut struct { debug bool verbose bool eth2Client eth2client.Service genesisTime time.Time slotDuration time.Duration slotsPerEpoch uint64 } func output(_ context.Context, data *dataOut) (string, error) { if data == nil { return "", errors.New("no data") } return "", nil } func outputBlockGeneral(ctx context.Context, verbose bool, slot phase0.Slot, proposerIndex phase0.ValidatorIndex, blockRoot phase0.Root, bodyRoot phase0.Root, parentRoot phase0.Root, stateRoot phase0.Root, graffiti []byte, genesisTime time.Time, slotDuration time.Duration, slotsPerEpoch uint64, ) ( string, error, ) { res := strings.Builder{} res.WriteString(fmt.Sprintf("Slot: %d\n", slot)) res.WriteString(fmt.Sprintf("Proposing validator index: %d\n", proposerIndex)) res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch))) res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0))) res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot)) res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot)) if verbose { res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot)) res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot)) } res.WriteString(blockGraffiti(ctx, graffiti)) return res.String(), nil } func outputBlockETH1Data(_ context.Context, eth1Data *phase0.ETH1Data) (string, error) { res := strings.Builder{} res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount)) res.WriteString(fmt.Sprintf("Ethereum 1 deposit root: %#x\n", eth1Data.DepositRoot)) res.WriteString(fmt.Sprintf("Ethereum 1 block hash: %#x\n", eth1Data.BlockHash)) return res.String(), nil } func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*phase0.Attestation) (string, error) { res := strings.Builder{} validatorCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex) res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations))) if verbose { beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider) if isProvider { for i, att := range attestations { res.WriteString(fmt.Sprintf(" %d:\n", i)) // Fetch committees for this epoch if not already obtained. committees, exists := validatorCommittees[att.Data.Slot] if !exists { response, err := beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{ State: fmt.Sprintf("%d", att.Data.Slot), }) if err != nil { // 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 response.Data { if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists { validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex) } validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators } } committees = validatorCommittees[att.Data.Slot] } res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index)) res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len())) res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits))) if _, exists := committees[att.Data.Index]; exists { res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees, []int{int(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)) res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root)) res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch)) res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root)) } } } return res.String(), nil } func outputElectraBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*electra.Attestation) (string, error) { res := strings.Builder{} validatorCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex) res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations))) if verbose { beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider) if isProvider { for i, att := range attestations { res.WriteString(fmt.Sprintf(" %d:\n", i)) // Fetch committees for this epoch if not already obtained. committees, exists := validatorCommittees[att.Data.Slot] if !exists { response, err := beaconCommitteesProvider.BeaconCommittees(ctx, &api.BeaconCommitteesOpts{ State: fmt.Sprintf("%d", att.Data.Slot), }) if err != nil { // 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 response.Data { if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists { validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex) } validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators } } committees = validatorCommittees[att.Data.Slot] } committeeIndices := make([]phase0.CommitteeIndex, 0) for _, committeeIndex := range att.CommitteeBits.BitIndices() { committeeIndices = append(committeeIndices, phase0.CommitteeIndex(committeeIndex)) } res.WriteString(fmt.Sprintf(" Committee indices: %d\n", committeeIndices)) 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))) if _, exists := committees[att.Data.Index]; exists { res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees, att.CommitteeBits.BitIndices()))) } 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)) res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root)) res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch)) res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root)) } } } return res.String(), nil } func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*phase0.AttesterSlashing) (string, error) { res := strings.Builder{} res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings))) if verbose { for i, slashing := range attesterSlashings { // Say what was slashed. att1 := slashing.Attestation1 att2 := slashing.Attestation2 slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices) if len(slashedIndices) == 0 { continue } res.WriteString(fmt.Sprintf(" %d:\n", i)) res.WriteString(fmt.Sprintln(" Slashed validators:")) response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{ State: "head", Indices: slashedIndices, }) if err != nil { return "", errors.Wrap(err, "failed to obtain beacon committees") } for k, v := range response.Data { res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k)) } // Say what caused the slashing. if att1.Data.Target.Epoch == att2.Data.Target.Epoch { res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch)) if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) { res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root)) res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root)) } if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) { res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot)) res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot)) } } else if att1.Data.Source.Epoch < att2.Data.Source.Epoch && att1.Data.Target.Epoch > att2.Data.Target.Epoch { res.WriteString(" Surround voted:\n") res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch)) res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch)) } } } return res.String(), nil } func outputElectraBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*electra.AttesterSlashing) (string, error) { res := strings.Builder{} res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings))) if verbose { for i, slashing := range attesterSlashings { // Say what was slashed. att1 := slashing.Attestation1 att2 := slashing.Attestation2 slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices) if len(slashedIndices) == 0 { continue } res.WriteString(fmt.Sprintf(" %d:\n", i)) res.WriteString(fmt.Sprintln(" Slashed validators:")) response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{ State: "head", Indices: slashedIndices, }) if err != nil { return "", errors.Wrap(err, "failed to obtain beacon committees") } for k, v := range response.Data { res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k)) } // Say what caused the slashing. if att1.Data.Target.Epoch == att2.Data.Target.Epoch { res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch)) if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) { res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root)) res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root)) } if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) { res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot)) res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot)) } } else if att1.Data.Source.Epoch < att2.Data.Source.Epoch && att1.Data.Target.Epoch > att2.Data.Target.Epoch { res.WriteString(" Surround voted:\n") res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch)) res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch)) } } } return res.String(), nil } func outputBlockDeposits(_ context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) { res := strings.Builder{} // Deposits. res.WriteString(fmt.Sprintf("Deposits: %d\n", len(deposits))) if verbose { for i, deposit := range deposits { data := deposit.Data res.WriteString(fmt.Sprintf(" %d:\n", i)) res.WriteString(fmt.Sprintf(" Public key: %#x\n", data.PublicKey)) res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(data.Amount), true))) res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", data.WithdrawalCredentials)) res.WriteString(fmt.Sprintf(" Signature: %#x\n", data.Signature)) } } return res.String(), nil } func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Service, verbose bool, voluntaryExits []*phase0.SignedVoluntaryExit) (string, error) { res := strings.Builder{} res.WriteString(fmt.Sprintf("Voluntary exits: %d\n", len(voluntaryExits))) if verbose { for i, voluntaryExit := range voluntaryExits { res.WriteString(fmt.Sprintf(" %d:\n", i)) response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{ State: "head", Indices: []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex}, }) if err != nil { res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err)) } else { res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[voluntaryExit.Message.ValidatorIndex].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex)) res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch)) } } } return res.String(), nil } func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client.Service, verbose bool, ops []*capella.SignedBLSToExecutionChange) (string, error) { res := strings.Builder{} res.WriteString(fmt.Sprintf("BLS to execution changes: %d\n", len(ops))) if verbose { for i, op := range ops { res.WriteString(fmt.Sprintf(" %d:\n", i)) response, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{ State: "head", Indices: []phase0.ValidatorIndex{op.Message.ValidatorIndex}, }) if err != nil { res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err)) } else { res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", response.Data[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex)) res.WriteString(fmt.Sprintf(" BLS public key: %#x\n", op.Message.FromBLSPubkey)) res.WriteString(fmt.Sprintf(" Execution address: %s\n", op.Message.ToExecutionAddress.String())) } } } return res.String(), nil } func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service, verbose bool, syncAggregate *altair.SyncAggregate, epoch phase0.Epoch) (string, error) { res := strings.Builder{} res.WriteString("Sync aggregate: ") res.WriteString(fmt.Sprintf("%d/%d\n", syncAggregate.SyncCommitteeBits.Count(), syncAggregate.SyncCommitteeBits.Len())) if verbose { specProvider, isProvider := eth2Client.(eth2client.SpecProvider) if isProvider { specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{}) if err == nil { slotsPerEpoch := specResponse.Data["SLOTS_PER_EPOCH"].(uint64) res.WriteString(" Contributions: ") res.WriteString(bitvectorToString(syncAggregate.SyncCommitteeBits)) res.WriteString("\n") syncCommitteesProvider, isProvider := eth2Client.(eth2client.SyncCommitteesProvider) if isProvider { syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{ State: strconv.FormatUint(uint64(epoch)*slotsPerEpoch, 10), }) if err != nil { res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err)) } else { res.WriteString(" Contributing validators:") for i := range syncAggregate.SyncCommitteeBits.Len() { if syncAggregate.SyncCommitteeBits.BitAt(i) { res.WriteString(fmt.Sprintf(" %d", syncCommitteeResponse.Data.Validators[i])) } } res.WriteString("\n") } } } } } return res.String(), nil } func outputCapellaBlockText(ctx context.Context, data *dataOut, signedBlock *capella.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, signedBlock.Message.ProposerIndex, 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 = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputCapellaBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload) if err != nil { return "", err } res.WriteString(tmp) return res.String(), nil } func outputDenebBlockText(ctx context.Context, data *dataOut, signedBlock *deneb.SignedBeaconBlock, blobs []*deneb.BlobSidecar, ) ( 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, signedBlock.Message.ProposerIndex, 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 = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputDenebBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputBlobInfo(ctx, data.verbose, signedBlock.Message.Body.BlobKZGCommitments, blobs) if err != nil { return "", err } res.WriteString(tmp) return res.String(), nil } func outputElectraBlockText(ctx context.Context, data *dataOut, signedBlock *electra.SignedBeaconBlock, blobs []*deneb.BlobSidecar, ) ( 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, signedBlock.Message.ProposerIndex, 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 = outputElectraBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations) if err != nil { return "", err } res.WriteString(tmp) // Attester slashings. tmp, err = outputElectraBlockAttesterSlashings(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. // Voluntary exits. tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputDenebBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputElectraBlockExecutionRequests(ctx, data.verbose, signedBlock.Message.Body.ExecutionRequests) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputBlobInfo(ctx, data.verbose, signedBlock.Message.Body.BlobKZGCommitments, blobs) if err != nil { return "", err } res.WriteString(tmp) return res.String(), nil } func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) { if signedBlock == nil { return "", errors.New("no block supplied") } body := signedBlock.Message.Body res := strings.Builder{} // General info. blockRoot, err := signedBlock.Message.HashTreeRoot() if err != nil { return "", errors.Wrap(err, "failed to obtain block root") } bodyRoot, err := signedBlock.Message.Body.HashTreeRoot() if err != nil { return "", errors.Wrap(err, "failed to generate body root") } tmp, err := outputBlockGeneral(ctx, data.verbose, signedBlock.Message.Slot, signedBlock.Message.ProposerIndex, blockRoot, bodyRoot, signedBlock.Message.ParentRoot, signedBlock.Message.StateRoot, signedBlock.Message.Body.Graffiti[:], data.genesisTime, data.slotDuration, data.slotsPerEpoch) if err != nil { return "", err } res.WriteString(tmp) // Eth1 data. if data.verbose { tmp, err := outputBlockETH1Data(ctx, body.ETH1Data) if err != nil { return "", err } res.WriteString(tmp) } // Sync aggregate. tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch)) if err != nil { return "", err } res.WriteString(tmp) // Attestations. tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations) if err != nil { return "", err } res.WriteString(tmp) // Attester slashings. tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings) if err != nil { return "", err } res.WriteString(tmp) res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings))) // Add verbose proposer slashings. tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits) if err != nil { return "", err } res.WriteString(tmp) // Voluntary exits. tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits) if err != nil { return "", err } res.WriteString(tmp) tmp, err = outputBellatrixBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload) if err != nil { return "", err } res.WriteString(tmp) return res.String(), nil } func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *altair.SignedBeaconBlock) (string, error) { if signedBlock == nil { return "", errors.New("no block supplied") } body := signedBlock.Message.Body res := strings.Builder{} // General info. blockRoot, err := signedBlock.Message.HashTreeRoot() if err != nil { return "", errors.Wrap(err, "failed to obtain block root") } bodyRoot, err := signedBlock.Message.Body.HashTreeRoot() if err != nil { return "", errors.Wrap(err, "failed to generate body root") } tmp, err := outputBlockGeneral(ctx, data.verbose, signedBlock.Message.Slot, signedBlock.Message.ProposerIndex, blockRoot, bodyRoot, signedBlock.Message.ParentRoot, signedBlock.Message.StateRoot, signedBlock.Message.Body.Graffiti[:], data.genesisTime, data.slotDuration, data.slotsPerEpoch) if err != nil { return "", err } res.WriteString(tmp) // Eth1 data. if data.verbose { tmp, err := outputBlockETH1Data(ctx, body.ETH1Data) if err != nil { return "", err } res.WriteString(tmp) } // Sync aggregate. tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch)) if err != nil { return "", err } res.WriteString(tmp) // Attestations. tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations) if err != nil { return "", err } res.WriteString(tmp) // Attester slashings. tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings) if err != nil { return "", err } res.WriteString(tmp) res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings))) // Add verbose proposer slashings. tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits) if err != nil { return "", err } res.WriteString(tmp) // Voluntary exits. tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits) if err != nil { return "", err } res.WriteString(tmp) return res.String(), nil } func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phase0.SignedBeaconBlock) (string, error) { if signedBlock == nil { return "", errors.New("no block supplied") } body := signedBlock.Message.Body res := strings.Builder{} // General info. blockRoot, err := signedBlock.Message.HashTreeRoot() if err != nil { return "", errors.Wrap(err, "failed to obtain block root") } bodyRoot, err := signedBlock.Message.Body.HashTreeRoot() if err != nil { return "", errors.Wrap(err, "failed to generate block root") } tmp, err := outputBlockGeneral(ctx, data.verbose, signedBlock.Message.Slot, signedBlock.Message.ProposerIndex, 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) } // Attestations. tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations) if err != nil { return "", err } res.WriteString(tmp) // Attester slashings. tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings) if err != nil { return "", err } res.WriteString(tmp) res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings))) // Add verbose proposer slashings. tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits) if err != nil { return "", err } res.WriteString(tmp) // Voluntary exits. tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits) if err != nil { return "", err } res.WriteString(tmp) return res.String(), nil } func outputCapellaBlockExecutionPayload(_ context.Context, verbose bool, payload *capella.ExecutionPayload, ) ( string, error, ) { if payload == nil { return "", nil } // If the block number is 0 then we're before the merge. if payload.BlockNumber == 0 { return "", nil } res := strings.Builder{} if !verbose { res.WriteString("Execution block number: ") res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber)) res.WriteString("Transactions: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions))) } else { res.WriteString("Execution payload:\n") res.WriteString(" Execution block number: ") res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber)) baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas)) for i := range 32 { 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(payload.FeeRecipient.String()) 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: ") if utf8.Valid(payload.ExtraData) { res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData))) } else { res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData)) } res.WriteString(" Logs bloom: ") res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom)) res.WriteString(" Transactions: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions))) res.WriteString(" Withdrawals: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals))) } return res.String(), nil } func outputDenebBlockExecutionPayload(_ context.Context, verbose bool, payload *deneb.ExecutionPayload, ) ( string, error, ) { if payload == nil { return "", nil } // If the block number is 0 then we're before the merge. if payload.BlockNumber == 0 { return "", nil } res := strings.Builder{} if !verbose { res.WriteString("Execution block number: ") res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber)) res.WriteString("Transactions: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions))) } else { res.WriteString("Execution payload:\n") res.WriteString(" Execution block number: ") res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber)) res.WriteString(" Base fee per gas: ") res.WriteString(string2eth.WeiToString(payload.BaseFeePerGas.ToBig(), 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(payload.FeeRecipient.String()) 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: ") if utf8.Valid(payload.ExtraData) { res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData))) } else { res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData)) } res.WriteString(" Logs bloom: ") res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom)) res.WriteString(" Transactions: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions))) res.WriteString(" Withdrawals: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals))) res.WriteString(" Excess blob gas: ") res.WriteString(fmt.Sprintf("%d\n", payload.ExcessBlobGas)) } return res.String(), nil } func outputElectraBlockExecutionRequests(_ context.Context, verbose bool, executionRequests *electra.ExecutionRequests, ) ( string, error, ) { if executionRequests == nil { return "", nil } res := strings.Builder{} res.WriteString("Deposit requests: ") res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Deposits))) if verbose { for i, deposit := range executionRequests.Deposits { res.WriteString(fmt.Sprintf("%3d:\n", i)) res.WriteString(fmt.Sprintf(" Public key: %#x\n", deposit.Pubkey)) res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", deposit.WithdrawalCredentials)) res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(deposit.Amount), true))) } } res.WriteString("Withdrawal requests: ") res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Withdrawals))) if verbose { for i, withdrawal := range executionRequests.Withdrawals { res.WriteString(fmt.Sprintf("%3d:\n", i)) res.WriteString(fmt.Sprintf(" Source address: %#x\n", withdrawal.SourceAddress)) res.WriteString(fmt.Sprintf(" Validator public key: %#x\n", withdrawal.ValidatorPubkey)) res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(withdrawal.Amount), true))) } } res.WriteString("Consolidation requests: ") res.WriteString(fmt.Sprintf("%d\n", len(executionRequests.Consolidations))) if verbose { for i, consolidation := range executionRequests.Consolidations { res.WriteString(fmt.Sprintf("%3d:\n", i)) res.WriteString(fmt.Sprintf(" Source address: %#x\n", consolidation.SourceAddress)) res.WriteString(fmt.Sprintf(" Source public key: %#x\n", consolidation.SourcePubkey)) res.WriteString(fmt.Sprintf(" Target public key: %#x\n", consolidation.TargetPubkey)) } } return res.String(), nil } func outputBlobInfo(_ context.Context, verbose bool, commitments []deneb.KZGCommitment, blobs []*deneb.BlobSidecar, ) ( string, error, ) { res := strings.Builder{} if len(blobs) == 0 && len(commitments) > 0 { res.WriteString(fmt.Sprintf("Blobs: %d (but no blobs obtained from the beacon node)\n", len(commitments))) } else { res.WriteString(fmt.Sprintf("Blobs: %d\n", len(blobs))) if verbose { for i, blob := range blobs { res.WriteString(fmt.Sprintf("%3d:\n", i)) res.WriteString(fmt.Sprintf(" KZG proof: %s\n", blob.KZGProof.String())) res.WriteString(fmt.Sprintf(" KZG commitment: %s\n", blob.KZGCommitment.String())) } } } return res.String(), nil } func outputBellatrixBlockExecutionPayload(_ context.Context, verbose bool, payload *bellatrix.ExecutionPayload, ) ( string, error, ) { if payload == nil { return "", nil } // If the block number is 0 then we're before the merge. if payload.BlockNumber == 0 { return "", nil } res := strings.Builder{} if !verbose { res.WriteString("Execution block number: ") res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber)) res.WriteString("Transactions: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions))) } else { res.WriteString("Execution payload:\n") res.WriteString(" Execution block number: ") res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber)) baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas)) for i := range 32 { 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(payload.FeeRecipient.String()) res.WriteString(" Gas limit: ") res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit)) res.WriteString(" Gas used: ") res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed)) res.WriteString(" Timestamp: ") res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp)) res.WriteString(" Prev RANDAO: ") res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao)) res.WriteString(" Receipts root: ") res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot)) res.WriteString(" State root: ") res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot)) res.WriteString(" Extra data: ") res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData)) res.WriteString(" Logs bloom: ") res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom)) res.WriteString(" Transactions: ") res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions))) } return res.String(), nil } // 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] }) sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] }) res := make([]phase0.ValidatorIndex, 0) set1Pos := 0 set2Pos := 0 for set1Pos < len(set1) && set2Pos < len(set2) { switch { case set1[set1Pos] < set2[set2Pos]: set1Pos++ case set2[set2Pos] < set1[set1Pos]: set2Pos++ default: res = append(res, phase0.ValidatorIndex(set1[set1Pos])) set1Pos++ set2Pos++ } } return res } func bitlistToString(input bitfield.Bitlist) string { bits := int(input.Len()) res := "" for i := range bits { if input.BitAt(uint64(i)) { res = fmt.Sprintf("%s✓", res) } else { res = fmt.Sprintf("%s✕", res) } if i%8 == 7 { res = fmt.Sprintf("%s ", res) } } return strings.TrimSpace(res) } func bitvectorToString(input bitfield.Bitvector512) string { bits := int(input.Len()) res := strings.Builder{} for i := range bits { if input.BitAt(uint64(i)) { res.WriteString("✓") } else { res.WriteString("✕") } if i%8 == 7 && i != bits-1 { res.WriteString(" ") } } return res.String() } func attestingIndices(input bitfield.Bitlist, committees map[phase0.CommitteeIndex][]phase0.ValidatorIndex, includedCommittees []int, ) string { bits := int(input.Len()) // Build up the validator list from the included committees. validatorIndices := make([]phase0.ValidatorIndex, 0) for _, committeeIndex := range includedCommittees { validatorIndices = append(validatorIndices, committees[phase0.CommitteeIndex(committeeIndex)]...) } res := strings.Builder{} for i := range bits { if input.BitAt(uint64(i)) { // Work out the committee and offset given the index. res.WriteString(fmt.Sprintf("%d ", validatorIndices[i])) } } return strings.TrimSpace(res.String()) } func blockGraffiti(_ context.Context, graffiti []byte) string { if len(graffiti) == 0 || hex.EncodeToString(graffiti) == "0000000000000000000000000000000000000000000000000000000000000000" { // No graffiti. return "" } // Remove any trailing null characters. graffiti = bytes.TrimRight(graffiti, "\u0000") if !utf8.Valid(graffiti) { // Graffiti is not valid UTF-8, return hex. return fmt.Sprintf("Graffiti: %#x\n", graffiti) } // See if there is client identification information present in the graffiti. // The client identification will always be the last entry in the graffiti, with a space beforehand. parts := bytes.Split(graffiti, []byte{' '}) // Consensus and execution client values come from // https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md consensusClients := map[string]string{ "GR": "grandine", "LH": "lighthouse", "LS": "lodestar", "NB": "nimbus", "PM": "prysm", "TK": "teku", } consensusRegex := regexp.MustCompile(`(GR|LH|LS|NB|PM|TK)([0-9a-f]*)`) consensusData := consensusRegex.Find(parts[len(parts)-1]) executionClients := map[string]string{ "BU": "besu", "EG": "erigon", "EJ": "ethereumJS", "GE": "go-ethereum", "NM": "nethermind", "RH": "reth", "TR": "trin-execution", } executionRegex := regexp.MustCompile(`(BU|EG|EJ|GE|NM|RH|TR)([0-9a-f]*)`) executionData := executionRegex.Find(parts[len(parts)-1]) if len(consensusData) == 0 && len(executionData) == 0 { // There is no identifier; return the graffiti as-is. return fmt.Sprintf("Graffiti: %s\n", string(graffiti)) } res := strings.Builder{} truncatedGraffiti := bytes.Join(parts[0:len(parts)-1], []byte(" ")) if len(truncatedGraffiti) > 0 { res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(truncatedGraffiti))) } if len(consensusData) > 0 { consensusClient := consensusData[0:2] consensusHash := "" if len(consensusData) > 2 { consensusHash = string(consensusData[2:]) } res.WriteString("Consensus client: ") res.WriteString(consensusClients[string(consensusClient)]) if consensusHash != "" { res.WriteString(" (version hash ") res.WriteString(consensusHash) res.WriteString(")") } res.WriteString("\n") } if len(executionData) > 0 { executionClient := executionData[0:2] executionHash := "" if len(executionData) > 2 { executionHash = string(executionData[2:]) } res.WriteString("Execution client: ") res.WriteString(executionClients[string(executionClient)]) if executionHash != "" { res.WriteString(" (version hash ") res.WriteString(executionHash) res.WriteString(")") } res.WriteString("\n") } return res.String() }