Compare commits

...

28 Commits

Author SHA1 Message Date
Jim McDonald
ac18cbab3e Bump version. 2021-10-27 14:30:02 +01:00
Jim McDonald
2f1c89d0a6 Update for test. 2021-10-26 18:02:51 +01:00
Jim McDonald
a3ad4181d3 Add exit/withdrawable epoch to validator info. 2021-10-26 17:29:38 +01:00
Jim McDonald
f8ac23e8d7 Merge pull request #39 from Blockdaemon/prater_launchpad
Output "prater" as network name in launchpad format
2021-10-01 15:13:06 +01:00
Jonas Pfannschmidt
b6815d1a2a Output "prater" as network name in launchpad format
Before it would output "unknown" in "eth2_network_name" when creating
a launchpad file using prater's fork version: 0x00001020
2021-10-01 12:19:03 +01:00
Jim McDonald
a79b813bd0 Add "chain verify signedcontributionandproof". 2021-09-28 20:34:01 +01:00
Jim McDonald
ad971145f0 Show both block and body root in "block info". 2021-09-25 17:21:33 +01:00
Jim McDonald
602948921c Generate SHA256 of compressed file. 2021-09-24 12:09:07 +01:00
Jim McDonald
607e969a30 Rework chain status output. 2021-09-24 08:52:09 +01:00
Jim McDonald
79f1ae9930 Update dependencies. 2021-09-21 14:23:17 +01:00
Jim McDonald
a98f681f98 Fix bad dependency. 2021-09-21 13:58:37 +01:00
Jim McDonald
e0e1f697d3 Bump version. 2021-09-21 13:47:33 +01:00
Jim McDonald
1b70a66120 Update workflow. 2021-09-21 13:44:02 +01:00
Jim McDonald
94eba96a6e Tidy-ups. 2021-09-15 22:50:49 +01:00
Jim McDonald
f052d8e307 Update dependencies. 2021-09-15 08:54:07 +01:00
Jim McDonald
df45686828 Update dependencies. 2021-09-15 08:51:28 +01:00
Jim McDonald
84d228877a Documentation updates. 2021-08-28 20:18:29 +01:00
Jim McDonald
b2b26742b0 Fix documentation. 2021-08-28 20:04:52 +01:00
Jim McDonald
9dc630c809 Add synccommittee members. 2021-08-21 00:08:50 +01:00
Jim McDonald
452430db56 Linting. 2021-08-19 13:52:06 +01:00
Jim McDonald
b0d676a734 Update version. 2021-08-19 13:50:29 +01:00
Jim McDonald
ff73470085 Merge pull request #38 from wealdtech/altair
Altair block info
2021-08-19 13:48:18 +01:00
Jim McDonald
a41349999f Add block info for Altair. 2021-08-19 13:44:06 +01:00
Jim McDonald
004f4bc41a Update for block info. 2021-08-19 13:42:22 +01:00
Jim McDonald
64c8e1a051 Updates for Altair. 2021-08-19 13:41:42 +01:00
Jim McDonald
d95d48f6b2 Add more data to "chain info". 2021-08-19 13:18:43 +01:00
Jim McDonald
3e702f0c51 Bump go build version. 2021-08-03 23:50:58 +01:00
Jim McDonald
2e36fcc3ce Use local shamir codebase. 2021-08-03 23:34:16 +01:00
47 changed files with 3356 additions and 1291 deletions

View File

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

View File

@@ -1,3 +1,21 @@
1.14.0:
- add "chain verify signedcontributionandproof"
- show both block and body root in "block info"
- add exit / withdrawable epoch to "validator info"
1.13.0:
- rework and provide additional information to "chain status" output
1.12.0:
- add "synccommittee members"
1.11.0
- add Altair information to "block info"
- add more information to "chain info"
1.10.2
- use local shamir code (copied from github.com/hashicorp/vault)
1.10.0
- add "wallet sharedexport" and "wallet sharedimport"

View File

@@ -1,4 +1,4 @@
FROM golang:1.14-buster as builder
FROM golang:1.16-buster as builder
WORKDIR /app
@@ -16,4 +16,4 @@ WORKDIR /app
COPY --from=builder /app/ethdo /app
ENTRYPOINT ["/app/ethdo"]
ENTRYPOINT ["/app/ethdo"]

View File

@@ -21,7 +21,7 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
@@ -53,7 +53,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
// Fetch validator
pubKeys := make([]spec.BLSPubKey, 1)
pubKeys := make([]phase0.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
@@ -92,13 +92,21 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if signedBlock == nil {
continue
}
if signedBlock.Message.Slot != slot {
blockSlot, err := signedBlock.Slot()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block slot")
}
if blockSlot != slot {
continue
}
if data.debug {
fmt.Printf("Fetched block for slot %d\n", slot)
}
for i, attestation := range signedBlock.Message.Body.Attestations {
attestations, err := signedBlock.Attestations()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block attestations")
}
for i, attestation := range attestations {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
@@ -113,9 +121,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}

View File

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

View File

@@ -21,7 +21,9 @@ import (
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
@@ -55,9 +57,20 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
if err := outputBlock(ctx, data.jsonOutput, signedBlock); err != nil {
return nil, errors.Wrap(err, "failed to output block")
if signedBlock == nil {
return nil, errors.New("empty beacon block")
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, signedBlock.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
if data.stream {
@@ -82,13 +95,30 @@ func headEventHandler(event *api.Event) {
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
if err != nil {
fmt.Printf("Failed to obtain block: %v\n", err)
return
}
if err := outputBlock(context.Background(), jsonOutput, signedBlock); err != nil {
fmt.Printf("Failed to display block: %v\n", err)
if signedBlock == nil {
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)
return
}
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, signedBlock.Altair); err != nil {
fmt.Printf("Failed to output block: %v\n", err)
return
}
default:
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
return
}
}
func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedBeaconBlock) error {
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
@@ -97,7 +127,25 @@ func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedB
}
fmt.Printf("%s\n", string(data))
default:
data, err := outputBlockText(ctx, results, signedBlock)
data, err := outputPhase0BlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Printf("%s\n", data)
}
return nil
}
func outputAltairBlock(ctx context.Context, jsonOutput bool, signedBlock *altair.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
default:
data, err := outputAltairBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -17,12 +17,13 @@ import (
"context"
"fmt"
"os"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/spf13/viper"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
@@ -40,53 +41,108 @@ 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)
nextPeriod := period + 1
nextPeriodStartEpoch := chainTime.FirstEpochOfSyncPeriod(nextPeriod)
periodEndEpoch := nextPeriodStartEpoch - 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("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

@@ -1,4 +1,4 @@
// Copyright © 2019 Weald Technology Trading
// Copyright © 2019 - 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -23,6 +23,7 @@ import (
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
@@ -56,12 +57,15 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
return nil
}
// Disable service logging.
zerolog.SetGlobalLevel(zerolog.Disabled)
// We bind viper here so that we bind to the correct command.
quiet = viper.GetBool("quiet")
verbose = viper.GetBool("verbose")
debug = viper.GetBool("debug")
// Command-specific bindings.
switch fmt.Sprintf("%s/%s", cmd.Parent().Name(), cmd.Name()) {
switch commandPath(cmd) {
case "account/create":
accountCreateBindings()
case "account/derive":
@@ -76,12 +80,16 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
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/members":
synccommitteeMembersBindings()
case "validator/depositdata":
validatorDepositdataBindings()
case "validator/duties":
@@ -109,11 +117,7 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
fmt.Println("Cannot supply both quiet and debug flags")
}
if err := util.SetupStore(); err != nil {
return err
}
return nil
return util.SetupStore()
}
// Execute adds all child commands to the root command and sets flags appropriately.
@@ -387,3 +391,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()
}
}

