Compare commits

...

11 Commits

Author SHA1 Message Date
Jim McDonald
12f3154157 Update release workflow. 2022-12-22 01:26:09 +00:00
Jim McDonald
51689db315 Fix typo. 2022-12-21 23:23:36 +00:00
Jim McDonald
1a8897ff0f Fix typo. 2022-12-21 23:18:04 +00:00
Jim McDonald
5ee92bee78 Tidy up validator credentials set.
Provide more help and methods to generate credentials change operations.
2022-12-21 23:11:43 +00:00
Jim McDonald
eba7a0c88a Tidy workflow. 2022-12-21 18:28:35 +00:00
Jim McDonald
f6a6224968 Update dockerfile. 2022-12-19 22:43:29 +00:00
Jim McDonald
60e8878dc3 Update for altered BLSToExecutionChange API. 2022-12-19 22:42:56 +00:00
Jim McDonald
41d9160b63 Update release workflow. 2022-12-19 22:36:05 +00:00
Jim McDonald
b3d4c9af08 Allow S3 credentials. 2022-12-19 22:35:46 +00:00
Jim McDonald
5a8a13d8f3 Merge pull request #50 from Myu-Unix/master
Update root.go
2022-12-13 22:03:41 +00:00
Myu-Unix
d94e1551a7 Update root.go
Fix typo (RET vs REST)
2022-12-13 22:04:42 +01:00
12 changed files with 357 additions and 171 deletions

View File

@@ -59,7 +59,7 @@ jobs:
- name: Compile
run: |
go build -tags osusergo,netgo -ldflags="-extldflags=-static" -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }}"
go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz ${{ needs.env_vars.outputs.binary }}
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-amd64.tar.gz.sha256
- name: Upload binary
@@ -87,7 +87,7 @@ jobs:
sudo apt-get update
sudo apt-get upgrade
sudo apt install -y gcc-aarch64-linux-gnu libstdc++-11-pic-arm64-cross
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -tags osusergo,netgo -ldflags="-extldflags=-static" -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }}"
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz ${{ needs.env_vars.outputs.binary }}
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
@@ -132,7 +132,7 @@ jobs:
- name: Compile
run: |
go build -tags osusergo,netgo -ldflags="-extldflags=-static" -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }}"
go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }}"
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz ${{ needs.env_vars.outputs.binary }}
brew install coreutils
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-darwin-amd64.tar.gz.sha256
@@ -177,7 +177,7 @@ jobs:
- name: Compile
run: |
go build -tags osusergo,netgo -ldflags="-extldflags=-static" -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }}"
go build -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
choco install zip
zip --junk-paths ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip ${{ needs.env_vars.outputs.binary }}.exe
$FileHash=(certutil -hashfile ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-windows-exe.zip SHA256 | findstr /v hash | findstr /v SHA).replace(" ", "")

View File

@@ -1,3 +1,7 @@
dev:
- use new build system
- support S3 credentials
1.26.5:
- provide validator information in "chain status" verbose output

View File

@@ -1,4 +1,4 @@
FROM golang:1.17-bullseye as builder
FROM golang:1.18-bullseye as builder
WORKDIR /app

View File

