Compare commits

...

23 Commits

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

Fixes #35.
2021-05-13 12:39:14 +01:00
Jim McDonald
2c4ccf62af Avoid crash with latest version of herumi/go-bls. 2021-05-13 12:37:46 +01:00
Jim McDonald
c7ad5194e6 Bump version number. 2021-03-14 22:03:22 +00:00
Jim McDonald
ddb866131b Merge pull request #32 from wealdtech/eth1-withdrawal-credentials
Allow use of Ethereum 1 withdrawal credentials
2021-03-14 21:47:21 +00:00
Jim McDonald
49fb03aa3a Allow use of Ethereum 1 withdrawal credentials.
Release 1.0.1 of the Ethereum 2 specification allows withdrawal
credentials to be Ethereum 1 addresses.  This enables the use of such
addresses when generating and verifying deposit data.
2021-03-12 12:53:42 +00:00
Jim McDonald
1ed3a51117 ETH1 withdrawal credentials. 2021-02-26 15:19:37 +00:00
Jim McDonald
4d5660ccbb Fix crash in attester/duties and inclusion.
A recent change for a return value going from an array to a map caused a
bad indexing in to the returned data.  This ensures that the value is
read directly from the map rather than using a hard-coded offset.
2021-02-13 22:25:26 +00:00
Jim McDonald
7596d271ad Linting. 2021-02-10 10:24:49 +00:00
Jim McDonald
943f9350f3 Add 'chain time' and 'validator keycheck' commands. 2021-02-10 10:13:24 +00:00
Jim McDonald
07863846e6 Use double quotes for Windows compatability. 2021-02-04 21:54:43 +00:00
Jim McDonald
cc59ab618d Tidy up tests. 2021-02-02 20:57:01 +00:00
Jim McDonald
9794949e8a Tidy up separation of input and process. 2021-02-02 20:50:45 +00:00
67 changed files with 3794 additions and 758 deletions

23
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: golangci-lint
on: [ push, pull_request ]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.29
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
args: --timeout=10m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

View File

@@ -1,4 +1,24 @@
1.7.4:
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.8.1
- fix issue where 'attester duties' and 'attester inclusion' could crash
1.8.0
- add "chain time"
- add "validator keycheck"
1.7.5:
- add "slot time"
- add "attester duties"
- add "node events"

View File

@@ -25,6 +25,9 @@ import (
func blsPrivateKey(input string) *e2types.BLSPrivateKey {
data, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
key, err := e2types.BLSPrivateKeyFromBytes(data)
if err != nil {
panic(err)
@@ -58,7 +61,7 @@ func TestOutput(t *testing.T) {
{
name: "PrivatKey",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
},
needs: []string{"Public key", "Private key"},
@@ -75,7 +78,7 @@ func TestOutput(t *testing.T) {
name: "All",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
showPrivateKey: true,
showWithdrawalCredentials: true,
},
needs: []string{"Public key", "Private key", "Withdrawal credentials"},

View File

@@ -27,6 +27,3 @@ var attestationCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(attestationCmd)
}
func attestationFlags(cmd *cobra.Command) {
}

View File

@@ -15,18 +15,13 @@ package attesterduties
import (
"context"
"encoding/hex"
"fmt"
"strings"
"time"
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/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
@@ -39,10 +34,10 @@ type dataIn struct {
// Chain information.
slotsPerEpoch uint64
// Operation.
validator *api.Validator
account string
pubKey string
eth2Client eth2client.Service
epoch spec.Epoch
account e2wtypes.Account
}
func input(ctx context.Context) (*dataIn, error) {
@@ -57,14 +52,15 @@ func input(ctx context.Context) (*dataIn, error) {
data.debug = viper.GetBool("debug")
data.json = viper.GetBool("json")
// Account.
var err error
data.account, err = attesterDutiesAccount()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
// Account or pubkey.
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account or pubkey is required")
}
data.account = viper.GetString("account")
data.pubKey = viper.GetString("pubkey")
// 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")
@@ -87,45 +83,5 @@ func input(ctx context.Context) (*dataIn, error) {
}
data.epoch = spec.Epoch(epoch)
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
data.validator = validators[0]
return data, nil
}
// attesterDutiesAccount obtains the account for the attester duties command.
func attesterDutiesAccount() (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, account, err = util.WalletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKey := viper.GetString("pubkey")
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey))
}
}
return account, nil
}

View File

@@ -65,7 +65,7 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to obtain account: invalid public key : public key must be 48 bytes",
err: "account or pubkey is required",
},
{
name: "ConnectionMissing",

View File

@@ -15,11 +15,16 @@ package attesterduties
import (
"context"
"encoding/hex"
"fmt"
"strings"
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/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -27,13 +32,52 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
var account e2wtypes.Account
var err error
if data.account != "" {
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
}
}
// Fetch validator
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, data.validator, data.epoch, data.slotsPerEpoch)
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}

View File

@@ -18,7 +18,6 @@ import (
"os"
"testing"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
@@ -48,10 +47,8 @@ func TestProcess(t *testing.T) {
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
validator: &api.Validator{
Index: 0,
},
epoch: 100,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
},
},
}

View File

@@ -15,18 +15,13 @@ package attesterinclusion
import (
"context"
"encoding/hex"
"fmt"
"strings"
"time"
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/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
@@ -38,10 +33,10 @@ type dataIn struct {
// Chain information.
slotsPerEpoch uint64
// Operation.
validator *api.Validator
eth2Client eth2client.Service
epoch spec.Epoch
account e2wtypes.Account
account string
pubKey string
}
func input(ctx context.Context) (*dataIn, error) {
@@ -55,14 +50,15 @@ func input(ctx context.Context) (*dataIn, error) {
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Account.
var err error
data.account, err = attesterInclusionAccount()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
// Account or pubkey.
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account or pubkey is required")
}
data.account = viper.GetString("account")
data.pubKey = viper.GetString("pubkey")
// 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")
@@ -89,49 +85,5 @@ func input(ctx context.Context) (*dataIn, error) {
}
data.epoch = spec.Epoch(epoch)
// Validator.
stateID := "head"
if viper.GetInt64("epoch") != -1 {
stateID = fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch)
}
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, stateID, pubKeys)
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
for _, validator := range validators {
data.validator = validator
}
return data, nil
}
// attesterInclusionAccount obtains the account for the attester inclusion command.
func attesterInclusionAccount() (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, account, err = util.WalletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKey := viper.GetString("pubkey")
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey))
}
}
return account, nil
}

