mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94eba96a6e | ||
|
|
f052d8e307 | ||
|
|
df45686828 | ||
|
|
84d228877a | ||
|
|
b2b26742b0 | ||
|
|
9dc630c809 | ||
|
|
452430db56 | ||
|
|
b0d676a734 | ||
|
|
ff73470085 | ||
|
|
a41349999f | ||
|
|
004f4bc41a | ||
|
|
64c8e1a051 | ||
|
|
d95d48f6b2 | ||
|
|
3e702f0c51 | ||
|
|
2e36fcc3ce | ||
|
|
aa0cda306b | ||
|
|
aa79f83f35 | ||
|
|
8de7e75c77 | ||
|
|
4a1b419c0e | ||
|
|
b6a08d5073 | ||
|
|
65d2ab5d53 | ||
|
|
34b03f9d53 | ||
|
|
dca513b8c9 | ||
|
|
446941be92 | ||
|
|
b76cdb01d1 | ||
|
|
ce5b250ef0 | ||
|
|
2c4ccf62af |
56
.github/workflows/release.yml
vendored
56
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
16
cmd/root.go
16
cmd/root.go
@@ -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
32
cmd/synccommittee.go
Normal 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) {
|
||||
}
|
||||
78
cmd/synccommittee/members/input.go
Normal file
78
cmd/synccommittee/members/input.go
Normal 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
|
||||
}
|
||||
88
cmd/synccommittee/members/input_internal_test.go
Normal file
88
cmd/synccommittee/members/input_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
61
cmd/synccommittee/members/output.go
Normal file
61
cmd/synccommittee/members/output.go
Normal 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
|
||||
}
|
||||
68
cmd/synccommittee/members/output_internal_test.go
Normal file
68
cmd/synccommittee/members/output_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/synccommittee/members/process.go
Normal file
50
cmd/synccommittee/members/process.go
Normal 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
|
||||
}
|
||||
74
cmd/synccommittee/members/process_internal_test.go
Normal file
74
cmd/synccommittee/members/process_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/synccommittee/members/run.go
Normal file
50
cmd/synccommittee/members/run.go
Normal 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
|
||||
}
|
||||
57
cmd/synccommitteemembers.go
Normal file
57
cmd/synccommitteemembers.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
83
cmd/wallet/sharedexport/input.go
Normal file
83
cmd/wallet/sharedexport/input.go
Normal 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
|
||||
}
|
||||
153
cmd/wallet/sharedexport/input_internal_test.go
Normal file
153
cmd/wallet/sharedexport/input_internal_test.go
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
42
cmd/wallet/sharedexport/output.go
Normal file
42
cmd/wallet/sharedexport/output.go
Normal 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
|
||||
}
|
||||
58
cmd/wallet/sharedexport/output_internal_test.go
Normal file
58
cmd/wallet/sharedexport/output_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
86
cmd/wallet/sharedexport/process.go
Normal file
86
cmd/wallet/sharedexport/process.go
Normal 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
|
||||
}
|
||||
93
cmd/wallet/sharedexport/process_internal_test.go
Normal file
93
cmd/wallet/sharedexport/process_internal_test.go
Normal 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/wallet/sharedexport/run.go
Normal file
50
cmd/wallet/sharedexport/run.go
Normal 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
|
||||
}
|
||||
67
cmd/wallet/sharedimport/input.go
Normal file
67
cmd/wallet/sharedimport/input.go
Normal 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
|
||||
}
|
||||
132
cmd/wallet/sharedimport/input_internal_test.go
Normal file
132
cmd/wallet/sharedimport/input_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
24
cmd/wallet/sharedimport/output.go
Normal file
24
cmd/wallet/sharedimport/output.go
Normal 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
|
||||
}
|
||||
47
cmd/wallet/sharedimport/output_internal_test.go
Normal file
47
cmd/wallet/sharedimport/output_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
73
cmd/wallet/sharedimport/process.go
Normal file
73
cmd/wallet/sharedimport/process.go
Normal 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
|
||||
}
|
||||
138
cmd/wallet/sharedimport/process_internal_test.go
Normal file
138
cmd/wallet/sharedimport/process_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
cmd/wallet/sharedimport/run.go
Normal file
50
cmd/wallet/sharedimport/run.go
Normal 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
|
||||
}
|
||||
@@ -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
60
cmd/walletsharedexport.go
Normal 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
58
cmd/walletsharedimport.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
80
go.mod
@@ -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
|
||||
)
|
||||
|
||||
57
services/chaintime/service.go
Normal file
57
services/chaintime/service.go
Normal 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
|
||||
}
|
||||
90
services/chaintime/standard/parameters.go
Normal file
90
services/chaintime/standard/parameters.go
Normal 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(¶meters)
|
||||
}
|
||||
}
|
||||
|
||||
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 ¶meters, nil
|
||||
}
|
||||
215
services/chaintime/standard/service.go
Normal file
215
services/chaintime/standard/service.go
Normal 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")
|
||||
}
|
||||
258
services/chaintime/standard/service_test.go
Normal file
258
services/chaintime/standard/service_test.go
Normal 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
234
shamir/shamir.go
Normal 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
198
shamir/shamir_test.go
Normal 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
79
shamir/tables.go
Normal 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
13
shamir/tables_test.go
Normal 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
151
testing/mock/eth2client.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user