32
cmd/synccommittee.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,68 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"testing"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Empty",
dataOut: &dataOut{},
res: "No sync committee validators found",
},
{
name: "Present",
dataOut: &dataOut{
validators: []phase0.ValidatorIndex{1, 2, 3},
},
res: "1,2,3",
},
{
name: "JSON",
dataOut: &dataOut{
json: true,
validators: []phase0.ValidatorIndex{1, 2, 3},
},
res: "[1,2,3]",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,50 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package members
import (
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.epoch < data.chainTime.AltairInitialEpoch() {
return nil, errors.New("not an Altair epoch")
}
syncCommittee, err := data.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommittee(ctx, fmt.Sprintf("%d", data.chainTime.FirstSlotOfEpoch(data.epoch)))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain sync committee information")
}
if syncCommittee == nil {
return nil, errors.New("no sync committee returned")
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
validators: syncCommittee.Validators,
}
return results, nil
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,8 +20,8 @@ import (
"fmt"
"os"
"github.com/hashicorp/vault/shamir"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/shamir"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)

View File

@@ -20,8 +20,8 @@ import (
"fmt"
"strings"
"github.com/hashicorp/vault/shamir"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/shamir"
e2wallet "github.com/wealdtech/go-eth2-wallet"
)

View File

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

View File

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

65
go.mod
View File

@@ -1,64 +1,45 @@
module github.com/wealdtech/ethdo
go 1.13
go 1.16
require (
github.com/DataDog/zstd v1.4.8 // indirect
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/attestantio/dirk v1.0.2
github.com/attestantio/go-eth2-client v0.6.30
github.com/aws/aws-sdk-go v1.40.14 // indirect
github.com/dgraph-io/badger/v2 v2.2007.3 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/ferranbt/fastssz v0.0.0-20210719200358-90640294cb9c
github.com/goccy/go-yaml v1.9.2 // indirect
github.com/attestantio/dirk v1.0.4
github.com/attestantio/go-eth2-client v0.8.0
github.com/ferranbt/fastssz v0.0.0-20210905181407-59cf6761a7d5
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/uuid v1.3.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/hashicorp/vault v1.8.0
github.com/herumi/bls-eth-go-binary v0.0.0-20210520070601-31246bfa8ac4
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/hashicorp/hcl v1.0.1-vault-3 // indirect
github.com/herumi/bls-eth-go-binary v0.0.0-20210902234237-7763804ee078
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/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.1 // indirect
github.com/protolambda/zssz v0.1.5 // indirect
github.com/prysmaticlabs/ethereumapis v0.0.0-20210201130911-92b2a467c108 // indirect
github.com/prysmaticlabs/go-bitfield v0.0.0-20210706153858-5cb5ce8bdbfe
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388
github.com/rs/zerolog v1.23.0
github.com/spf13/cast v1.4.0 // indirect
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
github.com/tyler-smith/go-bip39 v1.1.0
github.com/wealdtech/go-bytesutil v1.1.1
github.com/wealdtech/go-ecodec v1.1.1
github.com/wealdtech/go-eth2-types/v2 v2.5.5
github.com/wealdtech/go-eth2-util v1.6.4
github.com/wealdtech/go-eth2-wallet v1.14.4
github.com/wealdtech/go-eth2-wallet-dirk v1.1.6
github.com/wealdtech/go-eth2-wallet-distributed v1.1.3
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.5
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.5.4
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.3
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.14
github.com/wealdtech/go-eth2-wallet-store-s3 v1.9.4
github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.2
github.com/wealdtech/go-eth2-wallet-types/v2 v2.8.4
github.com/wealdtech/go-ecodec v1.1.2
github.com/wealdtech/go-eth2-types/v2 v2.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.8
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-string2eth v1.1.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/text v0.3.6
google.golang.org/genproto v0.0.0-20210803142424-70bd63adacf2 // indirect
google.golang.org/grpc v1.39.0
golang.org/x/text v0.3.7
google.golang.org/grpc v1.40.0
)

1132
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"time"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
// Service provides a number of functions for calculating chain-related times.
type Service interface {
// GenesisTime provides the time of the chain's genesis.
GenesisTime() time.Time
// SlotsPerEpoch provides the number of slots in the chain's epoch.
SlotsPerEpoch() uint64
// SlotDuration provides the duration of the chain's slot.
SlotDuration() time.Duration
// StartOfSlot provides the time at which a given slot starts.
StartOfSlot(slot phase0.Slot) time.Time
// StartOfEpoch provides the time at which a given epoch starts.
StartOfEpoch(epoch phase0.Epoch) time.Time
// CurrentSlot provides the current slot.
CurrentSlot() phase0.Slot
// CurrentEpoch provides the current epoch.
CurrentEpoch() phase0.Epoch
// CurrentSyncCommitteePeriod provides the current sync committee period.
CurrentSyncCommitteePeriod() uint64
// SlotToEpoch provides the epoch of the given slot.
SlotToEpoch(slot phase0.Slot) phase0.Epoch
// SlotToSyncCommitteePeriod provides the sync committee period of the given slot.
SlotToSyncCommitteePeriod(slot phase0.Slot) uint64
// FirstSlotOfEpoch provides the first slot of the given epoch.
FirstSlotOfEpoch(epoch phase0.Epoch) phase0.Slot
// TimestampToSlot provides the slot of the given timestamp.
TimestampToSlot(timestamp time.Time) phase0.Slot
// TimestampToEpoch provides the epoch of the given timestamp.
TimestampToEpoch(timestamp time.Time) phase0.Epoch
// FirstEpochOfSyncPeriod provides the first epoch of the given sync period.
FirstEpochOfSyncPeriod(period uint64) phase0.Epoch
// AltairInitialEpoch provides the epoch at which the Altair hard fork takes place.
AltairInitialEpoch() phase0.Epoch
// AltairInitialSyncCommitteePeriod provides the sync committee period in which the Altair hard fork takes place.
AltairInitialSyncCommitteePeriod() uint64
}

View File

@@ -0,0 +1,90 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package standard
import (
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
type parameters struct {
logLevel zerolog.Level
genesisTimeProvider eth2client.GenesisTimeProvider
specProvider eth2client.SpecProvider
forkScheduleProvider eth2client.ForkScheduleProvider
}
// Parameter is the interface for service parameters.
type Parameter interface {
apply(*parameters)
}
type parameterFunc func(*parameters)
func (f parameterFunc) apply(p *parameters) {
f(p)
}
// WithLogLevel sets the log level for the module.
func WithLogLevel(logLevel zerolog.Level) Parameter {
return parameterFunc(func(p *parameters) {
p.logLevel = logLevel
})
}
// WithGenesisTimeProvider sets the genesis time provider.
func WithGenesisTimeProvider(provider eth2client.GenesisTimeProvider) Parameter {
return parameterFunc(func(p *parameters) {
p.genesisTimeProvider = provider
})
}
// WithSpecProvider sets the spec provider.
func WithSpecProvider(provider eth2client.SpecProvider) Parameter {
return parameterFunc(func(p *parameters) {
p.specProvider = provider
})
}
// WithForkScheduleProvider sets the fork schedule provider.
func WithForkScheduleProvider(provider eth2client.ForkScheduleProvider) Parameter {
return parameterFunc(func(p *parameters) {
p.forkScheduleProvider = provider
})
}
// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct.
func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
parameters := parameters{
logLevel: zerolog.GlobalLevel(),
}
for _, p := range params {
if params != nil {
p.apply(&parameters)
}
}
if parameters.specProvider == nil {
return nil, errors.New("no spec provider specified")
}
if parameters.genesisTimeProvider == nil {
return nil, errors.New("no genesis time provider specified")
}
if parameters.forkScheduleProvider == nil {
return nil, errors.New("no fork schedule provider specified")
}
return &parameters, nil
}

View File

@@ -0,0 +1,215 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package standard
import (
"bytes"
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/rs/zerolog"
zerologger "github.com/rs/zerolog/log"
)
// Service provides chain time services.
type Service struct {
genesisTime time.Time
slotDuration time.Duration
slotsPerEpoch uint64
epochsPerSyncCommitteePeriod uint64
altairForkEpoch phase0.Epoch
}
// module-wide log.
var log zerolog.Logger
// New creates a new controller.
func New(ctx context.Context, params ...Parameter) (*Service, error) {
parameters, err := parseAndCheckParameters(params...)
if err != nil {
return nil, errors.Wrap(err, "problem with parameters")
}
// Set logging.
log = zerologger.With().Str("service", "chaintime").Str("impl", "standard").Logger().Level(parameters.logLevel)
genesisTime, err := parameters.genesisTimeProvider.GenesisTime(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis time")
}
log.Trace().Time("genesis_time", genesisTime).Msg("Obtained genesis time")
spec, err := parameters.specProvider.Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["SECONDS_PER_SLOT"]
if !exists {
return nil, errors.New("SECONDS_PER_SLOT not found in spec")
}
slotDuration, ok := tmp.(time.Duration)
if !ok {
return nil, errors.New("SECONDS_PER_SLOT of unexpected type")
}
tmp, exists = spec["SLOTS_PER_EPOCH"]
if !exists {
return nil, errors.New("SLOTS_PER_EPOCH not found in spec")
}
slotsPerEpoch, ok := tmp.(uint64)
if !ok {
return nil, errors.New("SLOTS_PER_EPOCH of unexpected type")
}
var epochsPerSyncCommitteePeriod uint64
if tmp, exists := spec["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"]; exists {
tmp2, ok := tmp.(uint64)
if !ok {
return nil, errors.New("EPOCHS_PER_SYNC_COMMITTEE_PERIOD of unexpected type")
}
epochsPerSyncCommitteePeriod = tmp2
}
altairForkEpoch, err := fetchAltairForkEpoch(ctx, parameters.forkScheduleProvider)
if err != nil {
// Set to far future epoch.
altairForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(altairForkEpoch)).Msg("Obtained Altair fork epoch")
s := &Service{
genesisTime: genesisTime,
slotDuration: slotDuration,
slotsPerEpoch: slotsPerEpoch,
epochsPerSyncCommitteePeriod: epochsPerSyncCommitteePeriod,
altairForkEpoch: altairForkEpoch,
}
return s, nil
}
// GenesisTime provides the time of the chain's genesis.
func (s *Service) GenesisTime() time.Time {
return s.genesisTime
}
// SlotsPerEpoch provides the number of slots in the chain's epoch.
func (s *Service) SlotsPerEpoch() uint64 {
return s.slotsPerEpoch
}
// SlotDuration provides the duration of the chain's slot.
func (s *Service) SlotDuration() time.Duration {
return s.slotDuration
}
// StartOfSlot provides the time at which a given slot starts.
func (s *Service) StartOfSlot(slot phase0.Slot) time.Time {
return s.genesisTime.Add(time.Duration(slot) * s.slotDuration)
}
// StartOfEpoch provides the time at which a given epoch starts.
func (s *Service) StartOfEpoch(epoch phase0.Epoch) time.Time {
return s.genesisTime.Add(time.Duration(uint64(epoch)*s.slotsPerEpoch) * s.slotDuration)
}
// CurrentSlot provides the current slot.
func (s *Service) CurrentSlot() phase0.Slot {
if s.genesisTime.After(time.Now()) {
return 0
}
return phase0.Slot(uint64(time.Since(s.genesisTime).Seconds()) / uint64(s.slotDuration.Seconds()))
}
// CurrentEpoch provides the current epoch.
func (s *Service) CurrentEpoch() phase0.Epoch {
return phase0.Epoch(uint64(s.CurrentSlot()) / s.slotsPerEpoch)
}
// CurrentSyncCommitteePeriod provides the current sync committee period.
func (s *Service) CurrentSyncCommitteePeriod() uint64 {
return uint64(s.CurrentEpoch()) / s.epochsPerSyncCommitteePeriod
}
// SlotToEpoch provides the epoch of a given slot.
func (s *Service) SlotToEpoch(slot phase0.Slot) phase0.Epoch {
return phase0.Epoch(uint64(slot) / s.slotsPerEpoch)
}
// SlotToSyncCommitteePeriod provides the sync committee period of the given slot.
func (s *Service) SlotToSyncCommitteePeriod(slot phase0.Slot) uint64 {
return uint64(s.SlotToEpoch(slot)) / s.epochsPerSyncCommitteePeriod
}
// FirstSlotOfEpoch provides the first slot of the given epoch.
func (s *Service) FirstSlotOfEpoch(epoch phase0.Epoch) phase0.Slot {
return phase0.Slot(uint64(epoch) * s.slotsPerEpoch)
}
// TimestampToSlot provides the slot of the given timestamp.
func (s *Service) TimestampToSlot(timestamp time.Time) phase0.Slot {
if timestamp.Before(s.genesisTime) {
return 0
}
secondsSinceGenesis := uint64(timestamp.Sub(s.genesisTime).Seconds())
return phase0.Slot(secondsSinceGenesis / uint64(s.slotDuration.Seconds()))
}
// TimestampToEpoch provides the epoch of the given timestamp.
func (s *Service) TimestampToEpoch(timestamp time.Time) phase0.Epoch {
if timestamp.Before(s.genesisTime) {
return 0
}
secondsSinceGenesis := uint64(timestamp.Sub(s.genesisTime).Seconds())
return phase0.Epoch(secondsSinceGenesis / uint64(s.slotDuration.Seconds()) / s.slotsPerEpoch)
}
// FirstEpochOfSyncPeriod provides the first epoch of the given sync period.
// Note that epochs before the sync committee period will provide the Altair hard fork epoch.
func (s *Service) FirstEpochOfSyncPeriod(period uint64) phase0.Epoch {
epoch := phase0.Epoch(period * s.epochsPerSyncCommitteePeriod)
if epoch < s.altairForkEpoch {
epoch = s.altairForkEpoch
}
return epoch
}
// AltairInitialEpoch provides the epoch at which the Altair hard fork takes place.
func (s *Service) AltairInitialEpoch() phase0.Epoch {
return s.altairForkEpoch
}
// AltairInitialSyncCommitteePeriod provides the sync committee period in which the Altair hard fork takes place.
func (s *Service) AltairInitialSyncCommitteePeriod() uint64 {
return uint64(s.altairForkEpoch) / s.epochsPerSyncCommitteePeriod
}
func fetchAltairForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
forkSchedule, err := provider.ForkSchedule(ctx)
if err != nil {
return 0, err
}
for i := range forkSchedule {
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
// This is the genesis fork; ignore it.
continue
}
return forkSchedule[i].Epoch, nil
}
return 0, errors.New("no altair fork obtained")
}

