Compare commits

...

8 Commits

Author SHA1 Message Date
Jim McDonald
aa79f83f35 Update changelog 2021-08-03 14:05:15 +01:00
Jim McDonald
8de7e75c77 Merge pull request #36 from wealdtech/sss-export
Shared wallet export/import
2021-08-03 14:03:18 +01:00
Jim McDonald
4a1b419c0e Update documentation. 2021-08-03 13:49:28 +01:00
Jim McDonald
b6a08d5073 Tidy-ups. 2021-08-03 13:49:28 +01:00
Jim McDonald
65d2ab5d53 Tidy-ups. 2021-08-03 13:49:27 +01:00
Jim McDonald
34b03f9d53 Handle timezone in chain time. 2021-08-03 13:49:27 +01:00
Jim McDonald
dca513b8c9 Handle timezone in chain time. 2021-07-30 08:31:43 +01:00
Jim McDonald
446941be92 Add SSS import/export. 2021-07-02 22:48:30 +01:00
25 changed files with 2152 additions and 483 deletions

View File

@@ -1,8 +1,10 @@
1.10.0
- add "wallet sharedexport" and "wallet sharedimport"
1.9.1
- Avoid crash when required interfaces for chain status command are not supported
- Avoid crash with latest version of herumi/go-bls
1.9.0
- allow use of Ethereum 1 address as withdrawal credentials

View File

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

View File

@@ -60,6 +60,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
curEpoch := spec.Epoch(uint64(curSlot) / slotsPerEpoch)
fmt.Printf("Current epoch: %d\n", curEpoch)
outputIf(verbose, fmt.Sprintf("Current slot: %d", curSlot))
fmt.Printf("Justified epoch: %d\n", finality.Justified.Epoch)
if verbose {
distance := curEpoch - finality.Justified.Epoch

View File

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

View File

@@ -96,6 +96,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 {

View File

@@ -24,7 +24,7 @@ import (
// ReleaseVersion is the release version of the codebase.
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.9.1)"
var ReleaseVersion = "local build (latest release 1.10.0)"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

View File

@@ -0,0 +1,83 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
// System.
timeout time.Duration
verbose bool
debug bool
wallet e2wtypes.Wallet
file string
participants uint32
threshold uint32
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetString("remote") != "" {
return nil, errors.New("wallet export not available for remote wallets")
}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
// Quiet is not allowed.
if viper.GetBool("quiet") {
return nil, errors.New("quiet not allowed")
}
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Wallet.
wallet, err := util.WalletFromInput(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to access wallet")
}
data.wallet = wallet
// File.
data.file = viper.GetString("file")
if data.file == "" {
return nil, errors.New("file is required")
}
// Participants
data.participants = viper.GetUint32("participants")
if data.participants == 0 {
return nil, errors.New("participants is required")
}
data.threshold = viper.GetUint32("threshold")
if data.threshold == 0 {
return nil, errors.New("threshold is required")
}
if data.threshold > data.participants {
return nil, errors.New("threshold cannot be more than participants")
}
return data, nil
}

View File

@@ -0,0 +1,153 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
wallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"wallet": "Test wallet",
},
err: "timeout is required",
},
{
name: "Quiet",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"quiet": "true",
},
err: "quiet not allowed",
},
{
name: "WalletMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to access wallet: cannot determine wallet",
},
{
name: "WalletUnknown",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "unknown",
},
err: "failed to access wallet: wallet not found",
},
{
name: "Remote",
vars: map[string]interface{}{
"timeout": "5s",
"remote": "remoteaddress",
},
err: "wallet export not available for remote wallets",
},
{
name: "FileMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
},
err: "file is required",
},
{
name: "ParticipantsMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
},
err: "participants is required",
},
{
name: "ThresholdMissing",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
},
err: "threshold is required",
},
{
name: "ThresholdTooHigh",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
"threshold": "6",
},
err: "threshold cannot be more than participants",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"wallet": "Test wallet",
"file": "test.dat",
"participants": "5",
"threshold": "3",
},
res: &dataIn{
timeout: 5 * time.Second,
wallet: wallet,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
require.Equal(t, test.vars["wallet"], res.wallet.Name())
}
})
}
}