View File

@@ -65,7 +65,7 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to obtain account: invalid public key : public key must be 48 bytes",
err: "account or pubkey is required",
},
{
name: "ConnectionMissing",

View File

@@ -15,12 +15,16 @@ package attesterinclusion
import (
"context"
"encoding/hex"
"fmt"
"strings"
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/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -28,13 +32,52 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.New("no data")
}
var account e2wtypes.Account
var err error
if data.account != "" {
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
_, account, err = util.WalletAndAccountFromPath(ctx, data.account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey))
}
}
// Fetch validator
pubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
}
copy(pubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys)
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, data.validator, data.epoch, data.slotsPerEpoch)
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}

View File

@@ -18,7 +18,6 @@ import (
"os"
"testing"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
@@ -48,10 +47,8 @@ func TestProcess(t *testing.T) {
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
validator: &api.Validator{
Index: 0,
},
epoch: 100,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
},
},
}

81
cmd/chain/time/input.go Normal file
View File

@@ -0,0 +1,81 @@
// 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 (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
json bool
// Input
connection string
allowInsecureConnections bool
timestamp string
slot string
epoch string
}
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")
data.json = viper.GetBool("json")
haveInput := false
if viper.GetString("timestamp") != "" {
data.timestamp = viper.GetString("timestamp")
haveInput = true
}
if viper.GetString("slot") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.slot = viper.GetString("slot")
haveInput = true
}
if viper.GetString("epoch") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.epoch = viper.GetString("epoch")
haveInput = true
}
if !haveInput {
return nil, errors.New("one of timestamp, slot or epoch required")
}
if viper.GetString("connection") == "" {
return nil, errors.New("connection is required")
}
data.connection = viper.GetString("connection")
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return data, nil
}

View File

@@ -0,0 +1,97 @@
// 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 (
"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",
"slot": "1",
},
err: "connection is required",
},
{
name: "IDMissing",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "one of timestamp, slot or epoch required",
},
}
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)
}
})
}
}

66
cmd/chain/time/output.go Normal file
View File

@@ -0,0 +1,66 @@
// 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 (
"context"
"fmt"
"strings"
"time"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
epoch spec.Epoch
epochStart time.Time
epochEnd time.Time
slot spec.Slot
slotStart time.Time
slotEnd time.Time
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.quiet {
return "", nil
}
builder := strings.Builder{}
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d", data.epoch))
builder.WriteString("\n Epoch start ")
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Epoch end ")
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\nSlot ")
builder.WriteString(fmt.Sprintf("%d", data.slot))
builder.WriteString("\n Slot start ")
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,85 @@
// 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 (
// "context"
// "testing"
//
// api "github.com/attestantio/go-eth2-client/api/v1"
// "github.com/stretchr/testify/require"
// "github.com/wealdtech/ethdo/testutil"
// )
//
// 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 duties found",
// },
// {
// name: "Present",
// dataOut: &dataOut{
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: "Validator attesting in slot 1 committee 3",
// },
// {
// name: "JSON",
// dataOut: &dataOut{
// json: true,
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
// },
// }
//
// 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)
// }
// })
// }
// }

89
cmd/chain/time/process.go Normal file
View File

@@ -0,0 +1,89 @@
// 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 (
"context"
"strconv"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis data")
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
// Calculate the slot given the input.
switch {
case data.slot != "":
slot, err := strconv.ParseUint(data.slot, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse slot")
}
results.slot = spec.Slot(slot)
case data.epoch != "":
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse epoch")
}
results.slot = spec.Slot(epoch * slotsPerEpoch)
case 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")
}
secs := timestamp.Sub(genesis.GenesisTime)
if secs < 0 {
return nil, errors.New("timestamp prior to genesis")
}
results.slot = spec.Slot(secs / slotDuration)
}
// Fill in the info given the slot.
results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration)
results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration)
results.epoch = spec.Epoch(uint64(results.slot) / slotsPerEpoch)
results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration)
results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration)
return results, nil
}

View File

@@ -0,0 +1,103 @@
// 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 (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
dataIn *dataIn
expected *dataOut
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Slot",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
slot: "1",
},
expected: &dataOut{
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
},
},
{
name: "Epoch",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
epoch: "2",
},
expected: &dataOut{
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
},
},
{
name: "Timestamp",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
timestamp: "2021-01-01T00:00:00",
},
expected: &dataOut{
epoch: 6862,
epochStart: time.Unix(1609459031, 0),
epochEnd: time.Unix(1609459415, 0),
slot: 219598,
slotStart: time.Unix(1609459199, 0),
slotEnd: time.Unix(1609459211, 0),
},
},
}
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)
require.Equal(t, test.expected, res)
}
})
}
}

50
cmd/chain/time/run.go Normal file
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 chaintime
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Run runs the wallet create data command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
}
if viper.GetBool("quiet") {
return "", nil
}
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}
return results, nil
}

View File

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

