Compare commits

...

27 Commits

Author SHA1 Message Date
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
Jim McDonald
aa0cda306b Update dependencies. 2021-08-03 22:58:35 +01:00
Jim McDonald
aa79f83f35 Update changelog 2021-08-03 14:05:15 +01:00
Jim McDonald
8de7e75c77 Merge pull request #36 from wealdtech/sss-export
Shared wallet export/import
2021-08-03 14:03:18 +01:00
Jim McDonald
4a1b419c0e Update documentation. 2021-08-03 13:49:28 +01:00
Jim McDonald
b6a08d5073 Tidy-ups. 2021-08-03 13:49:28 +01:00
Jim McDonald
65d2ab5d53 Tidy-ups. 2021-08-03 13:49:27 +01:00
Jim McDonald
34b03f9d53 Handle timezone in chain time. 2021-08-03 13:49:27 +01:00
Jim McDonald
dca513b8c9 Handle timezone in chain time. 2021-07-30 08:31:43 +01:00
Jim McDonald
446941be92 Add SSS import/export. 2021-07-02 22:48:30 +01:00
Jim McDonald
b76cdb01d1 Update version. 2021-05-13 12:42:14 +01:00
Jim McDonald
ce5b250ef0 Report on missing interfaces.
This update handles the situation where an ETH2 client does not provide
all required interfaces for the 'chain status' command, returning an
error rather than simply panicing.

Fixes #35.
2021-05-13 12:39:14 +01:00
Jim McDonald
2c4ccf62af Avoid crash with latest version of herumi/go-bls. 2021-05-13 12:37:46 +01:00
52 changed files with 3779 additions and 839 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 get github.com/wealdtech/xgo
- 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: |
@@ -72,11 +72,11 @@ jobs:
sha256sum ethdo >ethdo-${RELEASE_VERSION}-linux-amd64.sha256
tar zcf ethdo-${RELEASE_VERSION}-linux-amd64.tar.gz ethdo
- 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
# sha256sum ethdo >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
# tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
- 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,20 @@
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"
1.9.1
- Avoid crash when required interfaces for chain status command are not supported
- Avoid crash with latest version of herumi/go-bls
1.9.0
- allow use of Ethereum 1 address as withdrawal credentials

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,42 @@ 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,
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("Slot: %d\n", slot))
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", bodyRoot))
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("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 +93,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 +113,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 +122,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 +137,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 +184,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 +203,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 +223,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 +271,98 @@ 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)
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,
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.
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,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti,
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
@@ -273,10 +411,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 +425,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 +434,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 +451,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

@@ -67,7 +67,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
results.slot = spec.Slot(epoch * slotsPerEpoch)
case data.timestamp != "":
timestamp, err := time.Parse("2006-01-02T15:04:05", data.timestamp)
timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp)
if err != nil {
return nil, errors.Wrap(err, "failed to parse timestamp")
}

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

View File

@@ -40,13 +40,19 @@ 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")
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
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")
finality, err := eth2Client.(eth2client.FinalityProvider).Finality(ctx, "head")
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")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
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")
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
@@ -54,6 +60,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
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

View File

@@ -44,7 +44,7 @@ func init() {
chainFlags(chainTimeCmd)
chainTimeCmd.Flags().String("slot", "", "The slot for which to obtain information")
chainTimeCmd.Flags().String("epoch", "", "The epoch for which to obtain information")
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS)")
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS+ZZZZ)")
}
func chainTimeBindings() {

View File

@@ -70,7 +70,9 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis
}
exitRoot, err := exit.HashTreeRoot()
errCheck(err, "Failed to obtain exit hash tree root")
sig, err := e2types.BLSSignatureFromBytes(data.Exit.Signature[:])
signatureBytes := make([]byte, 96)
copy(signatureBytes, data.Exit.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(signatureBytes)
errCheck(err, "Invalid signature")
verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig)
errCheck(err, "Failed to verify voluntary exit")

View File

