2020-06-27 15:48:36 +00:00
|
|
|
package v1
|
2020-02-03 17:13:58 +00:00
|
|
|
|
|
|
|
import (
|
2020-07-29 23:54:23 +00:00
|
|
|
"context"
|
2020-02-03 17:13:58 +00:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-05-18 22:25:36 +00:00
|
|
|
"os"
|
2020-02-03 17:13:58 +00:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/prysmaticlabs/prysm/shared/bls"
|
|
|
|
"github.com/prysmaticlabs/prysm/shared/bytesutil"
|
|
|
|
e2wallet "github.com/wealdtech/go-eth2-wallet"
|
2020-02-07 05:03:33 +00:00
|
|
|
filesystem "github.com/wealdtech/go-eth2-wallet-store-filesystem"
|
2020-04-16 21:56:13 +00:00
|
|
|
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
2020-02-03 17:13:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type walletOpts struct {
|
2020-02-07 05:03:33 +00:00
|
|
|
Location string `json:"location"`
|
2020-02-03 17:13:58 +00:00
|
|
|
Accounts []string `json:"accounts"`
|
|
|
|
Passphrases []string `json:"passphrases"`
|
|
|
|
}
|
|
|
|
|
|
|
|
var walletOptsHelp = `The wallet key manager stores keys in a local encrypted store. The options are:
|
2020-02-07 05:03:33 +00:00
|
|
|
- location This is the location to look for wallets. If not supplied it will
|
|
|
|
use the standard (operating system-dependent) path.
|
2020-02-03 17:13:58 +00:00
|
|
|
- accounts This is a list of account specifiers. An account specifier is of
|
|
|
|
the form <wallet name>/[account name], where the account name can be a
|
2020-02-07 05:03:33 +00:00
|
|
|
regular expression. If the account specifier is just <wallet name> all
|
|
|
|
accounts in that wallet will be used. Multiple account specifiers can be
|
|
|
|
supplied if required.
|
2020-02-03 17:13:58 +00:00
|
|
|
- passphrase This is the passphrase used to encrypt the accounts when they
|
|
|
|
were created. Multiple passphrases can be supplied if required.
|
2020-02-07 05:03:33 +00:00
|
|
|
|
|
|
|
An sample keymanager options file (with annotations; these should be removed if
|
|
|
|
using this as a template) is:
|
|
|
|
|
2020-02-03 17:13:58 +00:00
|
|
|
{
|
2020-02-07 05:03:33 +00:00
|
|
|
"location": "/wallets", // Look for wallets in the directory '/wallets'
|
2020-02-03 17:13:58 +00:00
|
|
|
"accounts": ["Validators/Account.*"], // Use all accounts in the 'Validators' wallet starting with 'Account'
|
|
|
|
"passphrases": ["secret1","secret2"] // Use the passphrases 'secret1' and 'secret2' to decrypt accounts
|
|
|
|
}`
|
|
|
|
|
|
|
|
// NewWallet creates a key manager populated with the keys from a wallet at the given path.
|
|
|
|
func NewWallet(input string) (KeyManager, string, error) {
|
|
|
|
opts := &walletOpts{}
|
|
|
|
err := json.Unmarshal([]byte(input), opts)
|
|
|
|
if err != nil {
|
|
|
|
return nil, walletOptsHelp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(opts.Accounts) == 0 {
|
|
|
|
return nil, walletOptsHelp, errors.New("at least one account specifier is required")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(opts.Passphrases) == 0 {
|
|
|
|
return nil, walletOptsHelp, errors.New("at least one passphrase is required to decrypt accounts")
|
|
|
|
}
|
2020-05-18 22:25:36 +00:00
|
|
|
for i, passphrase := range opts.Passphrases {
|
|
|
|
if strings.HasPrefix(passphrase, "$") {
|
|
|
|
envPassphrase := os.Getenv(strings.TrimPrefix(passphrase, "$"))
|
|
|
|
if envPassphrase != "" {
|
|
|
|
// N.B. We do not log here if the environment variable is not found, as it is possible that this is actually a
|
|
|
|
// passphrase that just happens to begin with the '$' character. If this is a missing environment variable it will
|
|
|
|
// be noticed when the account fails to decrypt.
|
|
|
|
opts.Passphrases[i] = envPassphrase
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-02-03 17:13:58 +00:00
|
|
|
|
|
|
|
km := &Wallet{
|
|
|
|
accounts: make(map[[48]byte]e2wtypes.Account),
|
|
|
|
}
|
|
|
|
|
2020-03-04 18:34:23 +00:00
|
|
|
if strings.Contains(opts.Location, "$") || strings.Contains(opts.Location, "~") || strings.Contains(opts.Location, "%") {
|
|
|
|
log.WithField("path", opts.Location).Warn("Keystore path contains unexpanded shell expansion characters")
|
|
|
|
}
|
2020-02-07 05:03:33 +00:00
|
|
|
var store e2wtypes.Store
|
|
|
|
if opts.Location == "" {
|
|
|
|
store = filesystem.New()
|
|
|
|
} else {
|
|
|
|
store = filesystem.New(filesystem.WithLocation(opts.Location))
|
|
|
|
}
|
2020-07-29 23:54:23 +00:00
|
|
|
ctx := context.Background()
|
2020-02-03 17:13:58 +00:00
|
|
|
for _, path := range opts.Accounts {
|
|
|
|
parts := strings.Split(path, "/")
|
|
|
|
if len(parts[0]) == 0 {
|
|
|
|
return nil, walletOptsHelp, fmt.Errorf("did not understand account specifier %q", path)
|
|
|
|
}
|
2020-02-07 05:03:33 +00:00
|
|
|
wallet, err := e2wallet.OpenWallet(parts[0], e2wallet.WithStore(store))
|
2020-02-03 17:13:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, walletOptsHelp, err
|
|
|
|
}
|
|
|
|
accountSpecifier := "^.*$"
|
|
|
|
if len(parts) > 1 && len(parts[1]) > 0 {
|
|
|
|
accountSpecifier = fmt.Sprintf("^%s$", parts[1])
|
|
|
|
}
|
|
|
|
re := regexp.MustCompile(accountSpecifier)
|
2020-07-29 23:54:23 +00:00
|
|
|
for account := range wallet.Accounts(ctx) {
|
2020-03-13 12:26:10 +00:00
|
|
|
log := log.WithField("account", fmt.Sprintf("%s/%s", wallet.Name(), account.Name()))
|
2020-02-03 17:13:58 +00:00
|
|
|
if re.Match([]byte(account.Name())) {
|
|
|
|
pubKey := bytesutil.ToBytes48(account.PublicKey().Marshal())
|
2020-03-13 12:26:10 +00:00
|
|
|
unlocked := false
|
2020-02-03 17:13:58 +00:00
|
|
|
for _, passphrase := range opts.Passphrases {
|
2020-07-29 23:54:23 +00:00
|
|
|
locker, ok := account.(e2wtypes.AccountLocker)
|
|
|
|
if !ok {
|
|
|
|
log.WithError(err).Trace("Account does not implement the AccountLocker interface")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err := locker.Unlock(ctx, []byte(passphrase)); err != nil {
|
2020-03-13 12:26:10 +00:00
|
|
|
log.WithError(err).Trace("Failed to unlock account with one of the supplied passphrases")
|
2020-02-03 17:13:58 +00:00
|
|
|
} else {
|
|
|
|
km.accounts[pubKey] = account
|
2020-03-13 12:26:10 +00:00
|
|
|
unlocked = true
|
|
|
|
break
|
2020-02-03 17:13:58 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-13 12:26:10 +00:00
|
|
|
if !unlocked {
|
|
|
|
log.Warn("Failed to unlock account with any supplied passphrase; cannot validate with this key")
|
|
|
|
}
|
2020-02-03 17:13:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return km, walletOptsHelp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wallet is a key manager that loads keys from a local Ethereum 2 wallet.
|
|
|
|
type Wallet struct {
|
|
|
|
accounts map[[48]byte]e2wtypes.Account
|
|
|
|
}
|
|
|
|
|
|
|
|
// FetchValidatingKeys fetches the list of public keys that should be used to validate with.
|
|
|
|
func (km *Wallet) FetchValidatingKeys() ([][48]byte, error) {
|
|
|
|
res := make([][48]byte, 0, len(km.accounts))
|
|
|
|
for pubKey := range km.accounts {
|
|
|
|
res = append(res, pubKey)
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sign signs a message for the validator to broadcast.
|
2020-06-25 00:47:51 +00:00
|
|
|
func (km *Wallet) Sign(pubKey [48]byte, root [32]byte) (bls.Signature, error) {
|
2020-02-03 17:13:58 +00:00
|
|
|
account, exists := km.accounts[pubKey]
|
|
|
|
if !exists {
|
|
|
|
return nil, ErrNoSuchKey
|
|
|
|
}
|
2020-04-14 20:27:03 +00:00
|
|
|
// TODO(#4817) Update with new library to remove domain here.
|
2020-07-29 23:54:23 +00:00
|
|
|
signer, ok := account.(e2wtypes.AccountSigner)
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("account does not implement the AccountSigner interface")
|
|
|
|
}
|
|
|
|
sig, err := signer.Sign(context.Background(), root[:])
|
2020-02-03 17:13:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return bls.SignatureFromBytes(sig.Marshal())
|
|
|
|
}
|