View File

@@ -0,0 +1,258 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package standard_test
import (
"context"
"testing"
"time"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/testing/mock"
)
func TestService(t *testing.T) {
genesisTime := time.Now()
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
forkSchedule := []*phase0.Fork{
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
Epoch: 0,
},
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x05, 0x06, 0x07, 0x08},
Epoch: 10,
},
}
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
tests := []struct {
name string
params []standard.Parameter
err string
}{
{
name: "GenesisTimeProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
err: "problem with parameters: no genesis time provider specified",
},
{
name: "SpecProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
err: "problem with parameters: no spec provider specified",
},
{
name: "ForkScheduleProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
},
err: "problem with parameters: no fork schedule provider specified",
},
{
name: "Good",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := standard.New(context.Background(), test.params...)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}
// createService is a helper that creates a mock chaintime service.
func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uint64, uint64, []*phase0.Fork, error) {
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
forkSchedule := []*phase0.Fork{
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
Epoch: 0,
},
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x05, 0x06, 0x07, 0x08},
Epoch: 10,
},
}
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
s, err := standard.New(context.Background(),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
)
return s, slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
}
func TestGenesisTime(t *testing.T) {
genesisTime := time.Now()
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, genesisTime, s.GenesisTime())
}
func TestStartOfSlot(t *testing.T) {
genesisTime := time.Now()
s, slotDuration, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, genesisTime, s.StartOfSlot(0))
require.Equal(t, genesisTime.Add(1000*slotDuration), s.StartOfSlot(1000))
}
func TestStartOfEpoch(t *testing.T) {
genesisTime := time.Now()
s, slotDuration, slotsPerEpoch, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, genesisTime, s.StartOfEpoch(0))
require.Equal(t, genesisTime.Add(time.Duration(1000*slotsPerEpoch)*slotDuration), s.StartOfEpoch(1000))
}
func TestCurrentSlot(t *testing.T) {
genesisTime := time.Now().Add(-60 * time.Second)
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, phase0.Slot(5), s.CurrentSlot())
}
func TestCurrentEpoch(t *testing.T) {
genesisTime := time.Now().Add(-1000 * time.Second)
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
require.Equal(t, phase0.Epoch(2), s.CurrentEpoch())
}
func TestTimestampToSlot(t *testing.T) {
genesisTime := time.Now()
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
tests := []struct {
name string
timestamp time.Time
slot phase0.Slot
}{
{
name: "PreGenesis",
timestamp: genesisTime.AddDate(0, 0, -1),
slot: 0,
},
{
name: "Genesis",
timestamp: genesisTime,
slot: 0,
},
{
name: "Slot1",
timestamp: genesisTime.Add(12 * time.Second),
slot: 1,
},
{
name: "Slot999",
timestamp: genesisTime.Add(999 * 12 * time.Second),
slot: 999,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.slot, s.TimestampToSlot(test.timestamp))
})
}
}
func TestTimestampToEpoch(t *testing.T) {
genesisTime := time.Now()
s, _, _, _, _, err := createService(genesisTime)
require.NoError(t, err)
tests := []struct {
name string
timestamp time.Time
epoch phase0.Epoch
}{
{
name: "PreGenesis",
timestamp: genesisTime.AddDate(0, 0, -1),
epoch: 0,
},
{
name: "Genesis",
timestamp: genesisTime,
epoch: 0,
},
{
name: "Epoch1",
timestamp: genesisTime.Add(32 * 12 * time.Second),
epoch: 1,
},
{
name: "Epoch1Boundary",
timestamp: genesisTime.Add(64 * 12 * time.Second).Add(-1 * time.Millisecond),
epoch: 1,
},
{
name: "Epoch999",
timestamp: genesisTime.Add(999 * 32 * 12 * time.Second),
epoch: 999,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.epoch, s.TimestampToEpoch(test.timestamp))
})
}
}