@@ -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,6 +57,9 @@ 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")
@@ -82,6 +86,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
nodeEventsBindings()
case "slot/time":
slotTimeBindings()
case "synccommittee/members":
synccommitteeMembersBindings()
case "validator/depositdata":
validatorDepositdataBindings()
case "validator/duties":
@@ -96,6 +102,10 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
walletCreateBindings()
case "wallet/import":
walletImportBindings()
case "wallet/sharedexport":
walletSharedExportBindings()
case "wallet/sharedimport":
walletSharedImportBindings()
}
if quiet && verbose {
@@ -105,11 +115,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.

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

@@ -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.9.0)"
var ReleaseVersion = "local build (latest release 1.12.0)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

View File

@@ -0,0 +1,83 @@
// 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 walletsharedexport
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
// System.
timeout time.Duration
verbose bool
debug bool
wallet e2wtypes.Wallet
file string
participants uint32
threshold uint32
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetString("remote") != "" {
return nil, errors.New("wallet export not available for remote wallets")
}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
// Quiet is not allowed.
if viper.GetBool("quiet") {
return nil, errors.New("quiet not allowed")
}
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Wallet.
wallet, err := util.WalletFromInput(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to access wallet")
}
data.wallet = wallet
// File.
data.file = viper.GetString("file")
if data.file == "" {
return nil, errors.New("file is required")
}
// Participants
data.participants = viper.GetUint32("participants")
if data.participants == 0 {
return nil, errors.New("participants is required")
}
data.threshold = viper.GetUint32("threshold")
if data.threshold == 0 {
return nil, errors.New("threshold is required")
}
if data.threshold > data.participants {
return nil, errors.New("threshold cannot be more than participants")
}
return data, nil
}

View File

@@ -0,0 +1,153 @@
// 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 walletsharedexport
import (
"context"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
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"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
wallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"wallet": "Test wallet",
},
err: "timeout is required",
},
{
name: "Quiet",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"quiet": "true",
},
err: "quiet not allowed",
},
{
name: "WalletMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to access wallet: cannot determine wallet",
},
{
name: "WalletUnknown",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "unknown",
},
err: "failed to access wallet: wallet not found",
},
{
name: "Remote",
vars: map[string]interface{}{
"timeout": "5s",
"remote": "remoteaddress",
},
err: "wallet export not available for remote wallets",
},
{
name: "FileMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
},
err: "file is required",
},
{
name: "ParticipantsMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
},
err: "participants is required",
},
{
name: "ThresholdMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
},
err: "threshold is required",
},
{
name: "ThresholdTooHigh",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
"threshold": "6",
},
err: "threshold cannot be more than participants",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
"threshold": "3",
},
res: &dataIn{
timeout: 5 * time.Second,
wallet: wallet,
},
},
}
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)
require.Equal(t, test.vars["wallet"], res.wallet.Name())
}
})
}
}

View File

@@ -0,0 +1,42 @@
// 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 walletsharedexport
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
)
type dataOut struct {
shares [][]byte
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
builder := strings.Builder{}
for i := range data.shares {
builder.WriteString(fmt.Sprintf("%x", data.shares[i]))
if i != len(data.shares)-1 {
builder.WriteString("\n")
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,58 @@
// 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 walletsharedexport
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
expected string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Good",
dataOut: &dataOut{
shares: [][]byte{
{0x01, 0x02},
{0x02, 0x03},
{0x03, 0x04},
},
},
expected: "0102\n0203\n0304",
},
}
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.expected, res)
}
})
}
}

View File

@@ -0,0 +1,86 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"os"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/shamir"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type sharedExport struct {
Version uint32 `json:"version"`
Participants uint32 `json:"participants"`
Threshold uint32 `json:"threshold"`
Data string `json:"data"`
}
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.wallet == nil {
return nil, errors.New("wallet is required")
}
passphrase := make([]byte, 64)
n, err := rand.Read(passphrase)
if err != nil {
return nil, errors.Wrap(err, "failed to generate passphrase")
}
if n != 64 {
return nil, errors.New("failed to obtain passphrase")
}
exporter, isExporter := data.wallet.(e2wtypes.WalletExporter)
if !isExporter {
return nil, errors.New("wallet does not provide export")
}
export, err := exporter.Export(ctx, passphrase)
if err != nil {
return nil, errors.Wrap(err, "failed to export wallet")
}
shares, err := shamir.Split(passphrase, int(data.participants), int(data.threshold))
if err != nil {
return nil, errors.Wrap(err, "failed to create shamir shares")
}
sharedExport := &sharedExport{
Version: 1,
Participants: data.participants,
Threshold: data.threshold,
Data: fmt.Sprintf("%#x", export),
}
sharedFile, err := json.Marshal(sharedExport)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal shamir export")
}
if err := os.WriteFile(data.file, sharedFile, 0600); err != nil {
return nil, errors.Wrap(err, "failed to write export file")
}
results := &dataOut{
shares: shares,
}
return results, nil
}

