Part 1: Implement Accounts-V2 New, Wallet Creation (#6451)

* begin accounts-v2 new

* password validation

* validator accounts new with eip-2335 keystore

* select different wallet type based on enum

* clean up code significantly

* more robust code structure

* check if wallet exists

* define read and create wallet methods

* fmt

* go mod and comment

* comment

* redundant name

* satify gofmt

* add instructions with keymanager opts

* wrap up create and read wallet functionality

* prep for readiness

* doc improvements

* tests for create and read wallet

* update deps

* tidy

* visibility

* gaz

* fix up

* refactor for proper usage, with wallet and keymanager ifaces

* Update validator/flags/flags.go

Co-authored-by: Ivan Martinez <ivanthegreatdev@gmail.com>

* import

* improve structure

* wrap up all comments

* simplify

* lint

* Update validator/accounts/v2/cmd.go

* viz check

* add interface methods as needed

* fix build

* lint

* nishant feedback

* simplify structure

* add tests for strong password check

* all feedback done

* ivan feedback

* ivan feedback

Co-authored-by: Ivan Martinez <ivanthegreatdev@gmail.com>
Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
Raul Jordan
2020-07-01 16:30:01 -05:00
committed by GitHub
parent 8c8cc144f1
commit 2d6f4ebf18
19 changed files with 924 additions and 2 deletions

View File

@@ -157,7 +157,7 @@ go_rules_dependencies()
go_register_toolchains(nogo = "@//:nogo")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
gazelle_dependencies()
@@ -354,3 +354,10 @@ load("@com_github_ethereum_go_ethereum//:deps.bzl", "geth_dependencies")
geth_dependencies()
# Do NOT add new go dependencies here! Refer to DEPENDENCIES.md!
go_repository(
name = "com_github_nbutton23_zxcvbn_go",
importpath = "github.com/nbutton23/zxcvbn-go",
sum = "h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E=",
version = "v0.0.0-20180912185939-ae427f1e4c1d",
)

View File

@@ -3519,3 +3519,23 @@ def prysm_deps():
sum = "h1:Qh4dB5D/WpoUUp3lSod7qgoyEHbDGPUWjIbnqdqqe1k=",
version = "v0.0.0-20190515093506-e2840ee46a6b",
)
go_repository(
name = "com_github_juju_ansiterm",
importpath = "github.com/juju/ansiterm",
sum = "h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=",
version = "v0.0.0-20180109212912-720a0952cc2a",
)
go_repository(
name = "com_github_manifoldco_promptui",
importpath = "github.com/manifoldco/promptui",
sum = "h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4=",
version = "v0.7.0",
)
go_repository(
name = "com_github_dustinkirkland_golang_petname",
importpath = "github.com/dustinkirkland/golang-petname",
sum = "h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=",
version = "v0.0.0-20191129215211-8e5a1ed0cff0",
)

2
go.mod
View File