234
shamir/shamir.go Normal file
View File

@@ -0,0 +1,234 @@
package shamir
import (
"crypto/rand"
"crypto/subtle"
"fmt"
mathrand "math/rand"
"time"
)
const (
// ShareOverhead is the byte size overhead of each share
// when using Split on a secret. This is caused by appending
// a one byte tag to the share.
ShareOverhead = 1
)
// polynomial represents a polynomial of arbitrary degree
type polynomial struct {
coefficients []uint8
}
// makePolynomial constructs a random polynomial of the given
// degree but with the provided intercept value.
func makePolynomial(intercept, degree uint8) (polynomial, error) {
// Create a wrapper
p := polynomial{
coefficients: make([]byte, degree+1),
}
// Ensure the intercept is set
p.coefficients[0] = intercept
// Assign random co-efficients to the polynomial
if _, err := rand.Read(p.coefficients[1:]); err != nil {
return p, err
}
return p, nil
}
// evaluate returns the value of the polynomial for the given x
func (p *polynomial) evaluate(x uint8) uint8 {
// Special case the origin
if x == 0 {
return p.coefficients[0]
}
// Compute the polynomial value using Horner's method.
degree := len(p.coefficients) - 1
out := p.coefficients[degree]
for i := degree - 1; i >= 0; i-- {
coeff := p.coefficients[i]
out = add(mult(out, x), coeff)
}
return out
}
// interpolatePolynomial takes N sample points and returns
// the value at a given x using a lagrange interpolation.
func interpolatePolynomial(xSamples, ySamples []uint8, x uint8) uint8 {
limit := len(xSamples)
var result, basis uint8
for i := 0; i < limit; i++ {
basis = 1
for j := 0; j < limit; j++ {
if i == j {
continue
}
num := add(x, xSamples[j])
denom := add(xSamples[i], xSamples[j])
term := div(num, denom)
basis = mult(basis, term)
}
group := mult(ySamples[i], basis)
result = add(result, group)
}
return result
}
// div divides two numbers in GF(2^8)
func div(a, b uint8) uint8 {
if b == 0 {
// leaks some timing information but we don't care anyways as this
// should never happen, hence the panic
panic("divide by zero")
}
logA := logTable[a]
logB := logTable[b]
diff := ((int(logA) - int(logB)) + 255) % 255
ret := int(expTable[diff])
// Ensure we return zero if a is zero but aren't subject to timing attacks
ret = subtle.ConstantTimeSelect(subtle.ConstantTimeByteEq(a, 0), 0, ret)
return uint8(ret)
}
// mult multiplies two numbers in GF(2^8)
func mult(a, b uint8) (out uint8) {
logA := logTable[a]
logB := logTable[b]
sum := (int(logA) + int(logB)) % 255
ret := int(expTable[sum])
// Ensure we return zero if either a or b are zero but aren't subject to
// timing attacks
ret = subtle.ConstantTimeSelect(subtle.ConstantTimeByteEq(a, 0), 0, ret)
ret = subtle.ConstantTimeSelect(subtle.ConstantTimeByteEq(b, 0), 0, ret)
return uint8(ret)
}
// add combines two numbers in GF(2^8)
// This can also be used for subtraction since it is symmetric.
func add(a, b uint8) uint8 {
return a ^ b
}
// Split takes an arbitrarily long secret and generates a `parts`
// number of shares, `threshold` of which are required to reconstruct
// the secret. The parts and threshold must be at least 2, and less
// than 256. The returned shares are each one byte longer than the secret
// as they attach a tag used to reconstruct the secret.
func Split(secret []byte, parts, threshold int) ([][]byte, error) {
// Sanity check the input
if parts < threshold {
return nil, fmt.Errorf("parts cannot be less than threshold")
}
if parts > 255 {
return nil, fmt.Errorf("parts cannot exceed 255")
}
if threshold < 2 {
return nil, fmt.Errorf("threshold must be at least 2")
}
if threshold > 255 {
return nil, fmt.Errorf("threshold cannot exceed 255")
}
if len(secret) == 0 {
return nil, fmt.Errorf("cannot split an empty secret")
}
// Generate random list of x coordinates
mathrand.Seed(time.Now().UnixNano())
xCoordinates := mathrand.Perm(255)
// Allocate the output array, initialize the final byte
// of the output with the offset. The representation of each
// output is {y1, y2, .., yN, x}.
out := make([][]byte, parts)
for idx := range out {
out[idx] = make([]byte, len(secret)+1)
out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1
}
// Construct a random polynomial for each byte of the secret.
// Because we are using a field of size 256, we can only represent
// a single byte as the intercept of the polynomial, so we must
// use a new polynomial for each byte.
for idx, val := range secret {
p, err := makePolynomial(val, uint8(threshold-1))
if err != nil {
return nil, fmt.Errorf("failed to generate polynomial: %w", err)
}
// Generate a `parts` number of (x,y) pairs
// We cheat by encoding the x value once as the final index,
// so that it only needs to be stored once.
for i := 0; i < parts; i++ {
x := uint8(xCoordinates[i]) + 1
y := p.evaluate(x)
out[i][idx] = y
}
}
// Return the encoded secrets
return out, nil
}
// Combine is used to reverse a Split and reconstruct a secret
// once a `threshold` number of parts are available.
func Combine(parts [][]byte) ([]byte, error) {
// Verify enough parts provided
if len(parts) < 2 {
return nil, fmt.Errorf("less than two parts cannot be used to reconstruct the secret")
}
// Verify the parts are all the same length
firstPartLen := len(parts[0])
if firstPartLen < 2 {
return nil, fmt.Errorf("parts must be at least two bytes")
}
for i := 1; i < len(parts); i++ {
if len(parts[i]) != firstPartLen {
return nil, fmt.Errorf("all parts must be the same length")
}
}
// Create a buffer to store the reconstructed secret
secret := make([]byte, firstPartLen-1)
// Buffer to store the samples
xSamples := make([]uint8, len(parts))
ySamples := make([]uint8, len(parts))
// Set the x value for each sample and ensure no x_sample values are the same,
// otherwise div() can be unhappy
checkMap := map[byte]bool{}
for i, part := range parts {
samp := part[firstPartLen-1]
if exists := checkMap[samp]; exists {
return nil, fmt.Errorf("duplicate part detected")
}
checkMap[samp] = true
xSamples[i] = samp
}
// Reconstruct each byte
for idx := range secret {
// Set the y value for each sample
for i, part := range parts {
ySamples[i] = part[idx]
}
// Interpolate the polynomial and compute the value at 0
val := interpolatePolynomial(xSamples, ySamples, 0)
// Evaluate the 0th value to get the intercept
secret[idx] = val
}
return secret, nil
}