View File

@@ -0,0 +1,93 @@
// 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 walletsharedexport
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
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"
filesystem "github.com/wealdtech/go-eth2-wallet-store-filesystem"
)
func TestProcess(t *testing.T) {
require.NoError(t, e2types.InitBLS())
base, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(base)
store := filesystem.New(filesystem.WithLocation(base))
require.NoError(t, e2wallet.UseStore(store))
wallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "WalletMissing",
dataIn: &dataIn{
timeout: 5 * time.Second,
},
err: "wallet is required",
},
{
name: "FileInvalid",
dataIn: &dataIn{
timeout: 5 * time.Second,
wallet: wallet,
file: "/bad/bad/bad/backup.dat",
participants: 5,
threshold: 3,
},
err: "failed to write export file: open /bad/bad/bad/backup.dat: no such file or directory",
},
{
name: "Good",
dataIn: &dataIn{
timeout: 5 * time.Second,
wallet: wallet,
file: "test.dat",
participants: 5,
threshold: 3,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
os.Remove(test.dataIn.file)
require.Len(t, res.shares, int(test.dataIn.participants))
}
})
}
}

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 walletsharedexport
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()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

@@ -0,0 +1,67 @@
// Copyright © 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 walletsharedimport
import (
"context"
"io/ioutil"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
file []byte
shares []string
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetString("remote") != "" {
return nil, errors.New("wallet import not available for remote wallets")
}
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")
// Data.
if viper.GetString("file") == "" {
return nil, errors.New("file is required")
}
data.file, err = ioutil.ReadFile(viper.GetString("file"))
if err != nil {
return nil, errors.Wrap(err, "failed to read wallet import file")
}
// Shares.
data.shares = viper.GetStringSlice("shares")
if len(data.shares) == 0 {
return nil, errors.New("failed to obtain shares")
}
return data, nil
}

View File

@@ -0,0 +1,132 @@
// 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 walletsharedimport
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
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) {
require.NoError(t, e2types.InitBLS())
dir := os.TempDir()
datFile := filepath.Join(dir, "backup.dat")
err := ioutil.WriteFile(datFile, []byte("dummy"), 0600)
require.NoError(t, err)
// defer os.RemoveAll(dir)
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
wallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
data, err := wallet.(e2wtypes.WalletExporter).Export(context.Background(), []byte("ce%NohGhah4ye5ra"))
require.NoError(t, err)
require.NoError(t, e2wallet.UseStore(scratch.New()))
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"data": fmt.Sprintf("%#x", data),
},
err: "timeout is required",
},
{
name: "FileMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "file is required",
},
{
name: "Remote",
vars: map[string]interface{}{
"timeout": "5s",
"file": "test.dat",
"remote": "remoteaddress",
},
err: "wallet import not available for remote wallets",
},
{
name: "FilMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "file is required",
},
{
name: "FileBad",
vars: map[string]interface{}{
"timeout": "5s",
"file": "bad.dat",
},
err: "failed to read wallet import file: open bad.dat: no such file or directory",
},
{
name: "SharesMissing",
vars: map[string]interface{}{
"timeout": "5s",
"file": datFile,
},
err: "failed to obtain shares",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"file": datFile,
"shares": "01 02 03",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
}
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.NotNil(t, res)
}
})
}
}

View File

@@ -0,0 +1,24 @@
// 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 walletsharedimport
import (
"context"
)
type dataOut struct{}
func output(ctx context.Context, data *dataOut) (string, error) {
return "Wallet imported", nil
}