60
cmd/chaintime.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"
chaintime "github.com/wealdtech/ethdo/cmd/chain/time"
)
var chainTimeCmd = &cobra.Command{
Use: "time",
Short: "Obtain info about the chain at a given time",
Long: `Obtain info about the chain at a given time. For example:
ethdo chain time --slot=12345`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := chaintime.Run(cmd)
if err != nil {
return err
}
if res != "" {
fmt.Print(res)
}
return nil
},
}
func init() {
chainCmd.AddCommand(chainTimeCmd)
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+ZZZZ)")
}
func chainTimeBindings() {
if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019-2021 Weald Technology Limited.
// 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
@@ -32,6 +32,7 @@ import (
var depositVerifyData string
var depositVerifyWithdrawalPubKey string
var depositVerifyWithdrawalAddress string
var depositVerifyValidatorPubKey string
var depositVerifyDepositAmount string
var depositVerifyForkVersion string
@@ -81,7 +82,14 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis
withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
errCheck(err, "Value supplied with --withdrawalpubkey is not a valid public key")
withdrawalCredentials = eth2util.SHA256(withdrawalPubKey.Marshal())
withdrawalCredentials[0] = 0 // BLS_WITHDRAWAL_PREFIX
withdrawalCredentials[0] = 0x00 // BLS_WITHDRAWAL_PREFIX
} else if depositVerifyWithdrawalAddress != "" {
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(depositVerifyWithdrawalAddress, "0x"))
errCheck(err, "Invalid withdrawal address")
assert(len(withdrawalAddressBytes) == 20, "address should be 20 bytes")
withdrawalCredentials = make([]byte, 32)
withdrawalCredentials[0] = 0x01 // ETH1_ADDRESS_WITHDRAWAL_PREFIX
copy(withdrawalCredentials[12:], withdrawalAddressBytes)
}
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
@@ -181,10 +189,10 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, validatorPubKeys map[[48]byte]bool, amount uint64) (bool, error) {
if withdrawalCredentials == nil {
outputIf(!quiet, "Withdrawal public key not supplied; withdrawal credentials NOT checked")
outputIf(!quiet, "Withdrawal public key or address not supplied; withdrawal credentials NOT checked")
} else {
if !bytes.Equal(deposit.WithdrawalCredentials, withdrawalCredentials) {
outputIf(!quiet, "Withdrawal public key incorrect")
outputIf(!quiet, "Withdrawal credentials incorrect")
return false, nil
}
outputIf(!quiet, "Withdrawal credentials verified")
@@ -246,7 +254,7 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
if err != nil {
return false, errors.Wrap(err, "failed to decode fork version")
}
if bytes.Equal(deposit.ForkVersion, forkVersion[:]) {
if bytes.Equal(deposit.ForkVersion, forkVersion) {
outputIf(!quiet, "Fork version verified")
} else {
outputIf(!quiet, "Fork version incorrect")
@@ -263,6 +271,7 @@ func init() {
depositFlags(depositVerifyCmd)
depositVerifyCmd.Flags().StringVar(&depositVerifyData, "data", "", "JSON data, or path to JSON data")
depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalPubKey, "withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn")
depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalAddress, "withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn")
depositVerifyCmd.Flags().StringVar(&depositVerifyDepositAmount, "depositvalue", "32 Ether", "Value of the amount to be deposited")
depositVerifyCmd.Flags().StringVar(&depositVerifyValidatorPubKey, "validatorpubkey", "", "Public key(s) of the account(s) that will be carrying out validation")
depositVerifyCmd.Flags().StringVar(&depositVerifyForkVersion, "forkversion", "0x00000000", "Fork version of the chain of the deposit")

View File

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

View File

@@ -23,8 +23,6 @@ import (
"github.com/pkg/errors"
)
var jsonOutput bool
func process(ctx context.Context, data *dataIn) error {
if data == nil {
return errors.New("no data")

View File

@@ -27,6 +27,3 @@ var proposerCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(proposerCmd)
}
func proposerFlags(cmd *cobra.Command) {
}

View File

@@ -74,6 +74,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
attesterInclusionBindings()
case "block/info":
blockInfoBindings()
case "chain/time":
chainTimeBindings()
case "exit/verify":
exitVerifyBindings()
case "node/events":
@@ -88,10 +90,16 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
validatorExitBindings()
case "validator/info":
validatorInfoBindings()
case "validator/keycheck":
validatorKeycheckBindings()
case "wallet/create":
walletCreateBindings()
case "wallet/import":
walletImportBindings()
case "wallet/sharedexport":
walletSharedExportBindings()
case "wallet/sharedimport":
walletSharedImportBindings()
}
if quiet && verbose {

View File

@@ -40,5 +40,5 @@ func output(ctx context.Context, data *dataOut) (string, error) {
if data.verbose {
return fmt.Sprintf("%s - %s", data.startTime, data.endTime), nil
}
return fmt.Sprintf("%s", data.startTime), nil
return data.startTime.String(), nil
}

View File

@@ -53,7 +53,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
slotDuration := config["SECONDS_PER_SLOT"].(time.Duration)
results.startTime = genesis.GenesisTime.Add(time.Duration((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second)))
results.startTime = genesis.GenesisTime.Add((time.Duration(slot*int64(slotDuration.Seconds())) * time.Second))
results.endTime = results.startTime.Add(slotDuration)
return results, nil

View File

@@ -17,25 +17,28 @@ import (
"context"
"encoding/hex"
"strings"
"time"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
ethdoutil "github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
string2eth "github.com/wealdtech/go-string2eth"
)
type dataIn struct {
format string
withdrawalCredentials []byte
amount spec.Gwei
validatorAccounts []e2wtypes.Account
forkVersion *spec.Version
domain *spec.Domain
passphrases []string
format string
timeout time.Duration
withdrawalAccount string
withdrawalPubKey string
withdrawalAddress string
amount spec.Gwei
validatorAccounts []e2wtypes.Account
forkVersion *spec.Version
domain *spec.Domain
passphrases []string
}
func input() (*dataIn, error) {
@@ -49,6 +52,11 @@ func input() (*dataIn, error) {
return nil, errors.New("validator account is required")
}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, data.validatorAccounts, err = ethdoutil.WalletAndAccountsFromPath(ctx, viper.GetString("validatoraccount"))
@@ -70,37 +78,25 @@ func input() (*dataIn, error) {
data.passphrases = ethdoutil.GetPassphrases()
switch {
case viper.GetString("withdrawalaccount") != "":
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, withdrawalAccount, err := ethdoutil.WalletAndAccountFromPath(ctx, viper.GetString("withdrawalaccount"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain withdrawal account")
}
pubKey, err := ethdoutil.BestPublicKey(withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for withdrawal account")
}
data.withdrawalCredentials = util.SHA256(pubKey.Marshal())
case viper.GetString("withdrawalpubkey") != "":
withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("withdrawalpubkey"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal public key")
}
if len(withdrawalPubKeyBytes) != 48 {
return nil, errors.New("withdrawal public key must be exactly 48 bytes in length")
}
withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "withdrawal public key is not valid")
}
data.withdrawalCredentials = util.SHA256(withdrawalPubKey.Marshal())
default:
return nil, errors.New("withdrawalaccount or withdrawal public key is required")
data.withdrawalAccount = viper.GetString("withdrawalaccount")
data.withdrawalPubKey = viper.GetString("withdrawalpubkey")
data.withdrawalAddress = viper.GetString("withdrawaladdress")
withdrawalDetailsPresent := 0
if data.withdrawalAccount != "" {
withdrawalDetailsPresent++
}
if data.withdrawalPubKey != "" {
withdrawalDetailsPresent++
}
if data.withdrawalAddress != "" {
withdrawalDetailsPresent++
}
if withdrawalDetailsPresent == 0 {
return nil, errors.New("withdrawal account, public key or address is required")
}
if withdrawalDetailsPresent > 1 {
return nil, errors.New("only one of withdrawal account, public key or address is allowed")
}
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
data.withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
if viper.GetString("depositvalue") == "" {
return nil, errors.New("deposit value is required")
@@ -135,7 +131,7 @@ func inputForkVersion(ctx context.Context) (*spec.Version, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to decode fork version")
}
if len(forkVersion) != 4 {
if len(data) != 4 {
return nil, errors.New("fork version must be exactly 4 bytes in length")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019-2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -84,9 +84,20 @@ func TestInput(t *testing.T) {
name: "Nil",
err: "validator account is required",
},
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "timeout is required",
},
{
name: "ValidatorAccountMissing",
vars: map[string]interface{}{
"timeout": "10s",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
@@ -96,6 +107,7 @@ func TestInput(t *testing.T) {
{
name: "ValidatorAccountUnknown",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Unknown",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
@@ -104,59 +116,74 @@ func TestInput(t *testing.T) {
err: "unknown validator account",
},
{
name: "WithdrawalAccountMissing",
name: "WithdrawalDetailsMissing",
vars: map[string]interface{}{
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "withdrawalaccount or withdrawal public key is required",
err: "withdrawal account, public key or address is required",
},
{
name: "WithdrawalAccountUnknown",
name: "WithdrawalDetailsTooMany1",
vars: map[string]interface{}{
"raw": true,
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Unknown",
"withdrawalaccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "failed to obtain withdrawal account: failed to obtain account: no account with name \"Unknown\"",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "WithdrawalPubKeyInvalid",
name: "WithdrawalDetailsTooMany2",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "invalid",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "failed to decode withdrawal public key: encoding/hex: invalid byte: U+0069 'i'",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "WithdrawalPubKeyWrongLength",
name: "WithdrawalDetailsTooMany3",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0bff",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "withdrawal public key must be exactly 48 bytes in length",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "WithdrawalPubKeyNotPubKey",
name: "WithdrawalDetailsTooMany4",
vars: map[string]interface{}{
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0x089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
"timeout": "10s",
"launchpad": true,
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"withdrawaladdress": "0x30C99930617B7b793beaB603ecEB08691005f2E5",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
err: "withdrawal public key is not valid: failed to deserialize public key: err blsPublicKeyDeserialize 089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
err: "only one of withdrawal account, public key or address is allowed",
},
{
name: "DepositValueMissing",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"forkversion": "0x01020304",
@@ -166,6 +193,7 @@ func TestInput(t *testing.T) {
{
name: "DepositValueTooSmall",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "1000 Wei",
@@ -176,6 +204,7 @@ func TestInput(t *testing.T) {
{
name: "DepositValueInvalid",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "1 groat",
@@ -186,6 +215,7 @@ func TestInput(t *testing.T) {
{
name: "ForkVersionInvalid",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
@@ -193,54 +223,68 @@ func TestInput(t *testing.T) {
},
err: "failed to obtain fork version: failed to decode fork version: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "ForkVersionShort",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01",
},
err: "failed to obtain fork version: fork version must be exactly 4 bytes in length",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
},
res: &dataIn{
format: "json",
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: mainnetForkVersion,
domain: mainnetDomain,
format: "json",
withdrawalAccount: "Test/Interop 0",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: mainnetForkVersion,
domain: mainnetDomain,
},
},
{
name: "GoodForkVersionOverride",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalaccount": "Test/Interop 0",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
res: &dataIn{
format: "json",
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
format: "json",
withdrawalAccount: "Test/Interop 0",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
},
{
name: "GoodWithdrawalPubKey",
vars: map[string]interface{}{
"timeout": "10s",
"validatoraccount": "Test/Interop 0",
"withdrawalpubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
"depositvalue": "32 Ether",
"forkversion": "0x01020304",
},
res: &dataIn{
format: "json",
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
format: "json",
withdrawalPubKey: "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
},
}
@@ -258,7 +302,9 @@ func TestInput(t *testing.T) {
require.NoError(t, err)
// Cannot compare accounts directly, so need to check each element individually.
require.Equal(t, test.res.format, res.format)
require.Equal(t, test.res.withdrawalCredentials, res.withdrawalCredentials)
require.Equal(t, test.res.withdrawalAccount, res.withdrawalAccount)
require.Equal(t, test.res.withdrawalAddress, res.withdrawalAddress)
require.Equal(t, test.res.withdrawalPubKey, res.withdrawalPubKey)
require.Equal(t, test.res.amount, res.amount)
require.Equal(t, test.res.forkVersion, res.forkVersion)
require.Equal(t, test.res.domain, res.domain)

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019-2021 Weald Technology Limited.
// 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
@@ -15,12 +15,16 @@ package depositdata
import (
"context"
"encoding/hex"
"fmt"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/signing"
"github.com/wealdtech/ethdo/util"
ethdoutil "github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -31,8 +35,13 @@ func process(data *dataIn) ([]*dataOut, error) {
results := make([]*dataOut, 0)
withdrawalCredentials, err := createWithdrawalCredentials(data)
if err != nil {
return nil, err
}
for _, validatorAccount := range data.validatorAccounts {
validatorPubKey, err := util.BestPublicKey(validatorAccount)
validatorPubKey, err := ethdoutil.BestPublicKey(validatorAccount)
if err != nil {
return nil, errors.Wrap(err, "validator account does not provide a public key")
}
@@ -41,7 +50,7 @@ func process(data *dataIn) ([]*dataOut, error) {
copy(pubKey[:], validatorPubKey.Marshal())
depositMessage := &spec.DepositMessage{
PublicKey: pubKey,
WithdrawalCredentials: data.withdrawalCredentials,
WithdrawalCredentials: withdrawalCredentials,
Amount: data.amount,
}
root, err := depositMessage.HashTreeRoot()
@@ -58,7 +67,7 @@ func process(data *dataIn) ([]*dataOut, error) {
depositData := &spec.DepositData{
PublicKey: pubKey,
WithdrawalCredentials: data.withdrawalCredentials,
WithdrawalCredentials: withdrawalCredentials,
Amount: data.amount,
Signature: sig,
}
@@ -75,7 +84,7 @@ func process(data *dataIn) ([]*dataOut, error) {
format: data.format,
account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()),
validatorPubKey: &pubKey,
withdrawalCredentials: data.withdrawalCredentials,
withdrawalCredentials: withdrawalCredentials,
amount: data.amount,
signature: &sig,
forkVersion: data.forkVersion,
@@ -85,3 +94,80 @@ func process(data *dataIn) ([]*dataOut, error) {
}
return results, nil
}
// createWithdrawalCredentials creates withdrawal credentials given an account, public key or Ethereum 1 address.
func createWithdrawalCredentials(data *dataIn) ([]byte, error) {
var withdrawalCredentials []byte
switch {
case data.withdrawalAccount != "":
ctx, cancel := context.WithTimeout(context.Background(), data.timeout)
defer cancel()
_, withdrawalAccount, err := ethdoutil.WalletAndAccountFromPath(ctx, data.withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain withdrawal account")
}
pubKey, err := ethdoutil.BestPublicKey(withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for withdrawal account")
}
withdrawalCredentials = util.SHA256(pubKey.Marshal())
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
case data.withdrawalPubKey != "":
withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalPubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal public key")
}
if len(withdrawalPubKeyBytes) != 48 {
return nil, errors.New("withdrawal public key must be exactly 48 bytes in length")
}
pubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "withdrawal public key is not valid")
}
withdrawalCredentials = util.SHA256(pubKey.Marshal())
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
case data.withdrawalAddress != "":
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalAddress, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal address")
}
if len(withdrawalAddressBytes) != 20 {
return nil, errors.New("withdrawal address must be exactly 20 bytes in length")
}
// Ensure the address is properly checksummed.
checksummedAddress := addressBytesToEIP55(withdrawalAddressBytes)
if checksummedAddress != data.withdrawalAddress {
return nil, fmt.Errorf("withdrawal address checksum does not match (expected %s)", checksummedAddress)
}
withdrawalCredentials = make([]byte, 32)
copy(withdrawalCredentials[12:32], withdrawalAddressBytes)
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX
default:
return nil, errors.New("withdrawal account, public key or address is required")
}
return withdrawalCredentials, nil
}
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
func addressBytesToEIP55(address []byte) string {
bytes := []byte(fmt.Sprintf("%x", address))
hash := util.Keccak256(bytes)
for i := 0; i < len(bytes); i++ {
hashByte := hash[i/2]
if i%2 == 0 {
hashByte >>= 4
} else {
hashByte &= 0xf
}
if bytes[i] > '9' && hashByte > 7 {
bytes[i] -= 32
}
}
return fmt.Sprintf("0x%s", string(bytes))
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 eald Technology Trading
// Copyright © 2019-2021 Weald Technology Limited.
// 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
@@ -15,6 +15,8 @@ package depositdata
import (
"context"
"encoding/hex"
"strings"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
@@ -49,6 +51,10 @@ func TestProcess(t *testing.T) {
)
require.NoError(t, err)
withdrawalAccount := "Test/Interop 0"
withdrawalPubKey := "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"
withdrawalAddress := "0x30C99930617B7b793beaB603ecEB08691005f2E5"
var validatorPubKey *spec.BLSPubKey
{
tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c")
@@ -101,6 +107,22 @@ func TestProcess(t *testing.T) {
depositMessageRoot2 = &tmp
}
var depositDataRoot3 *spec.Root
{
tmp := testutil.HexToRoot("0x489500535b03dd9deffa0f00cb38d82346111856fb58a9541fe1f01a1a97429c")
depositDataRoot3 = &tmp
}
var depositMessageRoot3 *spec.Root
{
tmp := testutil.HexToRoot("0x7b8ee5694e4338cf2bfe5a4d2f46540f0ade85ebd30713673cf5783c4e925681")
depositMessageRoot3 = &tmp
}
var signature3 *spec.BLSSignature
{
tmp := testutil.HexToSignature("0xba0019d5c421f205d845782f52a87ab95cd489fbef2911f8a1f9cf7c14b4ce59eefa82641e770a4cb405534b7776d0f801b0a8b178c1b71b718c104e89f4e633da10a398c7919a00c403d58f3f4b827af8adb263b192e7a45b0ed1926dff5f66")
signature3 = &tmp
}
tests := []struct {
name string
dataIn *dataIn
@@ -111,16 +133,119 @@ func TestProcess(t *testing.T) {
name: "Nil",
err: "no data",
},
{
name: "WithdrawalDetailsMissing",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal account, public key or address is required",
},
{
name: "WithdrawalAccountUnknown",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAccount: "Unknown",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "failed to obtain withdrawal account: failed to open wallet for account: wallet not found",
},
{
name: "WithdrawalPubKeyInvalid",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: "invalid",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "failed to decode withdrawal public key: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "WithdrawalPubKeyWrongLength",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0bff",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal public key must be exactly 48 bytes in length",
},
{
name: "WithdrawalPubKeyNotPubKey",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: "0x089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal public key is not valid: failed to deserialize public key: err blsPublicKeyDeserialize 089bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b",
},
{
name: "WithdrawalAddressInvalid",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: "invalid",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "failed to decode withdrawal address: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "WithdrawalAddressWrongLength",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: "0x30C99930617B7b793beaB603ecEB08691005f2",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal address must be exactly 20 bytes in length",
},
{
name: "WithdrawalAddressIncorrectChecksum",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: "0x30c99930617b7b793beab603eceb08691005f2e5",
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
err: "withdrawal address checksum does not match (expected 0x30C99930617B7b793beaB603ecEB08691005f2E5)",
},
{
name: "Single",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
format: "raw",
passphrases: []string{"pass"},
withdrawalAccount: withdrawalAccount,
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
@@ -139,13 +264,13 @@ func TestProcess(t *testing.T) {
{
name: "Double",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0, interop1},
forkVersion: forkVersion,
domain: domain,
format: "raw",
passphrases: []string{"pass"},
withdrawalPubKey: withdrawalPubKey,
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0, interop1},
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
@@ -172,6 +297,31 @@ func TestProcess(t *testing.T) {
},
},
},
{
name: "WithdrawalAddress",
dataIn: &dataIn{
format: "raw",
passphrases: []string{"pass"},
withdrawalAddress: withdrawalAddress,
amount: 32000000000,
validatorAccounts: []e2wtypes.Account{interop0},
forkVersion: forkVersion,
domain: domain,
},
res: []*dataOut{
{
format: "raw",
account: "Test/Interop 0",
validatorPubKey: validatorPubKey,
amount: 32000000000,
withdrawalCredentials: testutil.HexToBytes("0x01000000000000000000000030C99930617B7b793beaB603ecEB08691005f2E5"),
signature: signature3,
forkVersion: forkVersion,
depositDataRoot: depositDataRoot3,
depositMessageRoot: depositMessageRoot3,
},
},
},
}
for _, test := range tests {
@@ -186,3 +336,18 @@ func TestProcess(t *testing.T) {
})
}
}
func TestAddressBytesToEIP55(t *testing.T) {
tests := []string{
"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
}
for _, test := range tests {
bytes, err := hex.DecodeString(strings.TrimPrefix(test, "0x"))
require.NoError(t, err)
require.Equal(t, addressBytesToEIP55(bytes), test)
}
}

View File

@@ -58,17 +58,20 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
thisEpochAttesterDuty, err := attesterDuty(ctx, eth2Client, validatorIndex, thisEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain this epoch duty for validator")
return nil, errors.Wrap(err, "failed to obtain this epoch attester duty for validator")
}
results.thisEpochAttesterDuty = thisEpochAttesterDuty
thisEpochProposerDuties, err := proposerDuties(ctx, eth2Client, validatorIndex, thisEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain this epoch proposer duties for validator")
}
results.thisEpochProposerDuties = thisEpochProposerDuties
nextEpoch := thisEpoch + 1
nextEpochAttesterDuty, err := attesterDuty(ctx, eth2Client, validatorIndex, nextEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain next epoch duty for validator")
return nil, errors.Wrap(err, "failed to obtain next epoch attester duty for validator")
}
results.nextEpochAttesterDuty = nextEpochAttesterDuty

View File

@@ -0,0 +1,55 @@
// 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 validatorkeycheck
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
quiet bool
verbose bool
debug bool
// Withdrawal credentials.
withdrawalCredentials string
// Operation.
mnemonic string
privKey string
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Withdrawal credentials.
data.withdrawalCredentials = viper.GetString("withdrawal-credentials")
if data.withdrawalCredentials == "" {
return nil, errors.New("withdrawal credentials are required")
}
data.mnemonic = viper.GetString("mnemonic")
data.privKey = viper.GetString("privkey")
if data.mnemonic == "" && data.privKey == "" {
return nil, errors.New("mnemonic or privkey is required")
}
return data, nil
}

View File

@@ -0,0 +1,71 @@
// 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 validatorkeycheck
import (
"context"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestInput(t *testing.T) {
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "WithdrawalCredentialsMissing",
vars: map[string]interface{}{},
err: "withdrawal credentials are required",
},
{
name: "MnemonicAndPrivateKeyMissing",
vars: map[string]interface{}{
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
},
err: "mnemonic or privkey is required",
},
{
name: "GoodWithMnemonic",
vars: map[string]interface{}{
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
},
res: &dataIn{
withdrawalCredentials: "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
},
},
}
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.withdrawalCredentials, res.withdrawalCredentials)
}
})
}
}

View File

@@ -0,0 +1,52 @@
// 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 validatorkeycheck
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
match bool
path string
}
func output(ctx context.Context, data *dataOut) (string, int, error) {
if data == nil {
return "", 1, errors.New("no data")
}
if data.quiet {
if !data.match {
os.Exit(1)
}
return "", 1, nil
}
if data.match {
if data.path == "" {
return "Withdrawal credentials confirmed", 0, nil
}
return fmt.Sprintf("Withdrawal credentials confirmed at path %s", data.path), 0, nil
}
return "Could not confirm withdrawal credentials with given information", 1, nil
}

View File

@@ -0,0 +1,81 @@
// 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 validatorkeycheck
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
exitCode int
expected []string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Not found",
dataOut: &dataOut{
match: false,
},
exitCode: 1,
expected: []string{
"Could not confirm withdrawal credentials with given information",
},
},
{
name: "Found",
dataOut: &dataOut{
match: true,
},
expected: []string{
"Withdrawal credentials confirmed",
},
},
{
name: "FoundWithPath",
dataOut: &dataOut{
match: true,
path: "m/12381/3600/10/0",
},
expected: []string{
"Withdrawal credentials confirmed at path m/12381/3600/10/0",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, exitCode, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.exitCode, exitCode)
for _, expected := range test.expected {
require.True(t, strings.Contains(res, expected))
}
}
})
}
}

View File

@@ -0,0 +1,123 @@
// 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 validatorkeycheck
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
"golang.org/x/text/unicode/norm"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
validatorWithdrawalCredentials, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalCredentials, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to parse withdrawal credentials")
}
match := false
path := ""
if data.privKey != "" {
// Single private key to check.
keyBytes, err := hex.DecodeString(strings.TrimPrefix(data.privKey, "0x"))
if err != nil {
return nil, err
}
key, err := e2types.BLSPrivateKeyFromBytes(keyBytes)
if err != nil {
return nil, err
}
match, err = checkPrivKey(ctx, data.debug, validatorWithdrawalCredentials, key)
if err != nil {
return nil, err
}
} else {
// Mnemonic to check.
match, path, err = checkMnemonic(ctx, data.debug, validatorWithdrawalCredentials, data.mnemonic)
if err != nil {
return nil, err
}
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
match: match,
path: path,
}
return results, nil
}
func checkPrivKey(ctx context.Context, debug bool, validatorWithdrawalCredentials []byte, key *e2types.BLSPrivateKey) (bool, error) {
pubKey := key.PublicKey()
withdrawalCredentials := util.SHA256(pubKey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
return bytes.Equal(withdrawalCredentials, validatorWithdrawalCredentials), nil
}
func checkMnemonic(ctx context.Context, debug bool, validatorWithdrawalCredentials []byte, mnemonic string) (bool, string, error) {
// If there are more than 24 words we treat the additional characters as the passphrase.
mnemonicParts := strings.Split(mnemonic, " ")
mnemonicPassphrase := ""
if len(mnemonicParts) > 24 {
mnemonic = strings.Join(mnemonicParts[:24], " ")
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
}
// Normalise the input.
mnemonic = string(norm.NFKD.Bytes([]byte(mnemonic)))
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
if !bip39.IsMnemonicValid(mnemonic) {
return false, "", errors.New("mnemonic is invalid")
}
// Create seed from mnemonic and passphrase.
seed := bip39.NewSeed(mnemonic, mnemonicPassphrase)
// Check first 1024 indices.
for i := 0; i < 1024; i++ {
path := fmt.Sprintf("m/12381/3600/%d/0", i)
if debug {
fmt.Printf("Checking path %s\n", path)
}
key, err := util.PrivateKeyFromSeedAndPath(seed, path)
if err != nil {
return false, "", errors.Wrap(err, "failed to generate key")
}
match, err := checkPrivKey(ctx, debug, validatorWithdrawalCredentials, key)
if err != nil {
return false, "", errors.Wrap(err, "failed to match key")
}
if match {
return true, path, nil
}
}
return false, "", nil
}

View File

@@ -0,0 +1,51 @@
// 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 validatorkeycheck
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// 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 "", err
}
results, exitCode, err := output(ctx, dataOut)
if err != nil {
return "", err
}
if exitCode != 0 {
fmt.Println(results)
os.Exit(exitCode)
}
return results, nil
}

View File

@@ -49,9 +49,10 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
func init() {
validatorCmd.AddCommand(validatorDepositDataCmd)
validatorFlags(validatorDepositDataCmd)
validatorDepositDataCmd.Flags().String("validatoraccount", "", "Account of the account carrying out the validation")
validatorDepositDataCmd.Flags().String("withdrawalaccount", "", "Account of the account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("validatoraccount", "", "Account carrying out the validation")
validatorDepositDataCmd.Flags().String("withdrawalaccount", "", "Account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("withdrawaladdress", "", "Ethereum 1 address of the account to which the validator funds will be withdrawn")
validatorDepositDataCmd.Flags().String("depositvalue", "", "Value of the amount to be deposited")
validatorDepositDataCmd.Flags().Bool("raw", false, "Print raw deposit data transaction data")
validatorDepositDataCmd.Flags().String("forkversion", "", "Use a hard-coded fork version (default is to fetch it from the node)")
@@ -68,6 +69,9 @@ func validatorDepositdataBindings() {
if err := viper.BindPFlag("withdrawalpubkey", validatorDepositDataCmd.Flags().Lookup("withdrawalpubkey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("withdrawaladdress", validatorDepositDataCmd.Flags().Lookup("withdrawaladdress")); err != nil {
panic(err)
}
if err := viper.BindPFlag("depositvalue", validatorDepositDataCmd.Flags().Lookup("depositvalue")); err != nil {
panic(err)
}

View File

@@ -39,7 +39,7 @@ In quiet mode this will return 0 if the the duties have been obtained, otherwise
if viper.GetBool("quiet") {
return nil
}
fmt.Printf(res)
fmt.Print(res)
return nil
},
}

67
cmd/validatorkeycheck.go Normal file
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 cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
validatorkeycheck "github.com/wealdtech/ethdo/cmd/validator/keycheck"
)
var validatorKeycheckCmd = &cobra.Command{
Use: "keycheck",
Short: "Check that the withdrawal credentials for a validator matches the given key.",
Long: `Check that the withdrawal credentials for a validator matches the given key. For example:
ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --privkey=0x1b46e61babc7a6a0fbfe8e416de3c71f85e367f24e0bfcb12e57adb11117662c
A mnemonic can be used in place of a private key, in which case the first 1,024 indices of the standard withdrawal key path will be scanned for a matching key.
In quiet mode this will return 0 if the withdrawal credentials match the key, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorkeycheck.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
validatorCmd.AddCommand(validatorKeycheckCmd)
validatorFlags(validatorKeycheckCmd)
validatorKeycheckCmd.Flags().String("withdrawal-credentials", "", "Withdrawal credentials to check (can run offline)")
validatorKeycheckCmd.Flags().String("mnemonic", "", "Mnemonic from which to generate withdrawal credentials")
validatorKeycheckCmd.Flags().String("privkey", "", "Private key from which to generate withdrawal credentials")
}
func validatorKeycheckBindings() {
if err := viper.BindPFlag("withdrawal-credentials", validatorKeycheckCmd.Flags().Lookup("withdrawal-credentials")); err != nil {
panic(err)
}
if err := viper.BindPFlag("mnemonic", validatorKeycheckCmd.Flags().Lookup("mnemonic")); err != nil {
panic(err)
}
if err := viper.BindPFlag("privkey", validatorKeycheckCmd.Flags().Lookup("privkey")); err != nil {
panic(err)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -15,7 +15,7 @@ The first thing you need to do is to create a wallet. To do this run the comman
- rename the wallet to something other than `Wallet` if you so desire. If so, you will need to change it in all subsequent commands
```
$ ethdo wallet create --type=hd --mnemonic='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art' --wallet=Wallet --wallet-passphrase=secret
$ ethdo wallet create --type=hd --mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" --wallet=Wallet --wallet-passphrase=secret
```
### I want an account with a specific public key.
@@ -87,5 +87,5 @@ If you wish to have this data for a particular test network you will need to sup
It is possible to derive keys directly from a mnemonic and path without going through the interim steps. Note that this will _not_ create accounts, and cannot be used to then sign data or requests. This may or not be desirable, depending on your requirements.
```
$ ethdo account derive --mnemonic='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art' --path=m/12381/3600/0/0
$ ethdo account derive --mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" --path=m/12381/3600/0/0
```

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.
@@ -325,6 +352,23 @@ Prior justified epoch: 3
Prior justified epoch distance: 4
```
#### `time`
`ethdo chain time` calculates the time period of Ethereum 2 epochs and slots. Options include:
- `epoch` show epoch and slot times for the given epoch
- `slot` show epoch and slot times for the given slot
- `timestamp` show epoch and slot times for the given timestamp
```sh
$ ethdo chain time --epoch=1234
Epoch 1234
Epoch start 2020-12-06 23:37:59
Epoch end 2020-12-06 23:44:23
Slot 39488
Slot start 2020-12-06 23:37:59
Slot end 2020-12-06 23:38:11
```
### `deposit` comands
Deposit commands focus on information about deposit data information in a JSON file generated by the `ethdo validator depositdata` command.
@@ -471,6 +515,18 @@ Balance: 3.201850307 Ether
Effective balance: 3.1 Ether
```
#### `keycheck`
`ethdo validator keycheck` checks if a given key matches a validator's withdrawal credentials. Options include:
- `withdrawal-credentials` the withdrawal credentials against which to match
- `privkey` the private key used to generat matching withdrawal credentials
- `mnemonic` the mnemonic used to generate matching withdrawal credentials
```sh
$ ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --mnemonic='abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art'
Withdrawal credentials confirmed at path m/12381/3600/10/0
```
### `attester` commands
Attester commands focus on Ethereum 2 validators' actions as attesters.

80
go.mod
View File

@@ -3,61 +3,65 @@ 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.40.14 // indirect
github.com/dgraph-io/badger/v2 v2.2007.3 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/ferranbt/fastssz v0.0.0-20210719200358-90640294cb9c
github.com/goccy/go-yaml v1.9.2 // indirect
github.com/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/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/uuid v1.3.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.1-vault-3 // indirect
github.com/herumi/bls-eth-go-binary v0.0.0-20210520070601-31246bfa8ac4
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/kr/text v0.2.0 // indirect
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.1 // indirect
github.com/protolambda/zssz v0.1.5 // indirect
github.com/prysmaticlabs/ethereumapis v0.0.0-20210201130911-92b2a467c108
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-20210706153858-5cb5ce8bdbfe
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/cast v1.4.0 // indirect
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-types/v2 v2.5.5
github.com/wealdtech/go-eth2-util v1.6.4
github.com/wealdtech/go-eth2-wallet v1.14.4
github.com/wealdtech/go-eth2-wallet-dirk v1.1.5
github.com/wealdtech/go-eth2-wallet-dirk v1.1.6
github.com/wealdtech/go-eth2-wallet-distributed v1.1.3
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.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.4
github.com/wealdtech/go-string2eth v1.1.0
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/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/text v0.3.6
google.golang.org/genproto v0.0.0-20210803142424-70bd63adacf2 // indirect
google.golang.org/grpc v1.39.0
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
)

680
go.sum

File diff suppressed because it is too large Load Diff

234
shamir/shamir.go Normal file
View File

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

198
shamir/shamir_test.go Normal file
View File

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

79
shamir/tables.go Normal file
View File

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

13
shamir/tables_test.go Normal file
View File

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