@@ -244,7 +244,7 @@ func init() {
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("connection", "", "URL to an Ethereum 2 node's RET API endpoint")
RootCmd.PersistentFlags().String("connection", "", "URL to an Ethereum 2 node's REST API endpoint")
if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil {
panic(err)
}

View File

@@ -35,6 +35,7 @@ type command struct {
// Input.
account string
withdrawalAccount string
passphrases []string
mnemonic string
path string
@@ -62,7 +63,7 @@ type command struct {
signedOperations []*capella.SignedBLSToExecutionChange
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
@@ -74,6 +75,7 @@ func newCommand(ctx context.Context) (*command, error) {
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
prepareOffline: viper.GetBool("prepare-offline"),
account: viper.GetString("account"),
withdrawalAccount: viper.GetString("withdrawal-account"),
passphrases: util.GetPassphrases(),
mnemonic: viper.GetString("mnemonic"),
path: viper.GetString("path"),
@@ -96,8 +98,8 @@ func newCommand(ctx context.Context) (*command, error) {
return c, nil
}
if c.account != "" && len(c.passphrases) == 0 {
return nil, errors.New("passphrase required with account")
if c.withdrawalAccount != "" && len(c.passphrases) == 0 {
return nil, errors.New("passphrase required with withdrawal-account")
}
return c, nil

View File

@@ -22,7 +22,8 @@ import (
"github.com/pkg/errors"
)
func (c *command) output(ctx context.Context) (string, error) {
//nolint:unparam
func (c *command) output(_ context.Context) (string, error) {
if c.quiet {
return "", nil
}

View File

@@ -43,24 +43,6 @@ var offlinePreparationFilename = "offline-preparation.json"
var changeOperationsFilename = "change-operations.json"
func (c *command) process(ctx context.Context) error {
// We should have exactly 1 specifier to know what we're working with.
validatorSpecifiers := 0
if c.validator != "" {
validatorSpecifiers++
}
if c.mnemonic != "" {
validatorSpecifiers++
}
if c.privateKey != "" {
validatorSpecifiers++
}
if validatorSpecifiers == 0 {
return errors.New("one of validator, mmenomic or private key should be specified")
}
if validatorSpecifiers > 1 {
return errors.New("only one of validator, mmenomic or private key should be specified")
}
if err := c.setup(ctx); err != nil {
return err
}
@@ -82,6 +64,9 @@ func (c *command) process(ctx context.Context) error {
}
if c.json || c.offline {
if c.debug {
fmt.Fprintf(os.Stderr, "Not broadcasting credentials change operations\n")
}
// Want JSON output, or cannot broadcast.
return nil
}
@@ -118,7 +103,7 @@ func (c *command) obtainRequiredInformation(ctx context.Context) error {
// populateChainInfo populates chain info structure from a beacon node.
func (c *command) populateChainInfo(ctx context.Context) error {
if c.debug {
fmt.Printf("Populating chain info from beacon node\n")
fmt.Fprintf(os.Stderr, "Populating chain info from beacon node\n")
}
// Obtain validators.
@@ -155,7 +140,7 @@ func (c *command) populateChainInfo(ctx context.Context) error {
c.chainInfo.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
}
if c.debug {
fmt.Printf("Genesis validators root is %#x\n", c.chainInfo.GenesisValidatorsRoot)
fmt.Fprintf(os.Stderr, "Genesis validators root is %#x\n", c.chainInfo.GenesisValidatorsRoot)
}
// Obtain epoch.
@@ -163,49 +148,16 @@ func (c *command) populateChainInfo(ctx context.Context) error {
// Obtain fork version.
if c.forkVersion != "" {
// Fork version supplied manually.
forkVersion, err := hex.DecodeString(strings.TrimPrefix(c.forkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "invalid fork version supplied")
if err := c.populateChainInfoForkVersionFromInput(ctx); err != nil {
return err
}
if len(forkVersion) != phase0.ForkVersionLength {
return errors.New("invalid length for fork version")
}
copy(c.chainInfo.ForkVersion[:], forkVersion)
} else {
// Fork version obtained from beacon node.
forkSchedule, err := c.consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain fork schedule")
}
if len(forkSchedule) == 0 {
return errors.New("beacon node did not provide any fork schedule; provide manually with --fork-version")
}
if c.debug {
fmt.Printf("Genesis fork version is %#x\n", forkSchedule[0].CurrentVersion)
}
if len(forkSchedule) < 4 {
return errors.New("beacon node not providing capella fork schedule; provide manually with --fork-version")
}
for i := range forkSchedule {
// Need to be at least fork 3 (i.e. capella)
if i < 3 {
continue
}
if i == 3 {
// Force use of capella even if we aren't there yet, to allow credential
// change operations to be signed in advance with a signature that will be
// valid once capella goes live.
c.chainInfo.ForkVersion = forkSchedule[i].CurrentVersion
continue
}
if forkSchedule[i].Epoch <= c.chainInfo.Epoch {
c.chainInfo.ForkVersion = forkSchedule[i].CurrentVersion
}
if err := c.populateChainInfoForkVersionFromChain(ctx); err != nil {
return err
}
}
if c.debug {
fmt.Printf("Fork version is %#x\n", c.chainInfo.ForkVersion)
fmt.Fprintf(os.Stderr, "Fork version is %#x\n", c.chainInfo.ForkVersion)
}
// Calculate domain.
@@ -217,13 +169,71 @@ func (c *command) populateChainInfo(ctx context.Context) error {
if !exists {
return errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
}
domainProvider, isProvider := c.consensusClient.(consensusclient.DomainProvider)
if !isProvider {
return errors.New("consensus node does not provide domain information")
if c.debug {
fmt.Fprintf(os.Stderr, "Domain type is %#x\n", domainType)
}
c.chainInfo.Domain, err = domainProvider.Domain(ctx, domainType, c.chainInfo.Epoch)
copy(c.chainInfo.Domain[:], domainType[:])
root, err := (&phase0.ForkData{
CurrentVersion: c.chainInfo.ForkVersion,
GenesisValidatorsRoot: c.chainInfo.GenesisValidatorsRoot,
}).HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to obtain domain")
return errors.Wrap(err, "failed to calculate signature domain")
}
copy(c.chainInfo.Domain[4:], root[:])
if c.debug {
fmt.Fprintf(os.Stderr, "Domain is %#x\n", c.chainInfo.Domain)
}
return nil
}
func (c *command) populateChainInfoForkVersionFromInput(_ context.Context) error {
// Fork version supplied manually.
forkVersion, err := hex.DecodeString(strings.TrimPrefix(c.forkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "invalid fork version supplied")
}
if len(forkVersion) != phase0.ForkVersionLength {
return errors.New("invalid length for fork version")
}
copy(c.chainInfo.ForkVersion[:], forkVersion)
return nil
}
func (c *command) populateChainInfoForkVersionFromChain(ctx context.Context) error {
// Fetch the capella fork version from the specification.
spec, err := c.consensusClient.(consensusclient.SpecProvider).Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["CAPELLA_FORK_VERSION"]
if !exists {
return errors.New("capella fork version not known by chain")
}
capellaForkVersion, isForkVersion := tmp.(phase0.Version)
if !isForkVersion {
//nolint:revive
return errors.New("CAPELLA_FORK_VERSION is not a fork version!")
}
c.chainInfo.ForkVersion = capellaForkVersion
// Work through the fork schedule to find the latest current fork post-Capella.
forkSchedule, err := c.consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain fork schedule")
}
foundCapella := false
for i := range forkSchedule {
if foundCapella && forkSchedule[i].Epoch <= c.chainInfo.Epoch {
c.chainInfo.ForkVersion = forkSchedule[i].CurrentVersion
}
if bytes.Equal(forkSchedule[i].CurrentVersion[:], capellaForkVersion[:]) {
foundCapella = true
}
}
return nil
@@ -231,7 +241,7 @@ func (c *command) populateChainInfo(ctx context.Context) error {
// dumpRequiredInformation prepares for an offline run of this command by dumping
// the chain information to a file.
func (c *command) dumpRequiredInformation(ctx context.Context) error {
func (c *command) dumpRequiredInformation(_ context.Context) error {
data, err := json.Marshal(c.chainInfo)
if err != nil {
return err
@@ -244,7 +254,7 @@ func (c *command) dumpRequiredInformation(ctx context.Context) error {
}
func (c *command) generateOperations(ctx context.Context) error {
if c.account == "" && c.mnemonic == "" && c.privateKey == "" {
if c.account == "" && c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
// No input information; fetch the operations from a file.
if err := c.loadOperations(ctx); err == nil {
return nil
@@ -252,47 +262,53 @@ func (c *command) generateOperations(ctx context.Context) error {
return fmt.Errorf("no account, mnemonic or private key specified and no %s file found; cannot proceed", changeOperationsFilename)
}
if c.mnemonic != "" && c.path == "" {
// Have a mnemonic and no path; scan mnemonic.
return c.generateOperationsFromMnemonic(ctx)
if c.mnemonic != "" {
switch {
case c.path != "":
// Have a mnemonic and path.
return c.generateOperationsFromMnemonicAndPath(ctx)
case c.validator != "":
// Have a mnemonic and validator.
return c.generateOperationsFromMnemonicAndValidator(ctx)
case c.privateKey != "":
// Have a mnemonic and a private key for the withdrawal address.
return c.generateOperationsFromMnemonicAndPrivateKey(ctx)
default:
// Have a mnemonic and nothing else; scan.
return c.generateOperationsFromMnemonic(ctx)
}
}
if c.mnemonic != "" && c.path != "" {
// Have a mnemonic and path.
return c.generateOperationsFromMnemonicAndPath(ctx)
if c.account != "" {
switch {
case c.withdrawalAccount != "":
// Have an account and a withdrawal account.
return c.generateOperationsFromAccountAndWithdrawalAccount(ctx)
case c.privateKey != "":
// Have an account and a private key for the withdrawal address.
return c.generateOperationsFromAccountAndPrivateKey(ctx)
}
}
// Have a validator index or public key ; fetch the validator info.
validatorInfo, err := c.fetchValidatorInfo(ctx)
if err != nil {
return err
if c.validator != "" && c.privateKey != "" {
// Have a validator and a private key for the withdrawal address.
return c.generateOperationsFromValidatorAndPrivateKey(ctx)
}
// Fetch the individual account.
withdrawalAccount, err := c.fetchAccount(ctx)
if err != nil {
return err
}
// Generate the operation.
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
return errors.New("unsupported combination of inputs; see help for details of supported combinations")
}
func (c *command) loadChainInfo(ctx context.Context) error {
func (c *command) loadChainInfo(_ context.Context) error {
_, err := os.Stat(offlinePreparationFilename)
if err != nil {
if c.debug {
fmt.Printf("Failed to read offline preparation file: %v\n", err)
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
}
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
}
if c.debug {
fmt.Printf("%s found; loading chain state\n", offlinePreparationFilename)
fmt.Fprintf(os.Stderr, "%s found; loading chain state\n", offlinePreparationFilename)
}
data, err := os.ReadFile(offlinePreparationFilename)
if err != nil {
@@ -305,17 +321,17 @@ func (c *command) loadChainInfo(ctx context.Context) error {
return nil
}
func (c *command) loadOperations(ctx context.Context) error {
func (c *command) loadOperations(_ context.Context) error {
_, err := os.Stat(changeOperationsFilename)
if err != nil {
if c.debug {
fmt.Printf("Failed to read change operations file: %v\n", err)
fmt.Fprintf(os.Stderr, "Failed to read change operations file: %v\n", err)
}
return err
}
if c.debug {
fmt.Printf("%s found; loading operations\n", changeOperationsFilename)
fmt.Fprintf(os.Stderr, "%s found; loading operations\n", changeOperationsFilename)
}
data, err := os.ReadFile(changeOperationsFilename)
if err != nil {
@@ -346,7 +362,7 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
for i := 0; ; i++ {
if i-lastFoundIndex > maxDistance {
if c.debug {
fmt.Printf("Gone %d indices without finding a validator, not scanning any further\n", maxDistance)
fmt.Fprintf(os.Stderr, "Gone %d indices without finding a validator, not scanning any further\n", maxDistance)
}
break
}
@@ -363,6 +379,68 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
return nil
}
func (c *command) generateOperationsFromMnemonicAndPrivateKey(ctx context.Context) error {
// Functionally identical to a simple scan, so use that.
return c.generateOperationsFromMnemonic(ctx)
}
func (c *command) generateOperationsFromMnemonicAndValidator(ctx context.Context) error {
seed, err := util.SeedFromMnemonic(c.mnemonic)
if err != nil {
return err
}
validator, err := c.fetchValidatorInfo(ctx)
if err != nil {
return err
}
// Scan the keys from the seed to find the path.
maxDistance := 1024
// Start scanning the validator keys.
for i := 0; ; i++ {
if i == maxDistance {
if c.debug {
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
}
break
}
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
if err != nil {
return errors.Wrap(err, "failed to generate validator private key")
}
validatorPubkey := validatorPrivkey.PublicKey().Marshal()
if bytes.Equal(validatorPubkey, validator.Pubkey[:]) {
// Recreate the withdrawal credentials to ensure a match.
withdrawalKeyPath := strings.TrimSuffix(validatorKeyPath, "/0")
withdrawalPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, withdrawalKeyPath)
if err != nil {
return errors.Wrap(err, "failed to generate withdrawal private key")
}
withdrawalPubkey := withdrawalPrivkey.PublicKey()
withdrawalCredentials := ethutil.SHA256(withdrawalPubkey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
if !bytes.Equal(withdrawalCredentials, validator.WithdrawalCredentials) {
return fmt.Errorf("validator %#x withdrawal credentials %#x do not match expected credentials, cannot update", validatorPubkey, validator.WithdrawalCredentials)
}
withdrawalAccount, err := util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
if err != nil {
return errors.Wrap(err, "failed to create withdrawal account")
}
err = c.generateOperationFromAccount(ctx, validator, withdrawalAccount)
if err != nil {
return err
}
break
}
}
return nil
}
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
validators map[string]*validatorInfo,
seed []byte,
@@ -379,45 +457,56 @@ func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
validator, exists := validators[validatorPubkey]
if !exists {
if c.debug {
fmt.Printf("No validator found with public key %s at path %s\n", validatorPubkey, path)
fmt.Fprintf(os.Stderr, "No validator found with public key %s at path %s\n", validatorPubkey, path)
}
return false, nil
}
if c.verbose {
fmt.Printf("Validator %d found with public key %s at path %s\n", validator.Index, validatorPubkey, path)
fmt.Fprintf(os.Stderr, "Validator %d found with public key %s at path %s\n", validator.Index, validatorPubkey, path)
}
if validator.WithdrawalCredentials[0] != byte(0) {
if c.debug {
fmt.Printf("Validator %s has non-BLS withdrawal credentials %#x\n", validatorPubkey, validator.WithdrawalCredentials)
fmt.Fprintf(os.Stderr, "Validator %s has non-BLS withdrawal credentials %#x\n", validatorPubkey, validator.WithdrawalCredentials)
}
return false, nil
}
// Recreate the withdrawal credentials to ensure a match.
withdrawalKeyPath := strings.TrimSuffix(path, "/0")
withdrawalPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, withdrawalKeyPath)
if err != nil {
return false, errors.Wrap(err, "failed to generate withdrawal private key")
var withdrawalPubkey []byte
var withdrawalAccount e2wtypes.Account
if c.privateKey == "" {
// Recreate the withdrawal credentials to ensure a match.
withdrawalKeyPath := strings.TrimSuffix(path, "/0")
withdrawalPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, withdrawalKeyPath)
if err != nil {
return false, errors.Wrap(err, "failed to generate withdrawal private key")
}
withdrawalPubkey = withdrawalPrivkey.PublicKey().Marshal()
withdrawalAccount, err = util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
if err != nil {
return false, errors.Wrap(err, "failed to create withdrawal account")
}
} else {
// Need the withdrawal credentials from the private key.
withdrawalAccount, err = util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return false, err
}
withdrawalPubkey = withdrawalAccount.PublicKey().Marshal()
}
withdrawalPubkey := withdrawalPrivkey.PublicKey()
withdrawalCredentials := ethutil.SHA256(withdrawalPubkey.Marshal())
withdrawalCredentials := ethutil.SHA256(withdrawalPubkey)
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
if !bytes.Equal(withdrawalCredentials, validator.WithdrawalCredentials) {
if c.verbose {
fmt.Printf("Validator %s withdrawal credentials %#x do not match expected credentials, cannot update\n", validatorPubkey, validator.WithdrawalCredentials)
if c.verbose && c.privateKey == "" {
fmt.Fprintf(os.Stderr, "Validator %s withdrawal credentials %#x do not match expected credentials, cannot update\n", validatorPubkey, validator.WithdrawalCredentials)
}
return false, nil
}
if c.debug {
fmt.Printf("Validator %s eligible for setting credentials\n", validatorPubkey)
}
withdrawalAccount, err := util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
if err != nil {
return false, errors.Wrap(err, "failed to create withdrawal account")
fmt.Fprintf(os.Stderr, "Validator %s eligible for setting credentials\n", validatorPubkey)
}
err = c.generateOperationFromAccount(ctx, validator, withdrawalAccount)
@@ -480,7 +569,7 @@ func (c *command) createSignedOperation(ctx context.Context,
}, nil
}
func (c *command) parseWithdrawalAddress(ctx context.Context) error {
func (c *command) parseWithdrawalAddress(_ context.Context) error {
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(c.withdrawalAddressStr, "0x"))
if err != nil {
return errors.Wrap(err, "failed to obtain execution address")
@@ -513,7 +602,7 @@ func (c *command) validateOperations(ctx context.Context) (bool, string) {
return true, ""
}
func (c *command) validateOperation(ctx context.Context,
func (c *command) validateOperation(_ context.Context,
validators map[phase0.ValidatorIndex]*validatorInfo,
signedOperation *capella.SignedBLSToExecutionChange,
) (
@@ -525,8 +614,8 @@ func (c *command) validateOperation(ctx context.Context,
return false, "validator not known on chain"
}
if c.debug {
fmt.Printf("Credentials change operation: %v", signedOperation)
fmt.Printf("On-chain validator info: %v\n", validator)
fmt.Fprintf(os.Stderr, "Credentials change operation: %v", signedOperation)
fmt.Fprintf(os.Stderr, "On-chain validator info: %v\n", validator)
}
if validator.WithdrawalCredentials[0] != byte(0) {
@@ -537,7 +626,7 @@ func (c *command) validateOperation(ctx context.Context,
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
if !bytes.Equal(withdrawalCredentials, validator.WithdrawalCredentials) {
if c.debug {
fmt.Printf("validator withdrawal credentials %#x do not match calculated operation withdrawal credentials %#x\n", validator.WithdrawalCredentials, withdrawalCredentials)
fmt.Fprintf(os.Stderr, "validator withdrawal credentials %#x do not match calculated operation withdrawal credentials %#x\n", validator.WithdrawalCredentials, withdrawalCredentials)
}
return false, "validator withdrawal credentials do not match those in the operation"
}
@@ -546,14 +635,7 @@ func (c *command) validateOperation(ctx context.Context,
}
func (c *command) broadcastOperations(ctx context.Context) error {
// Broadcast the operations.
for _, signedOperation := range c.signedOperations {
if err := c.consensusClient.(consensusclient.BLSToExecutionChangeSubmitter).SubmitBLSToExecutionChange(ctx, signedOperation); err != nil {
return err
}
}
return nil
return c.consensusClient.(consensusclient.BLSToExecutionChangesSubmitter).SubmitBLSToExecutionChanges(ctx, c.signedOperations)
}
func (c *command) setup(ctx context.Context) error {
@@ -633,24 +715,6 @@ func (c *command) fetchValidatorInfo(ctx context.Context) (*validatorInfo, error
return validatorInfo, nil
}
func (c *command) fetchAccount(ctx context.Context) (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
switch {
case c.account != "":
account, err = util.ParseAccount(ctx, c.account, c.passphrases, true)
case c.mnemonic != "":
account, err = util.ParseAccount(ctx, c.mnemonic, []string{c.path}, true)
case c.privateKey != "":
account, err = util.ParseAccount(ctx, c.privateKey, nil, true)
default:
err = errors.New("account, mnemonic or private key must be supplied")
}
return account, err
}
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
func addressBytesToEIP55(address []byte) string {
bytes := []byte(fmt.Sprintf("%x", address))
@@ -694,3 +758,101 @@ func (c *command) generateOperationsFromMnemonicAndPath(ctx context.Context) err
return nil
}
func (c *command) generateOperationsFromAccountAndWithdrawalAccount(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.withdrawalAccount, c.passphrases, true)
if err != nil {
return err
}
// Find the validator info given its account information.
validatorPubkey := validatorAccount.PublicKey().Marshal()
var validatorInfo *validatorInfo
for _, validator := range c.chainInfo.Validators {
if bytes.Equal(validator.Pubkey[:], validatorPubkey) {
// Found it.
validatorInfo = validator
}
}
if validatorInfo == nil {
return errors.New("could not find information for that validator on the chain")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}
func (c *command) generateOperationsFromAccountAndPrivateKey(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return err
}
// Find the validator info given its account information.
validatorPubkey := validatorAccount.PublicKey().Marshal()
var validatorInfo *validatorInfo
for _, validator := range c.chainInfo.Validators {
if bytes.Equal(validator.Pubkey[:], validatorPubkey) {
// Found it.
validatorInfo = validator
}
}
if validatorInfo == nil {
return errors.New("could not find information for that validator on the chain")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}
func (c *command) generateOperationsFromValidatorAndPrivateKey(ctx context.Context) error {
validator, err := c.fetchValidatorInfo(ctx)
if err != nil {
return err
}
validatorAccount, err := util.ParseAccount(ctx, validator.Pubkey.String(), nil, false)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return err
}
// Find the validator info given its account information.
validatorPubkey := validatorAccount.PublicKey().Marshal()
var validatorInfo *validatorInfo
for _, validator := range c.chainInfo.Validators {
if bytes.Equal(validator.Pubkey[:], validatorPubkey) {
// Found it.
validatorInfo = validator
}
}
if validatorInfo == nil {
return errors.New("could not find information for that validator on the chain")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}

View File

@@ -28,12 +28,14 @@ var validatorCredentialsSetCmd = &cobra.Command{
ethdo validator credentials set --validator=primary/validator --withdrawal-address=0x00...13 --private-key=0x00...1f
The existing account can be specified in one of a number of ways:
The validator account can be specified in one of a number of ways:
- mnemonic using --mnemonic; this will scan the mnemonic and generate all required operations
- mnemonic using --mnemonic; this will scan the mnemonic and generate all applicable operations
- mnemonic and path to the validator key using --mnemonic and --path; this will generate a single operation
- private key using --private-key; this will generate a single operation
- account and passphrase using --account and --passphrase; this will generate a single operation
- mnemonic and validator index or public key --mnemonic and --validator; this will generate a single operation
- mnemonic and withdrawal private key using --mnemonic and --private-key; this will generate all applicable operations
- validator and withdrawal private key using --validator and --private-key; this will generate a single operation
- account and withdrawal account using --account and --withdrawal-account; this will generate a single operation
In quiet mode this will return 0 if the credentials operation has been generated (and successfully broadcast if online), otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -56,6 +58,7 @@ func init() {
validatorCredentialsFlags(validatorCredentialsSetCmd)
validatorCredentialsSetCmd.Flags().Bool("prepare-offline", false, "Create files for offline use")
validatorCredentialsSetCmd.Flags().String("validator", "", "Validator for which to set validator credentials")
validatorCredentialsSetCmd.Flags().String("withdrawal-account", "", "Account with which the validator's withdrawal credentials were set")
validatorCredentialsSetCmd.Flags().String("withdrawal-address", "", "Execution address to which to direct withdrawals")
validatorCredentialsSetCmd.Flags().String("signed-operation", "", "Use pre-defined JSON signed operation as created by --json to transmit the credentials change operation")
validatorCredentialsSetCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
@@ -74,6 +77,9 @@ func validatorCredentialsSetBindings() {
if err := viper.BindPFlag("signed-operation", validatorCredentialsSetCmd.Flags().Lookup("signed-operation")); err != nil {
panic(err)
}
if err := viper.BindPFlag("withdrawal-account", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-account")); err != nil {
panic(err)
}
if err := viper.BindPFlag("withdrawal-address", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-address")); err != nil {
panic(err)
}

View File

@@ -179,28 +179,36 @@ A mnemonic is a 24-word phrase from which withdrawal and validator keys are deri
however this is only a standard and not a restriction, and it is possible for users to have created validators using paths of their own choice.
```
ethdo validator credentials set --validator=123 --mnemonic="abandon abandon abandon … art" --path='m/12381/3600/0/0/0' --withdrawal-address=0x0123…cdef
ethdo validator credentials set --mnemonic="abandon abandon abandon … art" --path='m/12381/3600/0/0/0' --withdrawal-address=0x0123…cdef
```
replacing the path with the path to your _withdrawal_ key, and all other parameters with your own values.
#### Using a private key
If you have the private key from which the current withdrawal credentials were derived this can be used to generate and broadcast the credentials change operation with the following command:
#### Using a mnemonic and validator.
Similar to the previous section, however instead of specifying a path instead the index, public key or account of the validator is provided.
```
ethdo validator credentials set --validator=123 --withdrawal-address=0x8f…9F --private-key=0x3b…9c
ethdo validator credentials set --mnemonic="abandon abandon abandon … art" --validator=123 --withdrawal-address=0x0123…cdef
```
replacing the parameters with your own values.
#### Using a mnemonic and withdrawal private key.
If the withdrawal address was created using a non-standard method then it is possible that you have the private key for the withdrawal address. In this situation you can supply the withdrawal private key.
```
ethdo validator credentials set --mnemonic="abandon abandon abandon … art" --private-key=0x3b…9c
```
Note that it is possible for there to be multiple validators that use the provided private key for a withdrawal address, in which case an operation will be generated for each validator that is eligible for change.
#### Using an account
If you used `ethdo` to generate your validator deposit data you will likely have used a separate account to generate the withdrawal credentials. You can specify the account to generate and broadcast the credentials change operation with the following command:
If you used `ethdo` to generate your validator deposit data you will likely have used a separate account to generate the withdrawal credentials. You can specify the accout of the validator and the accout of the withdrawal credentials to generate and broadcast the credentials change operation with the following command:
```
ethdo validator credentials set --validator=123 --withdrawal-address=0x8f…9F --account=Wallet/Account --passphrase=secret
ethdo validator credentials set --withdrawal-address=0x8f…9F --account=Wallet/Account --withdrawal-account=Withdrawals/Account --passphrase=secret
```
replacing the parameters with your own values.
replacing the parameters with your own values. Note that the passphrase here is the passphrsae of the withdrawal account, not the validator account.
## Confirming the process has succeeded
The final step is confirming the operation has taken place. To do so, run the following command on an online server:

4
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/wealdtech/ethdo
go 1.18
require (
github.com/attestantio/go-eth2-client v0.14.5
github.com/attestantio/go-eth2-client v0.15.0
github.com/ferranbt/fastssz v0.1.2
github.com/gofrs/uuid v4.2.0+incompatible
github.com/google/uuid v1.3.0
@@ -32,7 +32,7 @@ require (
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.6.0
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.4.0
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.17.0
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.0
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.1
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0
github.com/wealdtech/go-eth2-wallet-types/v2 v2.10.0
github.com/wealdtech/go-string2eth v1.2.0

8
go.sum
View File

@@ -73,8 +73,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/attestantio/dirk v1.1.0 h1:hwMTYZkwj/Y0um3OD0LQxg2xSl4/5xqVWV2MRePE4ec=
github.com/attestantio/dirk v1.1.0/go.mod h1:2jkOw/XHjvIDdhDcmj+Z3kuVPpxMcQ6zxzzjSSv71PY=
github.com/attestantio/go-eth2-client v0.8.1/go.mod h1:kEK9iAAOBoADO5wEkd84FEOzjT1zXgVWveQsqn+uBGg=
github.com/attestantio/go-eth2-client v0.14.5 h1:pKOTcbv9KOiVixVKM5Cr8nJrjD1VOIWmrlNFO0YtF64=
github.com/attestantio/go-eth2-client v0.14.5/go.mod h1:5kLLzdlyPGboWr8tAwnG/4Kpi43BHd/HWp++WmmP6Ws=
github.com/attestantio/go-eth2-client v0.15.0 h1:Ia8U1EPYFJ8KB/vsQ2+oEhzuPgCePlBkWXg1R3e0oWw=
github.com/attestantio/go-eth2-client v0.15.0/go.mod h1:5kLLzdlyPGboWr8tAwnG/4Kpi43BHd/HWp++WmmP6Ws=
github.com/aws/aws-sdk-go v1.33.17/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.40.41/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.41.19/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
@@ -588,8 +588,8 @@ github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.15/go.mod h1:v/JATYJQ
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.17.0 h1:cq7k9osiIkaYrdpetPQgk3ozl/dFvmxW364OC/uNuww=
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.17.0/go.mod h1:Fiw5If3/mgH+qYRKIH+kTpZZ3r6z2KgHUiE5Vf/QrfE=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.10.0/go.mod h1:DhAm7si8N/5qU1sZ/RLavm87LsOthnWuRyQaGWNFiyI=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.0 h1:qta5XIXe7o9djqB6L90dmf2CvnLmVunzmKeIAnrUyPs=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.0/go.mod h1:azzsylTwr1hnLisDWZJbUz3HToRsrG7ADpLG8TXJgOU=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.1 h1:q9/p/UfrT7AfR6MYZfr3nQOKdhcLCuKqldqplNHo3Ws=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.1/go.mod h1:azzsylTwr1hnLisDWZJbUz3HToRsrG7ADpLG8TXJgOU=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.3/go.mod h1:V4NUofSBIyzoqc5cNZaGciaDm2WFAgSQikRslOyh5Tg=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0 h1:1dMKx9jtw1v9JrwOPFf2JaOQKmvpMp1GEeuMRiNfq5o=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0/go.mod h1:O7BitrDeQVtBFNnvYmOYLzJCZAiCf5ur/4IRucIz+S0=

View File

@@ -47,10 +47,13 @@ func SetupStore() error {
return errors.New("basedir does not apply to the s3 store")
}
store, err = s3.New(s3.WithPassphrase([]byte(GetStorePassphrase("s3"))),
s3.WithID([]byte(viper.GetString("stores.s3.id"))),
s3.WithEndpoint(viper.GetString("stores.s3.endpoint")),
s3.WithRegion(viper.GetString("stores.s3.region")),
s3.WithBucket(viper.GetString("stores.s3.bucket")),
s3.WithPath(viper.GetString("stores.s3.path")),
s3.WithCredentialsID(viper.GetString("stores.s3.credentials.id")),
s3.WithCredentialsSecret(viper.GetString("stores.s3.credentials.secret")),
)
if err != nil {
return errors.Wrap(err, "failed to access Amazon S3 wallet store")