View File

@@ -0,0 +1,47 @@
// 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 walletsharedimport
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Good",
res: "Wallet imported",
},
}
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,73 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedimport
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/shamir"
e2wallet "github.com/wealdtech/go-eth2-wallet"
)
type sharedExport struct {
Version uint32 `json:"version"`
Participants uint32 `json:"participants"`
Threshold uint32 `json:"threshold"`
Data string `json:"data"`
}
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if len(data.file) == 0 {
return nil, errors.New("import file is required")
}
sharedExport := &sharedExport{}
err := json.Unmarshal(data.file, sharedExport)
if err != nil {
return nil, errors.Wrap(err, "failed to unmarshal export")
}
if len(data.shares) != int(sharedExport.Threshold) {
return nil, fmt.Errorf("import requires %d shares, %d were provided", sharedExport.Threshold, len(data.shares))
}
shares := make([][]byte, len(data.shares))
for i := range data.shares {
shares[i], err = hex.DecodeString(data.shares[i])
if err != nil {
return nil, errors.Wrap(err, "invalid share")
}
}
passphrase, err := shamir.Combine(shares)
if err != nil {
return nil, errors.Wrap(err, "failed to recreate passphrase from shares")
}
wallet, err := hex.DecodeString(strings.TrimPrefix(sharedExport.Data, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain data from export")
}
if _, err := e2wallet.ImportWallet(wallet, passphrase); err != nil {
return nil, errors.Wrap(err, "failed to import wallet")
}
return nil, nil
}

View File