198
shamir/shamir_test.go Normal file
View File

@@ -0,0 +1,198 @@
package shamir
import (
"bytes"
"testing"
)
func TestSplit_invalid(t *testing.T) {
secret := []byte("test")
if _, err := Split(secret, 0, 0); err == nil {
t.Fatalf("expect error")
}
if _, err := Split(secret, 2, 3); err == nil {
t.Fatalf("expect error")
}
if _, err := Split(secret, 1000, 3); err == nil {
t.Fatalf("expect error")
}
if _, err := Split(secret, 10, 1); err == nil {
t.Fatalf("expect error")
}
if _, err := Split(nil, 3, 2); err == nil {
t.Fatalf("expect error")
}
}
func TestSplit(t *testing.T) {
secret := []byte("test")
out, err := Split(secret, 5, 3)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(out) != 5 {
t.Fatalf("bad: %v", out)
}
for _, share := range out {
if len(share) != len(secret)+1 {
t.Fatalf("bad: %v", out)
}
}
}
func TestCombine_invalid(t *testing.T) {
// Not enough parts
if _, err := Combine(nil); err == nil {
t.Fatalf("should err")
}
// Mis-match in length
parts := [][]byte{
[]byte("foo"),
[]byte("ba"),
}
if _, err := Combine(parts); err == nil {
t.Fatalf("should err")
}
// Too short
parts = [][]byte{
[]byte("f"),
[]byte("b"),
}
if _, err := Combine(parts); err == nil {
t.Fatalf("should err")
}
parts = [][]byte{
[]byte("foo"),
[]byte("foo"),
}
if _, err := Combine(parts); err == nil {
t.Fatalf("should err")
}
}
func TestCombine(t *testing.T) {
secret := []byte("test")
out, err := Split(secret, 5, 3)
if err != nil {
t.Fatalf("err: %v", err)
}
// There is 5*4*3 possible choices,
// we will just brute force try them all
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if j == i {
continue
}
for k := 0; k < 5; k++ {
if k == i || k == j {
continue
}
parts := [][]byte{out[i], out[j], out[k]}
recomb, err := Combine(parts)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recomb, secret) {
t.Errorf("parts: (i:%d, j:%d, k:%d) %v", i, j, k, parts)
t.Fatalf("bad: %v %v", recomb, secret)
}
}
}
}
}
func TestField_Add(t *testing.T) {
if out := add(16, 16); out != 0 {
t.Fatalf("Bad: %v 16", out)
}
if out := add(3, 4); out != 7 {
t.Fatalf("Bad: %v 7", out)
}
}
func TestField_Mult(t *testing.T) {
if out := mult(3, 7); out != 9 {
t.Fatalf("Bad: %v 9", out)
}
if out := mult(3, 0); out != 0 {
t.Fatalf("Bad: %v 0", out)
}
if out := mult(0, 3); out != 0 {
t.Fatalf("Bad: %v 0", out)
}
}
func TestField_Divide(t *testing.T) {
if out := div(0, 7); out != 0 {
t.Fatalf("Bad: %v 0", out)
}
if out := div(3, 3); out != 1 {
t.Fatalf("Bad: %v 1", out)
}
if out := div(6, 3); out != 2 {
t.Fatalf("Bad: %v 2", out)
}
}
func TestPolynomial_Random(t *testing.T) {
p, err := makePolynomial(42, 2)
if err != nil {
t.Fatalf("err: %v", err)
}
if p.coefficients[0] != 42 {
t.Fatalf("bad: %v", p.coefficients)
}
}
func TestPolynomial_Eval(t *testing.T) {
p, err := makePolynomial(42, 1)
if err != nil {
t.Fatalf("err: %v", err)
}
if out := p.evaluate(0); out != 42 {
t.Fatalf("bad: %v", out)
}
out := p.evaluate(1)
exp := add(42, mult(1, p.coefficients[1]))
if out != exp {
t.Fatalf("bad: %v %v %v", out, exp, p.coefficients)
}
}
func TestInterpolate_Rand(t *testing.T) {
for i := 0; i < 256; i++ {
p, err := makePolynomial(uint8(i), 2)
if err != nil {
t.Fatalf("err: %v", err)
}
xVals := []uint8{1, 2, 3}
yVals := []uint8{p.evaluate(1), p.evaluate(2), p.evaluate(3)}
out := interpolatePolynomial(xVals, yVals, 0)
if out != uint8(i) {
t.Fatalf("Bad: %v %d", out, i)
}
}
}