View File

@@ -0,0 +1,42 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
)
type dataOut struct {
shares [][]byte
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
builder := strings.Builder{}
for i := range data.shares {
builder.WriteString(fmt.Sprintf("%x", data.shares[i]))
if i != len(data.shares)-1 {
builder.WriteString("\n")
}
}
return builder.String(), nil
}

View File

@@ -0,0 +1,58 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
expected string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Good",
dataOut: &dataOut{
shares: [][]byte{
{0x01, 0x02},
{0x02, 0x03},
{0x03, 0x04},
},
},
expected: "0102\n0203\n0304",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, res)
}
})
}
}

View File

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

View File

@@ -0,0 +1,93 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedexport
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
filesystem "github.com/wealdtech/go-eth2-wallet-store-filesystem"
)
func TestProcess(t *testing.T) {
require.NoError(t, e2types.InitBLS())
base, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(base)
store := filesystem.New(filesystem.WithLocation(base))
require.NoError(t, e2wallet.UseStore(store))
wallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "WalletMissing",
dataIn: &dataIn{
timeout: 5 * time.Second,
},
err: "wallet is required",
},
{
name: "FileInvalid",
dataIn: &dataIn{
timeout: 5 * time.Second,
wallet: wallet,
file: "/bad/bad/bad/backup.dat",
participants: 5,
threshold: 3,
},
err: "failed to write export file: open /bad/bad/bad/backup.dat: no such file or directory",
},
{
name: "Good",
dataIn: &dataIn{
timeout: 5 * time.Second,
wallet: wallet,
file: "test.dat",
participants: 5,
threshold: 3,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
os.Remove(test.dataIn.file)
require.Len(t, res.shares, int(test.dataIn.participants))
}
})
}
}

View File

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

View File

@@ -0,0 +1,67 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedimport
import (
"context"
"io/ioutil"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
file []byte
shares []string
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetString("remote") != "" {
return nil, errors.New("wallet import not available for remote wallets")
}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Data.
if viper.GetString("file") == "" {
return nil, errors.New("file is required")
}
data.file, err = ioutil.ReadFile(viper.GetString("file"))
if err != nil {
return nil, errors.Wrap(err, "failed to read wallet import file")
}
// Shares.
data.shares = viper.GetStringSlice("shares")
if len(data.shares) == 0 {
return nil, errors.New("failed to obtain shares")
}
return data, nil
}

View File

@@ -0,0 +1,132 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedimport
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
dir := os.TempDir()
datFile := filepath.Join(dir, "backup.dat")
err := ioutil.WriteFile(datFile, []byte("dummy"), 0600)
require.NoError(t, err)
// defer os.RemoveAll(dir)
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
wallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
data, err := wallet.(e2wtypes.WalletExporter).Export(context.Background(), []byte("ce%NohGhah4ye5ra"))
require.NoError(t, err)
require.NoError(t, e2wallet.UseStore(scratch.New()))
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"data": fmt.Sprintf("%#x", data),
},
err: "timeout is required",
},
{
name: "FileMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "file is required",
},
{
name: "Remote",
vars: map[string]interface{}{
"timeout": "5s",
"file": "test.dat",
"remote": "remoteaddress",
},
err: "wallet import not available for remote wallets",
},
{
name: "FilMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "file is required",
},
{
name: "FileBad",
vars: map[string]interface{}{
"timeout": "5s",
"file": "bad.dat",
},
err: "failed to read wallet import file: open bad.dat: no such file or directory",
},
{
name: "SharesMissing",
vars: map[string]interface{}{
"timeout": "5s",
"file": datFile,
},
err: "failed to obtain shares",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"file": datFile,
"shares": "01 02 03",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.NotNil(t, res)
}
})
}
}

View File

@@ -0,0 +1,24 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedimport
import (
"context"
)
type dataOut struct{}
func output(ctx context.Context, data *dataOut) (string, error) {
return "Wallet imported", nil
}