@@ -0,0 +1,138 @@
// 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 walletsharedimport
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func TestProcess(t *testing.T) {
require.NoError(t, e2types.InitBLS())
export := []byte(`{"version":1,"participants":5,"threshold":3,"data":"0x0106951ed83407552b501d97a31ee7bf6655450723dfcb0b8448690ce85838b7ba563cf536edf58bbb04f22cab8baee062c602175768d6419965545da206062b40cefe9887d2e89250b96cf99de1fcb2cc462b3eeb6b60128df66d5540edb93cfbdc805d353bf0223ca3f5c1c223f19928af742a54f2c2a60491f6fdae4bc5abc621babb625b6ec3610c3ce7943826b79b0cf3b1a84dbbe6b09c7edc87628269775576d2a1047689f31035ac1847e5b6e2511a86e58948478bbf885b814059a3f1b7c72c312f4e9fd6d962847e2c38f3bdc8df5deacfb2b7fddf851e4324a2433ebbcd0598bee8b493c27a1951bab894f1963dcc262ca1b47bda15f620d2d8d5006e5f798071db64f40a980ac77c759ab3f116d66a160d7516c92afd7d38be2681cfdbd750e6133c4d50e5555d9a9b69d223f389da737e352338f8c0e4b96e413362afc3561975a397715ef2fcbbf270b1d8a5ef41fafa6fb7241c4664627b420a2b40d06a5706ebcb005a39a7ed066fa13a206e396a572bab94829de52550d912ddbe2ee85b8775bb5886eb783426e3c79c2129bbe87b6be777cb79d70294f2541fcccc9bad8f603774c843ee5c7cabcb2bd5b6d160bd7e871e5cf90d4aca4e1e521089fc6d131ba3f9c0a6c0bd942837d598a78c8fd7a1c45409fba388ba1d16433acd93122c964d930a7dc5c5018128f5243a752d3cb56e4d7e607508490818b0237777543c90e2048a4fedf20b453adc2fac7aa4824d6805ed258de66c0d51f9d37cc616f1f84e0873dbb9edff03de8ba5839b898b55eb549ee34f4e587f6dd5a2bc892b0f11caebc33b314239d9567cca1477318c708cedba6c301e9c8edf58f46a7b4a07883c2dff30fe54eaf243718ccc464f276bb4045e72081248238eb9855d8b5f993c2b1e6049c95e5622685857016c2e72a89b322a24399f4476f4a3f7c0e219e06f8e46939d29874bebd5fb24407ca260ef1db362a79403c46776e5b205f956771d14aec6b4c54340a655acf5396ef9487e7acd8a154ff4392a79d35377ece9c09fbb114a935ff0f18b4469b9f94436d7b1790920e2cfcad4b7e187d6ecb47dc23336366baa8a70b3536a7df2489bf12d92aade034a185e5cc0a349229431e37b7f587d1dedd6a41cbe3452b7186fa25f1f22d7d17ae5750b42640b973f4503cb129beaa07f7fbb08bf09292336c96a1666da36c481904df944f74a5bc003a5b9e41a47b8240a996991e23d60f83d96590a67a621c780840fd6a256627d1202550e2b7b8c10d7e43dad01a88ce9757effacb82494948c94dd6eadf4452e2d396fd135eee347672ac33a4d224d9b79ff9438c46073aae6a1104606ca5a44d52f2b2ac93b7fe60b4db61a738d4f5db87ab92d987bb176d374a6306b7d5f4c974ac17153fed99aa8826a579c6806b74b25f21d7b232098d8845dbfb2645849dc4daefb9d9cc1079062af37dc9b976a9915803ce96abe3786ab5bc3d7a62c7a698a5f75a6a65c4aaa6972800ce8dbbd43b682d3f8ecd2a3074d14082ed60d7f969de5d59c66e0f4f3812bbdc536a92947e1d027c63d8595737d58cb62887237eb4ef9c704345677e1faf9b9ef0c524a28e8703e2814e897fabdaae1b2cd71360d19ff6c35275ecccfc834682b9094b66f42877942fec0a1b620eea4d6c8ce7a128c2bf07d77b448330abbe4c2405f769fd790f67a6adaa678677dd2238e77e60a0c324c2ad73a9e499b8cd4282d8ac6337e291563b5df2507d4ec8fe2ca568ac5d6af10448233a2900d6833a8cf6cdc06142b95410b3b21976ce95bfca87512805a70f7d193dceb25d62d12b280863b3165f11ee3f411f39132bd9e618e00225fbf0f9e39f2af15c1ec6cddc3dfb81089a69a0a8db9befcb3f987cb5f5f288b3259369ecc904145cf6bc2977a49c2977058886601155cd974951b37e2dc828cf2edbf3a60c1a8ba5ebdc27ed83a95ce8af9fdc4e0b6213fdcc02f8576d05f9ffe387ada68a4f0e3e538eb6be8433bb90be816e1f9a34e3d6fd1c60c46380ec1307e18011befd9f6399ece3e82001a32c5991055c5363b544bf7ec66d01e6eb26da41c382fe7954817fdc7a2d067569758897277e88b20f4cd93d4f61a61a609757b08d3579677262db5aef082d0e79fab11f52c9d86c0768890df96957dbbb4d425d5d271c4b18394e2b0c4f7c89b9a"}`)
shares := []string{
"d04a162f3f648647acbfc5af0475041c3f64c3d72752ddc52ab53786802ed7dfea3929488dbefb3af582e713fe967a6ff24c86757186abe7d93afdcd81cdff4f8a",
"f06533d9efae8b015a5b9c73d2b3652b5e0c80fa9a948fdcfbda3d4bd54ae31573c8649ffb0a8900dfe1cecb740b0c3a477938f3e01244cac39a068612beb72bbe",
"682b8e6256ce6a4fde515060a326214f7a3789b79c11e2cb53e5b185d522d196ca0b76dea7a03d739ec87605ede429a9f214dfb06703dbb143d8d5b56413d7a0a7",
"53ccad137def6fcbaac0ccfff0fdbb02ab3fa4ce075b221f15a80203a318f29f09cfc7a40b29c910675791f847e3e72dc6f80e74b80f517512c1fd6be14ff5b2ff",
"ed2166659f7b5412a169ec83627386bc6ff1a31e67735d405b2bf7cb122ad7ced35c87e42c8e8f7ba90b5899a94be506687a9c5b353af2a216018d9f1bf61745a5",
}
dir := os.TempDir()
datFile := filepath.Join(dir, "backup.dat")
err := ioutil.WriteFile(datFile, export, 0600)
require.NoError(t, err)
defer os.RemoveAll(dir)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "FileMissing",
dataIn: &dataIn{
timeout: 5 * time.Second,
},
err: "import file is required",
},
{
name: "FileBad",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: []byte("\001\002"),
shares: []string{
shares[0],
shares[1],
shares[2],
},
},
err: "failed to unmarshal export: invalid character '\\x01' looking for beginning of value",
},
{
name: "SharesTooLow",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
shares[0],
shares[1],
},
},
err: "import requires 3 shares, 2 were provided",
},
{
name: "SharesTooHigh",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
shares[0],
shares[1],
shares[2],
shares[3],
},
},
err: "import requires 3 shares, 4 were provided",
},
{
name: "ShareBad",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
"xxx",
shares[1],
shares[2],
},
},
err: "invalid share: encoding/hex: invalid byte: U+0078 'x'",
},
{
name: "Good",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
shares[0],
shares[1],
shares[2],
},
},
},
}
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 walletsharedimport
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()
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

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