@@ -65,11 +65,13 @@ require (
github.com/libp2p/go-libp2p-tls v0.1.4-0.20200421131144-8a8ad624a291 // indirect
github.com/libp2p/go-libp2p-yamux v0.2.8 // indirect
github.com/libp2p/go-maddr-filter v0.1.0 // indirect
github.com/manifoldco/promptui v0.7.0
github.com/minio/highwayhash v1.0.0
github.com/minio/sha256-simd v0.1.1
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/multiformats/go-multiaddr v0.2.2
github.com/multiformats/go-multiaddr-net v0.1.5
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d
github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/paulbellamy/ratecounter v0.2.0

14
go.sum
View File

@@ -113,6 +113,7 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/c-bata/go-prompt v0.2.2 h1:uyKRz6Z6DUyj49QVijyM339UJV9yhbr70gESwbNU3e0=
github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -125,8 +126,11 @@ github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
@@ -456,6 +460,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@@ -682,12 +688,16 @@ github.com/libp2p/go-yamux v1.3.7 h1:v40A1eSPJDIZwz2AvrV3cxpTZEGDP11QJbukmEhYyQI
github.com/libp2p/go-yamux v1.3.7/go.mod h1:fr7aVgmdNGJK+N1g+b6DW6VxzbRCjCOejR/hkmpooHE=
github.com/lucas-clemente/quic-go v0.15.7 h1:Pu7To5/G9JoP1mwlrcIvfV8ByPBlCzif3MCl8+1W83I=
github.com/lucas-clemente/quic-go v0.15.7/go.mod h1:Myi1OyS0FOjL3not4BxT7KN29bRkcMUV5JVVFLKtDp8=
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4=
github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
github.com/marten-seemann/qtls v0.9.1 h1:O0YKQxNVPaiFgMng0suWEOY2Sb4LT2sRn9Qimq3Z1IQ=
github.com/marten-seemann/qtls v0.9.1/go.mod h1:T1MmAdDPyISzxlK6kjRr0pcZFBVd1OZbBb/j3cvzHhk=
@@ -798,6 +808,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@@ -852,6 +864,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5 h1:tFwafIEMf0B7NlcxV/zJ6leBIa81D3hgGSgsE5hCkOQ=
github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -1230,6 +1243,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@@ -22,6 +22,7 @@ go_library(
"//shared/params:go_default_library",
"//shared/version:go_default_library",
"//validator/accounts/v1:go_default_library",
"//validator/accounts/v2:go_default_library",
"//validator/client/streaming:go_default_library",
"//validator/flags:go_default_library",
"//validator/node:go_default_library",
@@ -63,7 +64,7 @@ go_image(
"//shared/params:go_default_library",
"//shared/version:go_default_library",
"//validator/accounts/v1:go_default_library",
"//validator/client/polling:go_default_library",
"//validator/accounts/v2:go_default_library",
"//validator/client/streaming:go_default_library",
"//validator/flags:go_default_library",
"//validator/node:go_default_library",

View File

@@ -0,0 +1,43 @@
load("@io_bazel_rules_go//go:def.bzl", "go_test")
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"cmd.go",
"doc.go",
"new.go",
"wallet.go",
],
importpath = "github.com/prysmaticlabs/prysm/validator/accounts/v2",
visibility = [
"//validator:__pkg__",
"//validator:__subpackages__",
],
deps = [
"//shared/featureconfig:go_default_library",
"//validator/flags:go_default_library",
"//validator/keymanager/v2:go_default_library",
"//validator/keymanager/v2/direct:go_default_library",
"@com_github_manifoldco_promptui//:go_default_library",
"@com_github_nbutton23_zxcvbn_go//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"new_test.go",
"wallet_test.go",
],
embed = [":go_default_library"],
deps = [
"//shared/testutil:go_default_library",
"//validator/keymanager/v2:go_default_library",
"//validator/keymanager/v2/direct:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],
)

View File

@@ -0,0 +1,28 @@
package v2
import (
"github.com/prysmaticlabs/prysm/shared/featureconfig"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/urfave/cli/v2"
)
// Commands for accounts-v2 for Prysm validators.
var Commands = &cli.Command{
Name: "accounts-v2",
Category: "accounts-v2",
Usage: "defines commands for interacting with eth2 validator accounts (work in progress)",
Subcommands: []*cli.Command{
{
Name: "new",
Description: `creates a new validator account for eth2. If no account exists at the wallet path, creates a new wallet for a user based on
specified input, capable of creating a direct, derived, or remote wallet.
this command outputs a deposit data string which is required to become a validator in eth2.`,
Flags: append(featureconfig.ActiveFlags(featureconfig.ValidatorFlags),
[]cli.Flag{
flags.WalletDirFlag,
flags.WalletPasswordsDirFlag,
}...),
Action: NewAccount,
},
},
}

View File

@@ -0,0 +1,4 @@
// Package v2 defines a new model for accounts management in Prysm, using best
// practices for user security, UX, and extensibility via different wallet types
// including derived, HD wallets and remote-signing capable configurations.
package v2

View File

@@ -0,0 +1,304 @@
package v2
import (
"context"
"fmt"
"os"
"path"
"unicode"
"github.com/manifoldco/promptui"
strongPasswords "github.com/nbutton23/zxcvbn-go"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/validator/flags"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var log = logrus.WithField("prefix", "accounts-v2")
const (
minPasswordLength = 8
// Min password score of 3 out of 5 based on the https://github.com/nbutton23/zxcvbn-go
// library for strong-entropy password computation.
minPasswordScore = 3
)
var keymanagerKindSelections = map[v2keymanager.Kind]string{
v2keymanager.Direct: "Direct, On-Disk Accounts (Recommended)",
v2keymanager.Derived: "Derived Accounts (Advanced)",
v2keymanager.Remote: "Remote Accounts (Advanced)",
}
// NewAccount creates a new validator account from user input. If a user
// does not have an initialized wallet at the specified wallet path, this
// method will create a new wallet and ask user for input for their new wallet's
// available options.
func NewAccount(cliCtx *cli.Context) error {
// Read a wallet's directory from user input.
walletDir, err := inputWalletDir(cliCtx)
if err != nil {
log.Fatalf("Could not parse wallet directory: %v", err)
}
// Read the directory for password storage from user input.
passwordsDirPath := inputPasswordsDirectory(cliCtx)
ctx := context.Background()
// Check if the user has a wallet at the specified path.
// If a user does not have a wallet, we instantiate one
// based on specified options.
var wallet *Wallet
var isNewWallet bool
ok, err := hasWalletDir(walletDir)
if err != nil {
log.Fatalf("Could not check if wallet exists at %s: %v", walletDir, err)
}
if ok {
// Read the wallet from the specified path.
wallet, err = OpenWallet(ctx, &WalletConfig{
PasswordsDir: passwordsDirPath,
WalletDir: walletDir,
})
if err != nil {
log.Fatalf("Could not read wallet at specified path %s: %v", walletDir, err)
}
} else {
// Determine the desired keymanager kind for the wallet from user input.
keymanagerKind, err := inputKeymanagerKind(cliCtx)
if err != nil {
log.Fatalf("Could not select keymanager kind: %v", err)
}
walletConfig := &WalletConfig{
PasswordsDir: passwordsDirPath,
WalletDir: walletDir,
KeymanagerKind: keymanagerKind,
}
wallet, err = CreateWallet(ctx, walletConfig)
if err != nil {
log.Fatalf("Could not create wallet at specified path %s: %v", walletDir, err)
}
isNewWallet = true
}
// We initialize a new keymanager depending on the user's selected keymanager kind.
var keymanager v2keymanager.IKeymanager
if isNewWallet {
keymanager, err = initializeNewKeymanager(ctx, wallet)
} else {
keymanager, err = initializeExistingKeymanager(ctx, wallet)
}
if err != nil {
log.Fatalf("Could not initialize keymanager: %v", err)
}
// Read the new account's password from user input.
password, err := inputAccountPassword(cliCtx)
if err != nil {
log.Fatalf("Could not read password: %v", err)
}
// Create a new validator account using the specified keymanager.
// TODO(#6220): Implement.
if err := keymanager.CreateAccount(ctx, password); err != nil {
log.Fatalf("Could not create account in wallet: %v", err)
}
return nil
}
// Initializes a keymanager. If a config file exists in the wallet, it
// reads the config file and initializes the keymanager that way. Otherwise,
// writes a new configuration file to the wallet and returns the initialized
// keymanager for use.
func initializeNewKeymanager(ctx context.Context, wallet *Wallet) (v2keymanager.IKeymanager, error) {
var keymanager v2keymanager.IKeymanager
var err error
switch wallet.KeymanagerKind() {
case v2keymanager.Direct:
keymanager = direct.NewKeymanager(ctx, wallet, direct.DefaultConfig())
case v2keymanager.Derived:
return nil, errors.New("derived keymanager is unimplemented, work in progress")
case v2keymanager.Remote:
return nil, errors.New("remote keymanager is unimplemented, work in progress")
default:
log.Fatal("Keymanager type must be specified")
}
keymanagerConfig, err := keymanager.MarshalConfigFile(ctx)
if err != nil {
log.Fatalf("Could not marshal keymanager config file: %v", err)
}
if err := wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil {
log.Fatalf("Could not write keymanager config file to disk: %v", err)
}
return keymanager, nil
}
func initializeExistingKeymanager(ctx context.Context, wallet *Wallet) (v2keymanager.IKeymanager, error) {
var keymanager v2keymanager.IKeymanager
var err error
switch wallet.KeymanagerKind() {
case v2keymanager.Direct:
keymanager, err = direct.NewKeymanagerFromConfigFile(ctx, wallet)
if err != nil {
return nil, errors.Wrap(err, "could not initialize direct keymanager from config")
}
case v2keymanager.Derived:
return nil, errors.New("derived keymanager is unimplemented, work in progress")
case v2keymanager.Remote:
return nil, errors.New("remote keymanager is unimplemented, work in progress")
default:
return nil, errors.New("keymanager kind must be specified")
}
return keymanager, nil
}
// Check if a user has an existing wallet at the specified path.
func hasWalletDir(walletPath string) (bool, error) {
_, err := os.Stat(walletPath)
if os.IsNotExist(err) {
return false, nil
}
return true, err
}
func inputWalletDir(cliCtx *cli.Context) (string, error) {
walletDir := cliCtx.String(flags.WalletDirFlag.Name)
if walletDir == flags.DefaultValidatorDir() {
walletDir = path.Join(walletDir, walletDefaultDirName)
}
prompt := promptui.Prompt{
Label: "Enter a wallet directory",
Validate: validateDirectoryPath,
Default: walletDir,
}
walletPath, err := prompt.Run()
if err != nil {
return "", fmt.Errorf("could not determine wallet directory: %v", formatPromptError(err))
}
return walletPath, nil
}
func inputKeymanagerKind(_ *cli.Context) (v2keymanager.Kind, error) {
promptSelect := promptui.Select{
Label: "Select a type of wallet",
Items: []string{
keymanagerKindSelections[v2keymanager.Direct],
keymanagerKindSelections[v2keymanager.Derived],
keymanagerKindSelections[v2keymanager.Remote],
},
}
selection, _, err := promptSelect.Run()
if err != nil {
return v2keymanager.Direct, fmt.Errorf("could not select wallet type: %v", formatPromptError(err))
}
return v2keymanager.Kind(selection), nil
}
func inputAccountPassword(_ *cli.Context) (string, error) {
var hasValidPassword bool
var walletPassword string
for !hasValidPassword {
prompt := promptui.Prompt{
Label: "New account password",
Validate: validatePasswordInput,
Mask: '*',
}
walletPassword, err := prompt.Run()
if err != nil {
return "", fmt.Errorf("could not read wallet password: %v", formatPromptError(err))
}
prompt = promptui.Prompt{
Label: "Confirm password",
Mask: '*',
}
confirmPassword, err := prompt.Run()
if err != nil {
return "", fmt.Errorf("could not read password confirmation: %v", formatPromptError(err))
}
if walletPassword != confirmPassword {
log.Error("Passwords do not match")
continue
}
hasValidPassword = true
}
return walletPassword, nil
}
func inputPasswordsDirectory(cliCtx *cli.Context) string {
passwordsDir := cliCtx.String(flags.WalletPasswordsDirFlag.Name)
if passwordsDir == flags.DefaultValidatorDir() {
passwordsDir = path.Join(passwordsDir, walletDefaultDirName, passwordsDefaultDirName)
}
prompt := promptui.Prompt{
Label: "Passwords directory",
Validate: validateDirectoryPath,
Default: passwordsDir,
}
passwordsPath, err := prompt.Run()
if err != nil {
log.Fatalf("Could not determine passwords directory: %v", formatPromptError(err))
}
return passwordsPath
}
// Validate a strong password input for new accounts,
// including a min length, at least 1 number and at least
// 1 special character.
func validatePasswordInput(input string) error {
var (
hasMinLen = false
hasLetter = false
hasNumber = false
hasSpecial = false
)
if len(input) >= minPasswordLength {
hasMinLen = true
}
for _, char := range input {
switch {
case unicode.IsLetter(char):
hasLetter = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if !(hasMinLen && hasLetter && hasNumber && hasSpecial) {
return errors.New(
"password must have more than 8 characters, at least 1 special character, and 1 number",
)
}
strength := strongPasswords.PasswordStrength(input, nil)
if strength.Score < minPasswordScore {
return errors.New(
"password is too easy to guess, try a stronger password",
)
}
return nil
}
func validateDirectoryPath(input string) error {
if len(input) == 0 {
return errors.New("directory path must not be empty")
}
return nil
}
func formatPromptError(err error) error {
switch err {
case promptui.ErrAbort:
return errors.New("wallet creation aborted, closing")
case promptui.ErrInterrupt:
return errors.New("keyboard interrupt, closing")
case promptui.ErrEOF:
return errors.New("no input received, closing")
default:
return err
}
}

View File

@@ -0,0 +1,44 @@
package v2
import "testing"
func Test_validatePasswordInput(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "no numbers nor special characters",
input: "abcdefghijklmnopqrs",
wantErr: true,
},
{
name: "number and letters but no special characters",
input: "abcdefghijklmnopqrs2020",
wantErr: true,
},
{
name: "numbers, letters, special characters, but too short",
input: "abc2$",
wantErr: true,
},
{
name: "proper length and strong password",
input: "%Str0ngpassword32kjAjsd22020$%",
wantErr: false,
},
{
name: "password format correct but weak entropy score",
input: "aaaaaaa1$",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validatePasswordInput(tt.input); (err != nil) != tt.wantErr {
t.Errorf("validatePasswordInput() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -0,0 +1,140 @@
package v2
import (
"context"
"fmt"
"io"
"os"
"path"
"github.com/pkg/errors"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
)
const (
keymanagerConfigFileName = "keymanageropts.json"
walletDefaultDirName = ".prysm-wallet-v2"
passwordsDefaultDirName = ".passwords"
)
// WalletConfig for a wallet struct, containing important information
// such as the passwords directory, the wallet's directory, and keymanager.
type WalletConfig struct {
PasswordsDir string
WalletDir string
KeymanagerKind v2keymanager.Kind
}
// Wallet is a primitive in Prysm's v2 account management which
// has the capability of creating new accounts, reading existing accounts,
// and providing secure access to eth2 secrets depending on an
// associated keymanager (either direct, derived, or remote signing enabled).
type Wallet struct {
accountsPath string
passwordsDir string
keymanagerKind v2keymanager.Kind
}
// CreateWallet given a set of configuration options, will leverage
// a keymanager to create and write a new wallet to disk for a Prysm validator.
func CreateWallet(ctx context.Context, cfg *WalletConfig) (*Wallet, error) {
if cfg.WalletDir == "" || cfg.PasswordsDir == "" {
return nil, errors.New("wallet dir and passwords dir cannot be nil")
}
accountsPath := path.Join(cfg.WalletDir, cfg.KeymanagerKind.String())
if err := os.MkdirAll(accountsPath, os.ModePerm); err != nil {
return nil, errors.Wrap(err, "could not create wallet directory")
}
if err := os.MkdirAll(cfg.PasswordsDir, os.ModePerm); err != nil {
return nil, errors.Wrap(err, "could not create passwords directory")
}
w := &Wallet{
accountsPath: accountsPath,
passwordsDir: cfg.PasswordsDir,
keymanagerKind: cfg.KeymanagerKind,
}
return w, nil
}
// OpenWallet instantiates a wallet from a specified path.
func OpenWallet(ctx context.Context, cfg *WalletConfig) (*Wallet, error) {
walletPath := path.Join(cfg.WalletDir, cfg.KeymanagerKind.String())
return &Wallet{
accountsPath: walletPath,
passwordsDir: cfg.PasswordsDir,
keymanagerKind: cfg.KeymanagerKind,
}, nil
}
// ReadKeymanagerConfigFromDisk opens a keymanager config file
// for reading if it exists at the wallet path.
func (w *Wallet) ReadKeymanagerConfigFromDisk(ctx context.Context) (io.ReadCloser, error) {
if !fileExists(path.Join(w.accountsPath, keymanagerConfigFileName)) {
return nil, fmt.Errorf("no keymanager config file found at path: %s", w.accountsPath)
}
configFilePath := path.Join(w.accountsPath, keymanagerConfigFileName)
return os.Open(configFilePath)
}
// KeymanagerKind used by the wallet.
func (w *Wallet) KeymanagerKind() v2keymanager.Kind {
return w.keymanagerKind
}
// AccountsPath for the wallet.
func (w *Wallet) AccountsPath() string {
return w.accountsPath
}
// AccountPasswordsPath for the wallet's accounts.
func (w *Wallet) AccountPasswordsPath() string {
return w.passwordsDir
}
// WriteAccountToDisk writes an encoded account by its filename
// within the wallet's directory.
func (w *Wallet) WriteAccountToDisk(ctx context.Context, filename string, encoded []byte) error {
return errors.New("unimplemented")
}
// WriteKeymanagerConfigToDisk takes an encoded keymanager config file
// and writes it to the wallet path.
func (w *Wallet) WriteKeymanagerConfigToDisk(ctx context.Context, encoded []byte) error {
configFilePath := path.Join(w.accountsPath, keymanagerConfigFileName)
if fileExists(configFilePath) {
return nil
}
// Open the keymanager config file for writing.
f, err := os.Create(configFilePath)
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("Could not close keymanager opts file: %v", err)
}
}()
n, err := f.Write(encoded)
if err != nil {
return err
}
if n != len(encoded) {
return fmt.Errorf(
"expected to write %d bytes to disk, but wrote %d",
len(encoded),
n,
)
}
log.WithField("configFile", configFilePath).Debug("Wrote keymanager config file to disk")
return nil
}
// Returns true if a file is not a directory and exists
// at the specified path.
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}

View File

@@ -0,0 +1,106 @@
package v2
import (
"context"
"crypto/rand"
"fmt"
"io/ioutil"
"math/big"
"os"
"path"
"testing"
"github.com/prysmaticlabs/prysm/shared/testutil"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct"
"github.com/sirupsen/logrus"
)
func init() {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetOutput(ioutil.Discard)
}
var _ = direct.Wallet(&Wallet{})
type mockKeymanager struct {
configFileContents []byte
}
func (m *mockKeymanager) CreateAccount(ctx context.Context, password string) error {
return nil
}
func (m *mockKeymanager) MarshalConfigFile(ctx context.Context) ([]byte, error) {
return m.configFileContents, nil
}
func setupWalletDir(t testing.TB) (string, string) {
randPath, err := rand.Int(rand.Reader, big.NewInt(1000000))
if err != nil {
t.Fatalf("Could not generate random file path: %v", err)
}
walletDir := path.Join(testutil.TempDir(), fmt.Sprintf("/%d", randPath))
if err := os.RemoveAll(walletDir); err != nil {
t.Fatalf("Failed to remove directory: %v", err)
}
passwordsDir := path.Join(testutil.TempDir(), fmt.Sprintf("/%d", randPath))
if err := os.RemoveAll(passwordsDir); err != nil {
t.Fatalf("Failed to remove directory: %v", err)
}
t.Cleanup(func() {
if err := os.RemoveAll(walletDir); err != nil {
t.Fatalf("Failed to remove directory: %v", err)
}
if err := os.RemoveAll(passwordsDir); err != nil {
t.Fatalf("Failed to remove directory: %v", err)
}
})
return walletDir, passwordsDir
}
func TestCreateAndReadWallet(t *testing.T) {
ctx := context.Background()
if _, err := CreateWallet(ctx, &WalletConfig{
PasswordsDir: "",
WalletDir: "",
}); err == nil {
t.Error("Expected error when passing in empty directories, received nil")
}
walletDir, passwordsDir := setupWalletDir(t)
keymanagerKind := v2keymanager.Direct
wallet, err := CreateWallet(ctx, &WalletConfig{
PasswordsDir: passwordsDir,
WalletDir: walletDir,
KeymanagerKind: keymanagerKind,
})
if err != nil {
t.Fatal(err)
}
keymanager := &mockKeymanager{
configFileContents: []byte("hello-world"),
}
keymanagerConfig, err := keymanager.MarshalConfigFile(ctx)
if err != nil {
t.Fatalf("Could not marshal keymanager config file: %v", err)
}
if err := wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil {
t.Fatalf("Could not write keymanager config file to disk: %v", err)
}
walletPath := path.Join(walletDir, keymanagerKind.String())
configFilePath := path.Join(walletPath, keymanagerConfigFileName)
if !fileExists(configFilePath) {
t.Fatalf("Expected config file to have been created at path: %s", configFilePath)
}
// We should be able to now read the wallet as well.
if _, err := CreateWallet(ctx, &WalletConfig{
PasswordsDir: passwordsDir,
WalletDir: walletDir,
}); err != nil {
t.Fatal(err)
}
}

View File

@@ -3,6 +3,11 @@
package flags
import (
"os"
"os/user"
"path/filepath"
"runtime"
"github.com/urfave/cli/v2"
)
@@ -108,4 +113,45 @@ var (
Usage: "Filepath to a JSON file of unencrypted validator keys for easier launching of the validator client",
Value: "",
}
// WalletDirFlag defines the path to a wallet directory for Prysm accounts-v2.
WalletDirFlag = &cli.StringFlag{
Name: "wallet-dir",
Usage: "Path to a wallet directory on-disk for Prysm validator accounts",
Value: DefaultValidatorDir(),
}
// WalletPasswordsDirFlag defines the path for a passwords directory for
// Prysm accounts-v2.
WalletPasswordsDirFlag = &cli.StringFlag{
Name: "passwords-dir",
Usage: "Path to a directory on-disk where wallet passwords are stored",
Value: DefaultValidatorDir(),
}
)
// DefaultValidatorDir returns OS-specific default validator directory.
func DefaultValidatorDir() string {
// Try to place the data folder in the user's home dir
home := homeDir()
if home != "" {
if runtime.GOOS == "darwin" {
return filepath.Join(home, "Library", "Eth2Validators")
} else if runtime.GOOS == "windows" {
return filepath.Join(home, "AppData", "Roaming", "Eth2Validators")
} else {
return filepath.Join(home, ".eth2validators")
}
}
// As we cannot guess a stable location, return empty and handle later
return ""
}
// homeDir returns home directory path.
func homeDir() string {
if home := os.Getenv("HOME"); home != "" {
return home
}
if usr, err := user.Current(); err == nil {
return usr.HomeDir
}
return ""
}

View File

@@ -0,0 +1,20 @@
load("@io_bazel_rules_go//go:def.bzl", "go_test")
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["types.go"],
importpath = "github.com/prysmaticlabs/prysm/validator/keymanager/v2",
visibility = [
"//validator:__pkg__",
"//validator:__subpackages__",
],
deps = ["//shared/bls:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["types_test.go"],
embed = [":go_default_library"],
deps = ["//validator/keymanager/v2/direct:go_default_library"],
)

View File

@@ -0,0 +1,15 @@
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["direct.go"],
importpath = "github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct",
visibility = [
"//validator:__pkg__",
"//validator:__subpackages__",
],
deps = [
"//shared/bls:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],
)

View File

@@ -0,0 +1,74 @@
package direct
import (
"context"
"errors"
"io"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/sirupsen/logrus"
)
var log = logrus.WithField("prefix", "keymanager-v2")
// Wallet defines a struct which has capabilities and knowledge of how
// to read and write important accounts-related files to the filesystem.
// Useful for keymanager to have persistent capabilities for accounts on-disk.
type Wallet interface {
AccountsPath() string
AccountPasswordsPath() string
WriteAccountToDisk(ctx context.Context, filename string, encoded []byte) error
WriteKeymanagerConfigToDisk(ctx context.Context, encoded []byte) error
ReadKeymanagerConfigFromDisk(ctx context.Context) (io.ReadCloser, error)
}
// Config for a direct keymanager.
type Config struct{}
// Keymanager implementation for direct keystores.
type Keymanager struct {
wallet Wallet
}
// DefaultConfig for a direct keymanager implementation.
func DefaultConfig() *Config {
return &Config{}
}
// NewKeymanager instantiates a new direct keymanager from configuration options.
func NewKeymanager(ctx context.Context, wallet Wallet, cfg *Config) *Keymanager {
return &Keymanager{
wallet: wallet,
}
}
// NewKeymanagerFromConfigFile instantiates a direct keymanager instance
// from a configuration file accesed via a wallet.
// TODO(#6220): Implement.
func NewKeymanagerFromConfigFile(ctx context.Context, wallet Wallet) (*Keymanager, error) {
return &Keymanager{
wallet: wallet,
}, nil
}
// CreateAccount for a direct keymanager implementation.
// TODO(#6220): Implement.
func (dr *Keymanager) CreateAccount(ctx context.Context, password string) error {
return errors.New("unimplemented")
}
// MarshalConfigFile returns a marshaled configuration file for a direct keymanager.
// TODO(#6220): Implement.
func (dr *Keymanager) MarshalConfigFile(ctx context.Context) ([]byte, error) {
return nil, nil
}
// FetchValidatingPublicKeys fetches the list of public keys from the direct account keystores.
func (dr *Keymanager) FetchValidatingPublicKeys() ([][48]byte, error) {
return nil, errors.New("unimplemented")
}
// Sign signs a message using a validator key.
func (dr *Keymanager) Sign(context.Context, interface{}) (bls.Signature, error) {
return nil, errors.New("unimplemented")
}

View File

@@ -0,0 +1,47 @@
package v2
import (
"context"
"fmt"
"github.com/prysmaticlabs/prysm/shared/bls"
)
// IKeymanager defines a general keymanager-v2 interface for Prysm wallets.
type IKeymanager interface {
// CreateAccount based on the keymanager's logic.
CreateAccount(ctx context.Context, password string) error
// MarshalConfigFile for the keymanager's options.
MarshalConfigFile(ctx context.Context) ([]byte, error)
// FetchValidatingKeys fetches the list of public keys that should be used to validate with.
FetchValidatingPublicKeys() ([][48]byte, error)
// Sign signs a message using a validator key.
Sign(context.Context, interface{}) (bls.Signature, error)
}
// Kind defines an enum for either direct, derived, or remote-signing
// keystores for Prysm wallets.
type Kind int
const (
// Direct keymanager defines an on-disk, encrypted keystore-capable store.
Direct Kind = iota
// Derived keymanager using a hierarchical-deterministic algorithm.
Derived
// Remote keymanager capable of remote-signing data.
Remote
)
// String marshals a keymanager kind to a string value.
func (k Kind) String() string {
switch k {
case Direct:
return "direct"
case Derived:
return "derived"
case Remote:
return "remote"
default:
return fmt.Sprintf("%d", int(k))
}
}

View File

@@ -0,0 +1,5 @@
package v2
import "github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct"
var _ = IKeymanager(&direct.Keymanager{})

View File

@@ -22,6 +22,7 @@ import (
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/version"
v1 "github.com/prysmaticlabs/prysm/validator/accounts/v1"
v2 "github.com/prysmaticlabs/prysm/validator/accounts/v2"
"github.com/prysmaticlabs/prysm/validator/client/streaming"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/prysmaticlabs/prysm/validator/node"
@@ -101,6 +102,7 @@ func main() {
app.Version = version.GetVersion()
app.Action = startNode
app.Commands = []*cli.Command{
v2.Commands,
{
Name: "accounts",
Category: "accounts",