79
shamir/tables.go Normal file
View File

@@ -0,0 +1,79 @@
package shamir
// Tables taken from http://www.samiam.org/galois.html
// They use 0xe5 (229) as the generator
var (
// logTable provides the log(X)/log(g) at each index X
logTable = [256]uint8{
0x00, 0xff, 0xc8, 0x08, 0x91, 0x10, 0xd0, 0x36,
0x5a, 0x3e, 0xd8, 0x43, 0x99, 0x77, 0xfe, 0x18,
0x23, 0x20, 0x07, 0x70, 0xa1, 0x6c, 0x0c, 0x7f,
0x62, 0x8b, 0x40, 0x46, 0xc7, 0x4b, 0xe0, 0x0e,
0xeb, 0x16, 0xe8, 0xad, 0xcf, 0xcd, 0x39, 0x53,
0x6a, 0x27, 0x35, 0x93, 0xd4, 0x4e, 0x48, 0xc3,
0x2b, 0x79, 0x54, 0x28, 0x09, 0x78, 0x0f, 0x21,
0x90, 0x87, 0x14, 0x2a, 0xa9, 0x9c, 0xd6, 0x74,
0xb4, 0x7c, 0xde, 0xed, 0xb1, 0x86, 0x76, 0xa4,
0x98, 0xe2, 0x96, 0x8f, 0x02, 0x32, 0x1c, 0xc1,
0x33, 0xee, 0xef, 0x81, 0xfd, 0x30, 0x5c, 0x13,
0x9d, 0x29, 0x17, 0xc4, 0x11, 0x44, 0x8c, 0x80,
0xf3, 0x73, 0x42, 0x1e, 0x1d, 0xb5, 0xf0, 0x12,
0xd1, 0x5b, 0x41, 0xa2, 0xd7, 0x2c, 0xe9, 0xd5,
0x59, 0xcb, 0x50, 0xa8, 0xdc, 0xfc, 0xf2, 0x56,
0x72, 0xa6, 0x65, 0x2f, 0x9f, 0x9b, 0x3d, 0xba,
0x7d, 0xc2, 0x45, 0x82, 0xa7, 0x57, 0xb6, 0xa3,
0x7a, 0x75, 0x4f, 0xae, 0x3f, 0x37, 0x6d, 0x47,
0x61, 0xbe, 0xab, 0xd3, 0x5f, 0xb0, 0x58, 0xaf,
0xca, 0x5e, 0xfa, 0x85, 0xe4, 0x4d, 0x8a, 0x05,
0xfb, 0x60, 0xb7, 0x7b, 0xb8, 0x26, 0x4a, 0x67,
0xc6, 0x1a, 0xf8, 0x69, 0x25, 0xb3, 0xdb, 0xbd,
0x66, 0xdd, 0xf1, 0xd2, 0xdf, 0x03, 0x8d, 0x34,
0xd9, 0x92, 0x0d, 0x63, 0x55, 0xaa, 0x49, 0xec,
0xbc, 0x95, 0x3c, 0x84, 0x0b, 0xf5, 0xe6, 0xe7,
0xe5, 0xac, 0x7e, 0x6e, 0xb9, 0xf9, 0xda, 0x8e,
0x9a, 0xc9, 0x24, 0xe1, 0x0a, 0x15, 0x6b, 0x3a,
0xa0, 0x51, 0xf4, 0xea, 0xb2, 0x97, 0x9e, 0x5d,
0x22, 0x88, 0x94, 0xce, 0x19, 0x01, 0x71, 0x4c,
0xa5, 0xe3, 0xc5, 0x31, 0xbb, 0xcc, 0x1f, 0x2d,
0x3b, 0x52, 0x6f, 0xf6, 0x2e, 0x89, 0xf7, 0xc0,
0x68, 0x1b, 0x64, 0x04, 0x06, 0xbf, 0x83, 0x38,
}
// expTable provides the anti-log or exponentiation value
// for the equivalent index
expTable = [256]uint8{
0x01, 0xe5, 0x4c, 0xb5, 0xfb, 0x9f, 0xfc, 0x12,
0x03, 0x34, 0xd4, 0xc4, 0x16, 0xba, 0x1f, 0x36,
0x05, 0x5c, 0x67, 0x57, 0x3a, 0xd5, 0x21, 0x5a,
0x0f, 0xe4, 0xa9, 0xf9, 0x4e, 0x64, 0x63, 0xee,
0x11, 0x37, 0xe0, 0x10, 0xd2, 0xac, 0xa5, 0x29,
0x33, 0x59, 0x3b, 0x30, 0x6d, 0xef, 0xf4, 0x7b,
0x55, 0xeb, 0x4d, 0x50, 0xb7, 0x2a, 0x07, 0x8d,
0xff, 0x26, 0xd7, 0xf0, 0xc2, 0x7e, 0x09, 0x8c,
0x1a, 0x6a, 0x62, 0x0b, 0x5d, 0x82, 0x1b, 0x8f,
0x2e, 0xbe, 0xa6, 0x1d, 0xe7, 0x9d, 0x2d, 0x8a,
0x72, 0xd9, 0xf1, 0x27, 0x32, 0xbc, 0x77, 0x85,
0x96, 0x70, 0x08, 0x69, 0x56, 0xdf, 0x99, 0x94,
0xa1, 0x90, 0x18, 0xbb, 0xfa, 0x7a, 0xb0, 0xa7,
0xf8, 0xab, 0x28, 0xd6, 0x15, 0x8e, 0xcb, 0xf2,
0x13, 0xe6, 0x78, 0x61, 0x3f, 0x89, 0x46, 0x0d,
0x35, 0x31, 0x88, 0xa3, 0x41, 0x80, 0xca, 0x17,
0x5f, 0x53, 0x83, 0xfe, 0xc3, 0x9b, 0x45, 0x39,
0xe1, 0xf5, 0x9e, 0x19, 0x5e, 0xb6, 0xcf, 0x4b,
0x38, 0x04, 0xb9, 0x2b, 0xe2, 0xc1, 0x4a, 0xdd,
0x48, 0x0c, 0xd0, 0x7d, 0x3d, 0x58, 0xde, 0x7c,
0xd8, 0x14, 0x6b, 0x87, 0x47, 0xe8, 0x79, 0x84,
0x73, 0x3c, 0xbd, 0x92, 0xc9, 0x23, 0x8b, 0x97,
0x95, 0x44, 0xdc, 0xad, 0x40, 0x65, 0x86, 0xa2,
0xa4, 0xcc, 0x7f, 0xec, 0xc0, 0xaf, 0x91, 0xfd,
0xf7, 0x4f, 0x81, 0x2f, 0x5b, 0xea, 0xa8, 0x1c,
0x02, 0xd1, 0x98, 0x71, 0xed, 0x25, 0xe3, 0x24,
0x06, 0x68, 0xb3, 0x93, 0x2c, 0x6f, 0x3e, 0x6c,
0x0a, 0xb8, 0xce, 0xae, 0x74, 0xb1, 0x42, 0xb4,
0x1e, 0xd3, 0x49, 0xe9, 0x9c, 0xc8, 0xc6, 0xc7,
0x22, 0x6e, 0xdb, 0x20, 0xbf, 0x43, 0x51, 0x52,
0x66, 0xb2, 0x76, 0x60, 0xda, 0xc5, 0xf3, 0xf6,
0xaa, 0xcd, 0x9a, 0xa0, 0x75, 0x54, 0x0e, 0x01,
}
)