60
cmd/walletsharedexport.go Normal file
View File

@@ -0,0 +1,60 @@
// 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"
walletsharedexport "github.com/wealdtech/ethdo/cmd/wallet/sharedexport"
)
var walletSharedExportCmd = &cobra.Command{
Use: "sharedexport",
Short: "Export a wallet using Shamir secret sharing",
Long: `Export a wallet for backup of transfer using Shamir secret sharing. For example:
ethdo wallet sharedexport --wallet=primary --participants=5 --threshold=3 --file=backup.dat`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := walletsharedexport.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
walletCmd.AddCommand(walletSharedExportCmd)
walletFlags(walletSharedExportCmd)
walletSharedExportCmd.Flags().Uint32("participants", 0, "Number of participants in sharing scheme")
walletSharedExportCmd.Flags().Uint32("threshold", 0, "Number of participants required to recover the export")
walletSharedExportCmd.Flags().String("file", "", "Name of the file that stores the export")
}
func walletSharedExportBindings() {
if err := viper.BindPFlag("participants", walletSharedExportCmd.Flags().Lookup("participants")); err != nil {
panic(err)
}
if err := viper.BindPFlag("threshold", walletSharedExportCmd.Flags().Lookup("threshold")); err != nil {
panic(err)
}
if err := viper.BindPFlag("file", walletSharedExportCmd.Flags().Lookup("file")); err != nil {
panic(err)
}
}

58
cmd/walletsharedimport.go Normal file
View File