View File

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

View File

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

View File

@@ -0,0 +1,138 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package walletsharedimport
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func TestProcess(t *testing.T) {
require.NoError(t, e2types.InitBLS())
export := []byte(`{"version":1,"participants":5,"threshold":3,"data":"0x0106951ed83407552b501d97a31ee7bf6655450723dfcb0b8448690ce85838b7ba563cf536edf58bbb04f22cab8baee062c602175768d6419965545da206062b40cefe9887d2e89250b96cf99de1fcb2cc462b3eeb6b60128df66d5540edb93cfbdc805d353bf0223ca3f5c1c223f19928af742a54f2c2a60491f6fdae4bc5abc621babb625b6ec3610c3ce7943826b79b0cf3b1a84dbbe6b09c7edc87628269775576d2a1047689f31035ac1847e5b6e2511a86e58948478bbf885b814059a3f1b7c72c312f4e9fd6d962847e2c38f3bdc8df5deacfb2b7fddf851e4324a2433ebbcd0598bee8b493c27a1951bab894f1963dcc262ca1b47bda15f620d2d8d5006e5f798071db64f40a980ac77c759ab3f116d66a160d7516c92afd7d38be2681cfdbd750e6133c4d50e5555d9a9b69d223f389da737e352338f8c0e4b96e413362afc3561975a397715ef2fcbbf270b1d8a5ef41fafa6fb7241c4664627b420a2b40d06a5706ebcb005a39a7ed066fa13a206e396a572bab94829de52550d912ddbe2ee85b8775bb5886eb783426e3c79c2129bbe87b6be777cb79d70294f2541fcccc9bad8f603774c843ee5c7cabcb2bd5b6d160bd7e871e5cf90d4aca4e1e521089fc6d131ba3f9c0a6c0bd942837d598a78c8fd7a1c45409fba388ba1d16433acd93122c964d930a7dc5c5018128f5243a752d3cb56e4d7e607508490818b0237777543c90e2048a4fedf20b453adc2fac7aa4824d6805ed258de66c0d51f9d37cc616f1f84e0873dbb9edff03de8ba5839b898b55eb549ee34f4e587f6dd5a2bc892b0f11caebc33b314239d9567cca1477318c708cedba6c301e9c8edf58f46a7b4a07883c2dff30fe54eaf243718ccc464f276bb4045e72081248238eb9855d8b5f993c2b1e6049c95e5622685857016c2e72a89b322a24399f4476f4a3f7c0e219e06f8e46939d29874bebd5fb24407ca260ef1db362a79403c46776e5b205f956771d14aec6b4c54340a655acf5396ef9487e7acd8a154ff4392a79d35377ece9c09fbb114a935ff0f18b4469b9f94436d7b1790920e2cfcad4b7e187d6ecb47dc23336366baa8a70b3536a7df2489bf12d92aade034a185e5cc0a349229431e37b7f587d1dedd6a41cbe3452b7186fa25f1f22d7d17ae5750b42640b973f4503cb129beaa07f7fbb08bf09292336c96a1666da36c481904df944f74a5bc003a5b9e41a47b8240a996991e23d60f83d96590a67a621c780840fd6a256627d1202550e2b7b8c10d7e43dad01a88ce9757effacb82494948c94dd6eadf4452e2d396fd135eee347672ac33a4d224d9b79ff9438c46073aae6a1104606ca5a44d52f2b2ac93b7fe60b4db61a738d4f5db87ab92d987bb176d374a6306b7d5f4c974ac17153fed99aa8826a579c6806b74b25f21d7b232098d8845dbfb2645849dc4daefb9d9cc1079062af37dc9b976a9915803ce96abe3786ab5bc3d7a62c7a698a5f75a6a65c4aaa6972800ce8dbbd43b682d3f8ecd2a3074d14082ed60d7f969de5d59c66e0f4f3812bbdc536a92947e1d027c63d8595737d58cb62887237eb4ef9c704345677e1faf9b9ef0c524a28e8703e2814e897fabdaae1b2cd71360d19ff6c35275ecccfc834682b9094b66f42877942fec0a1b620eea4d6c8ce7a128c2bf07d77b448330abbe4c2405f769fd790f67a6adaa678677dd2238e77e60a0c324c2ad73a9e499b8cd4282d8ac6337e291563b5df2507d4ec8fe2ca568ac5d6af10448233a2900d6833a8cf6cdc06142b95410b3b21976ce95bfca87512805a70f7d193dceb25d62d12b280863b3165f11ee3f411f39132bd9e618e00225fbf0f9e39f2af15c1ec6cddc3dfb81089a69a0a8db9befcb3f987cb5f5f288b3259369ecc904145cf6bc2977a49c2977058886601155cd974951b37e2dc828cf2edbf3a60c1a8ba5ebdc27ed83a95ce8af9fdc4e0b6213fdcc02f8576d05f9ffe387ada68a4f0e3e538eb6be8433bb90be816e1f9a34e3d6fd1c60c46380ec1307e18011befd9f6399ece3e82001a32c5991055c5363b544bf7ec66d01e6eb26da41c382fe7954817fdc7a2d067569758897277e88b20f4cd93d4f61a61a609757b08d3579677262db5aef082d0e79fab11f52c9d86c0768890df96957dbbb4d425d5d271c4b18394e2b0c4f7c89b9a"}`)
shares := []string{
"d04a162f3f648647acbfc5af0475041c3f64c3d72752ddc52ab53786802ed7dfea3929488dbefb3af582e713fe967a6ff24c86757186abe7d93afdcd81cdff4f8a",
"f06533d9efae8b015a5b9c73d2b3652b5e0c80fa9a948fdcfbda3d4bd54ae31573c8649ffb0a8900dfe1cecb740b0c3a477938f3e01244cac39a068612beb72bbe",
"682b8e6256ce6a4fde515060a326214f7a3789b79c11e2cb53e5b185d522d196ca0b76dea7a03d739ec87605ede429a9f214dfb06703dbb143d8d5b56413d7a0a7",
"53ccad137def6fcbaac0ccfff0fdbb02ab3fa4ce075b221f15a80203a318f29f09cfc7a40b29c910675791f847e3e72dc6f80e74b80f517512c1fd6be14ff5b2ff",
"ed2166659f7b5412a169ec83627386bc6ff1a31e67735d405b2bf7cb122ad7ced35c87e42c8e8f7ba90b5899a94be506687a9c5b353af2a216018d9f1bf61745a5",
}
dir := os.TempDir()
datFile := filepath.Join(dir, "backup.dat")
err := ioutil.WriteFile(datFile, export, 0600)
require.NoError(t, err)
defer os.RemoveAll(dir)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "FileMissing",
dataIn: &dataIn{
timeout: 5 * time.Second,
},
err: "import file is required",
},
{
name: "FileBad",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: []byte("\001\002"),
shares: []string{
shares[0],
shares[1],
shares[2],
},
},
err: "failed to unmarshal export: invalid character '\\x01' looking for beginning of value",
},
{
name: "SharesTooLow",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
shares[0],
shares[1],
},
},
err: "import requires 3 shares, 2 were provided",
},
{
name: "SharesTooHigh",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
shares[0],
shares[1],
shares[2],
shares[3],
},
},
err: "import requires 3 shares, 4 were provided",
},
{
name: "ShareBad",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
"xxx",
shares[1],
shares[2],
},
},
err: "invalid share: encoding/hex: invalid byte: U+0078 'x'",
},
{
name: "Good",
dataIn: &dataIn{
timeout: 5 * time.Second,
file: export,
shares: []string{
shares[0],
shares[1],
shares[2],
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

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

60
cmd/walletsharedexport.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
walletsharedexport "github.com/wealdtech/ethdo/cmd/wallet/sharedexport"
)
var walletSharedExportCmd = &cobra.Command{
Use: "sharedexport",
Short: "Export a wallet using Shamir secret sharing",
Long: `Export a wallet for backup of transfer using Shamir secret sharing. For example:
ethdo wallet sharedexport --wallet=primary --participants=5 --threshold=3 --file=backup.dat`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := walletsharedexport.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
walletCmd.AddCommand(walletSharedExportCmd)
walletFlags(walletSharedExportCmd)
walletSharedExportCmd.Flags().Uint32("participants", 0, "Number of participants in sharing scheme")
walletSharedExportCmd.Flags().Uint32("threshold", 0, "Number of participants required to recover the export")
walletSharedExportCmd.Flags().String("file", "", "Name of the file that stores the export")
}
func walletSharedExportBindings() {
if err := viper.BindPFlag("participants", walletSharedExportCmd.Flags().Lookup("participants")); err != nil {
panic(err)
}
if err := viper.BindPFlag("threshold", walletSharedExportCmd.Flags().Lookup("threshold")); err != nil {
panic(err)
}
if err := viper.BindPFlag("file", walletSharedExportCmd.Flags().Lookup("file")); err != nil {
panic(err)
}
}

58
cmd/walletsharedimport.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
walletsharedimport "github.com/wealdtech/ethdo/cmd/wallet/sharedimport"
)
var walletSharedImportCmd = &cobra.Command{
Use: "sharedimport",
Short: "Import a wallet using Shamir secret sharing",
Long: `Import a wallet for backup of transfer using Shamir secret sharing. For example:
ethdo wallet sharedimport --file=backup.dat --shares="1234 2345 3456"
In quiet mode this will return 0 if the wallet is imported successfully, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := walletsharedimport.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
walletCmd.AddCommand(walletSharedImportCmd)
walletFlags(walletSharedImportCmd)
walletSharedImportCmd.Flags().String("file", "", "Name of the file that stores the export")
walletSharedImportCmd.Flags().String("shares", "", "Shares required to decrypt the export, separated with spaces")
}
func walletSharedImportBindings() {
if err := viper.BindPFlag("file", walletSharedImportCmd.Flags().Lookup("file")); err != nil {
panic(err)
}
if err := viper.BindPFlag("shares", walletSharedImportCmd.Flags().Lookup("shares")); err != nil {
panic(err)
}
}

View File

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

68
go.mod
View File

@@ -3,62 +3,56 @@ module github.com/wealdtech/ethdo
go 1.13
require (
github.com/DataDog/zstd v1.4.8 // indirect
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/attestantio/dirk v1.0.2
github.com/attestantio/go-eth2-client v0.6.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/go-eth2-client v0.6.30
github.com/aws/aws-sdk-go v1.38.68 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/ferranbt/fastssz v0.0.0-20210526181520-7df50c8568f8
github.com/gofrs/uuid v4.0.0+incompatible
github.com/gogo/protobuf v1.3.2
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/go-cmp v0.5.6 // indirect
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/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.1-vault-3 // indirect
github.com/hashicorp/vault v1.7.3
github.com/herumi/bls-eth-go-binary v0.0.0-20210520070601-31246bfa8ac4
github.com/mattn/go-isatty v0.0.13 // indirect
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/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
github.com/prometheus/common v0.29.0 // indirect
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-20210607200045-4da71aaf6c2d
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.22.0
github.com/spf13/cobra v1.1.3
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-types/v2 v2.5.4
github.com/wealdtech/go-eth2-util v1.6.4
github.com/wealdtech/go-eth2-wallet v1.14.4
github.com/wealdtech/go-eth2-wallet-dirk v1.1.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-encryptor-keystorev4 v1.1.5
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.5.4
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.3
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.14
github.com/wealdtech/go-eth2-wallet-store-s3 v1.9.4
github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.2
github.com/wealdtech/go-eth2-wallet-types/v2 v2.8.2
github.com/wealdtech/go-eth2-wallet-types/v2 v2.8.3
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/net v0.0.0-20210610132358-84b48f89b13b // indirect
golang.org/x/text v0.3.6
google.golang.org/genproto v0.0.0-20210611144927-798beca9d670 // indirect
google.golang.org/grpc v1.38.0
)

1311
go.sum

File diff suppressed because it is too large Load Diff