13
shamir/tables_test.go Normal file
View File

@@ -0,0 +1,13 @@
package shamir
import "testing"
func TestTables(t *testing.T) {
for i := 1; i < 256; i++ {
logV := logTable[i]
expV := expTable[logV]
if expV != uint8(i) {
t.Fatalf("bad: %d log: %d exp: %d", i, logV, expV)
}
}
}

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

@@ -0,0 +1,151 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mock
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
)
// GenesisTimeProvider is a mock for eth2client.GenesisTimeProvider.
type GenesisTimeProvider struct {
genesisTime time.Time
}
// NewGenesisTimeProvider returns a mock genesis time provider with the provided value.
func NewGenesisTimeProvider(genesisTime time.Time) eth2client.GenesisTimeProvider {
return &GenesisTimeProvider{
genesisTime: genesisTime,
}
}
// GenesisTime is a mock.
func (m *GenesisTimeProvider) GenesisTime(ctx context.Context) (time.Time, error) {
return m.genesisTime, nil
}
// SpecProvider is a mock for eth2client.SpecProvider.
type SpecProvider struct {
spec map[string]interface{}
}
// NewSpecProvider returns a mock spec provider with the provided values.
func NewSpecProvider(slotDuration time.Duration,
slotsPerEpoch uint64,
epochsPerSyncCommitteePeriod uint64,
) eth2client.SpecProvider {
return &SpecProvider{
spec: map[string]interface{}{
"SECONDS_PER_SLOT": slotDuration,
"SLOTS_PER_EPOCH": slotsPerEpoch,
"EPOCHS_PER_SYNC_COMMITTEE_PERIOD": epochsPerSyncCommitteePeriod,
},
}
}
// Spec is a mock.
func (m *SpecProvider) Spec(ctx context.Context) (map[string]interface{}, error) {
return m.spec, nil
}
// ForkScheduleProvider is a mock for eth2client.ForkScheduleProvider.
type ForkScheduleProvider struct {
schedule []*phase0.Fork
}
// NewForkScheduleProvider returns a mock spec provider with the provided values.
func NewForkScheduleProvider(schedule []*phase0.Fork) eth2client.ForkScheduleProvider {
return &ForkScheduleProvider{
schedule: schedule,
}
}
// ForkSchedule is a mock.
func (m *ForkScheduleProvider) ForkSchedule(ctx context.Context) ([]*phase0.Fork, error) {
return m.schedule, nil
}
// SlotsPerEpochProvider is a mock for eth2client.SlotsPerEpochProvider.
type SlotsPerEpochProvider struct {
slotsPerEpoch uint64
}
// NewSlotsPerEpochProvider returns a mock slots per epoch provider with the provided value.
func NewSlotsPerEpochProvider(slotsPerEpoch uint64) eth2client.SlotsPerEpochProvider {
return &SlotsPerEpochProvider{
slotsPerEpoch: slotsPerEpoch,
}
}
// SlotsPerEpoch is a mock.
func (m *SlotsPerEpochProvider) SlotsPerEpoch(ctx context.Context) (uint64, error) {
return m.slotsPerEpoch, nil
}
// AttestationsSubmitter is a mock for eth2client.AttestationsSubmitter.
type AttestationsSubmitter struct{}
// NewAttestationSubmitter returns a mock attestations submitter with the provided value.
func NewAttestationSubmitter() eth2client.AttestationsSubmitter {
return &AttestationsSubmitter{}
}
// SubmitAttestations is a mock.
func (m *AttestationsSubmitter) SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error {
return nil
}
// BeaconBlockSubmitter is a mock for eth2client.BeaconBlockSubmitter.
type BeaconBlockSubmitter struct{}
// NewBeaconBlockSubmitter returns a mock beacon block submitter with the provided value.
func NewBeaconBlockSubmitter() eth2client.BeaconBlockSubmitter {
return &BeaconBlockSubmitter{}
}
// SubmitBeaconBlock is a mock.
func (m *BeaconBlockSubmitter) SubmitBeaconBlock(ctx context.Context, bloc *spec.VersionedSignedBeaconBlock) error {
return nil
}
// AggregateAttestationsSubmitter is a mock for eth2client.AggregateAttestationsSubmitter.
type AggregateAttestationsSubmitter struct{}
// NewAggregateAttestationsSubmitter returns a mock aggregate attestation submitter with the provided value.
func NewAggregateAttestationsSubmitter() eth2client.AggregateAttestationsSubmitter {
return &AggregateAttestationsSubmitter{}
}
// SubmitAggregateAttestations is a mock.
func (m *AggregateAttestationsSubmitter) SubmitAggregateAttestations(ctx context.Context, aggregates []*phase0.SignedAggregateAndProof) error {
return nil
}
// BeaconCommitteeSubscriptionsSubmitter is a mock for eth2client.BeaconCommitteeSubscriptionsSubmitter.
type BeaconCommitteeSubscriptionsSubmitter struct{}
// NewBeaconCommitteeSubscriptionsSubmitter returns a mock beacon committee subscription submitter with the provided value.
func NewBeaconCommitteeSubscriptionsSubmitter() eth2client.BeaconCommitteeSubscriptionsSubmitter {
return &BeaconCommitteeSubscriptionsSubmitter{}
}
// SubmitBeaconCommitteeSubscriptions is a mock.
func (m *BeaconCommitteeSubscriptionsSubmitter) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*api.BeaconCommitteeSubscription) error {
return nil
}

View File

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