@@ -0,0 +1,58 @@
// 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"
walletsharedimport "github.com/wealdtech/ethdo/cmd/wallet/sharedimport"
)
var walletSharedImportCmd = &cobra.Command{
Use: "sharedimport",
Short: "Import a wallet using Shamir secret sharing",
Long: `Import a wallet for backup of transfer using Shamir secret sharing. For example:
ethdo wallet sharedimport --file=backup.dat --shares="1234 2345 3456"
In quiet mode this will return 0 if the wallet is imported successfully, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := walletsharedimport.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
walletCmd.AddCommand(walletSharedImportCmd)
walletFlags(walletSharedImportCmd)
walletSharedImportCmd.Flags().String("file", "", "Name of the file that stores the export")
walletSharedImportCmd.Flags().String("shares", "", "Shares required to decrypt the export, separated with spaces")
}
func walletSharedImportBindings() {
if err := viper.BindPFlag("file", walletSharedImportCmd.Flags().Lookup("file")); err != nil {
panic(err)
}
if err := viper.BindPFlag("shares", walletSharedImportCmd.Flags().Lookup("shares")); err != nil {
panic(err)
}
}

View File

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

View File

@@ -103,6 +103,33 @@ Personal wallet
**N.B.** encrypted wallets will not show up in this list unless the correct passphrase for the store is supplied.
#### `sharedexport`
`ethdo wallet sharedexport` exports the wallet and all of its accounts with shared keys. Options for exporting a wallet include:
- `wallet`: the name of the wallet to export (defaults to "primary")
- `participants`: the total number of participants that each hold a share
- `threshold`: the number of participants necessary to provide their share to restore the wallet
- `file`: the name of the file that stores the backup
```sh
$ ethdo wallet sharedexport --wallet="Personal wallet" --participants=3 --threshold=2 --file=backup.dat
298a4efce34c7f46114b7c4ea4be3d3bef925dccb153dd00227b53c3be7dad668b326f2659b2375e708bb824b33f0e7364e0f21dd18e5f5f2d7d04de7c122a9189
10eabc645fd1633e2e874ffc486fcbe313b33c34fbcb30511a74517296e01f332c6e3c40757c39b4dc47f3a417d321c4c81c2115e53fca57797c975913b8bf5063
559c77b56d36cbd84f23669a376d389f8c6644933a1a4112512e4d063d0779489c6b6312e6c46def0ee33ce5b5aca0941a833f65e64b5d270c7224323f4e28b238
```
Each line of the output is a share and should be provided to one of the participants, along with the backup file.
#### `sharedimport`
`ethdo wallet sharedimport` imports a wallet and all of its accounts exported by `ethdo wallet sharedexport`. Options for importing a wallet include:
- `file`: the name of the file that stores the backup
- `shares`: a number of shares, defined by _threshold_ during the export, separated by spaces
```sh
$ ethdo wallet sharedimport --file=backup.dat --shares="298a…9189 10ea…5063"
```
### `account` commands
Account commands focus on information about local accounts, generally those used by Geth and Parity but also those from hardware devices.

80
go.mod
View File

@@ -1,64 +1,46 @@
module github.com/wealdtech/ethdo
go 1.13
go 1.16
require (
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/attestantio/dirk v1.0.2
github.com/attestantio/go-eth2-client v0.6.21
github.com/aws/aws-sdk-go v1.37.1 // indirect
github.com/ferranbt/fastssz v0.0.0-20210120143747-11b9eff30ea9
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/goccy/go-yaml v1.8.6 // indirect
github.com/attestantio/dirk v1.0.3
github.com/attestantio/go-eth2-client v0.7.2
github.com/ferranbt/fastssz v0.0.0-20210905181407-59cf6761a7d5
github.com/gofrs/uuid v4.0.0+incompatible
github.com/gogo/protobuf v1.3.2
github.com/google/uuid v1.2.0
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/herumi/bls-eth-go-binary v0.0.0-20210130185500-57372fb27371
github.com/jackc/puddle v1.1.3 // indirect
github.com/magiconair/properties v1.8.4 // indirect
github.com/minio/highwayhash v1.0.1 // indirect
github.com/google/uuid v1.3.0
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/nbutton23/zxcvbn-go v0.0.0-20201221231540-e56b841a3c88
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
github.com/protolambda/zssz v0.1.5 // indirect
github.com/prysmaticlabs/ethereumapis v0.0.0-20210201130911-92b2a467c108
github.com/prysmaticlabs/go-bitfield v0.0.0-20210129193852-0db57134419f
github.com/prysmaticlabs/ethereumapis v0.0.0-20210201130911-92b2a467c108 // indirect
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388
github.com/rs/zerolog v1.20.0
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.1.1
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/rs/zerolog v1.23.0
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1
github.com/tj/assert v0.0.3 // indirect
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/ugorji/go v1.1.4 // indirect
github.com/wealdtech/eth2-signer-api v1.6.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.2
github.com/wealdtech/go-eth2-util v1.6.3
github.com/wealdtech/go-eth2-wallet v1.14.4
github.com/wealdtech/go-eth2-wallet-dirk v1.1.5
github.com/wealdtech/go-eth2-wallet-distributed v1.1.3
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.3
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.2
github.com/wealdtech/go-ecodec v1.1.2
github.com/wealdtech/go-eth2-types/v2 v2.5.6
github.com/wealdtech/go-eth2-util v1.6.5
github.com/wealdtech/go-eth2-wallet v1.14.6
github.com/wealdtech/go-eth2-wallet-dirk v1.1.7
github.com/wealdtech/go-eth2-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-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/text v0.3.5
google.golang.org/genproto v0.0.0-20210201184850-646a494a81ea // indirect
google.golang.org/grpc v1.35.0
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/text v0.3.7
google.golang.org/grpc v1.40.0
)

999
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
}