Skip to content

Commit

Permalink
initial pass of resharing end to end (#9)
Browse files Browse the repository at this point in the history
- added local DKG state for users
- created a state directory for sidecar
- refactored the key to be in relation to it
- implemented resharing for the same group
- updated to go 1.22
- moved keys from localhost to 127 for github runner
- user passes a hash of the old key share, to avoid confusion if some DKG fails for some nodes
- integration test added for multiple reshares and with new groups
- threshold isn't passed around, as we default to 50%+1
- added a debug logging flag
- added lots of logs
- graceful shutdown of stub server to avoid port contention
- fixed some PR feedback, moved some things to internal
  • Loading branch information
CluEleSsUK authored May 28, 2024
1 parent ab69634 commit cd8ee94
Show file tree
Hide file tree
Showing 36 changed files with 1,613 additions and 423 deletions.
17 changes: 16 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,23 @@ $ ssv-dkg sign --input /path/to/deposit/data \
}
```
Each operator must be in the form `$validatorNonce,$address`. Providing the wrong validator nonce may result in disaster for your DKG.
The output directory will default to `~/.ssv`. It will be in a file named after the date (and a counter if you create multiple clusters in a day).
You will need to maintain this state file if you wish to reshare the key for this cluster in the future, e.g. if operators become unresponsive and you wish to exclude them.

- combine both in a single command
```shell
$ ssv-dkg operators list --quiet | head --lines 3 | ssv-dkg sign --input /path/to/deposit --quiet > signed_deposit.json
```
```

- reshare the key of a validator cluster you've already created
```shell
$ ssv-dkg reshare --state ~/.ssv/deadbeefcafebabe.json \
--operator 1,https://example.org \
--operator 2,https://muster.de \
--operator 9,https://exemple.fr

⏳ contacting nodes
⏳ starting distributed key resharing
✅ distributed key reshared successfully!
```
Note: you will have to maintain a majority of operators from one cluster to the next.
105 changes: 105 additions & 0 deletions cli/internal/cmd/reshare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package cmd

import (
"errors"
"fmt"
"io"
"strings"

"github.com/randa-mu/ssv-dkg/cli"
"github.com/randa-mu/ssv-dkg/shared"
"github.com/spf13/cobra"
)

var stateFilePath string
var reshareCmd = &cobra.Command{
Use: "reshare",
Short: "Reshares the key for a validator cluster you have already created",
Long: "Reshares the key for a validator cluster you have already created",
Run: Reshare,
}

func init() {
reshareCmd.PersistentFlags().StringArrayVarP(
&operatorFlag,
"operator",
"o",
nil,
"SSV DKG node operators you wish to sign your ETH deposit data",
)
reshareCmd.PersistentFlags().StringVarP(&stateFilePath,
"state",
"s",
"",
"The filepath of the initial distributed key generated validator cluster that you wish to reshare. Note: this will get rewritten during execution.",
)
}

func Reshare(cmd *cobra.Command, _ []string) {
log := shared.QuietLogger{}
if stateFilePath == "" {
shared.Exit("you must enter the path to the state created from the initial distributed key generation")
}

// if the operator flag isn't passed, we consume operator addresses from stdin
operators, err := arrayOrReader(operatorFlag, cmd.InOrStdin())
if err != nil {
shared.Exit("you must pass your new set of operators either via the operator flag or from stdin")
}

state, err := cli.LoadState(stateFilePath)
if err != nil {
shared.Exit(fmt.Sprintf("❌ tried to load state from %s but it failed: %v", stateFilePath, err))
}

nextState, err := cli.Reshare(operators, state, log)
if err != nil {
shared.Exit(fmt.Sprintf("❌ resharing failed: %v", err))
}

bytes, err := cli.StoreState(stateFilePath, nextState)
if err != nil {
fmt.Printf("⚠️ there was an error storing your state; printing it to the console so you can save it in a flat file. Err: %v\n", err)
fmt.Println(string(bytes))
shared.Exit("")
}

log.MaybeLog(fmt.Sprintf("✅ reshare completed successfully. Encrypted shares stored in %s", stateFilePath))
}

func operatorsOrStdin(cmd *cobra.Command) ([]string, error) {
if len(operatorFlag) != 0 {
return operatorFlag, nil
}
// if the operator flag isn't passed, we consume operator addresses from stdin
stdin, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return nil, errors.New("error reading from stdin")
}

operatorString := strings.Trim(string(stdin), "\n")
if operatorString == "" {
return nil, errors.New("you must provider either the --operator flag or operators via stdin")
}

return strings.Split(operatorString, " "), nil
}

// arrayOrReader returns the array if it's non-empty, or reads an array of strings from the provided `Reader` if it's empty
func arrayOrReader(arr []string, r io.Reader) ([]string, error) {
if len(arr) != 0 {
return arr, nil
}

bytes, err := io.ReadAll(r)
if err != nil {
return nil, err
}

lines := strings.Trim(string(bytes), "\n")
if lines == "" {
return nil, errors.New("reader was empty")
}

return strings.Split(lines, " "), nil
}
3 changes: 2 additions & 1 deletion cli/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

Expand All @@ -14,7 +15,7 @@ var (
)

func init() {
rootCmd.AddCommand(versionCmd, operatorsCmd, signCmd)
rootCmd.AddCommand(versionCmd, operatorsCmd, signCmd, reshareCmd)
}

func Execute() error {
Expand Down
55 changes: 35 additions & 20 deletions cli/internal/cmd/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package cmd

import (
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/randa-mu/ssv-dkg/cli"
"github.com/randa-mu/ssv-dkg/shared"
Expand All @@ -19,7 +19,7 @@ var stateDirectory string
var signCmd = &cobra.Command{
Use: "sign",
Short: "Signs ETH deposit data by forming a validator cluster",
Long: "Signs ETH deposit data by forming a validator cluster that creates a distributed key.",
Long: "Signs ETH deposit data by forming a validator cluster that creates a distributed key. Operators can be passed via stdin.",
Run: Sign,
}

Expand All @@ -38,33 +38,48 @@ func init() {
}

func Sign(cmd *cobra.Command, _ []string) {
// if the operator flag isn't passed, we consume operator addresses from stdin
var args []string
if len(operatorFlag) == 0 {
stdin, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
shared.Exit("error reading from stdin")
}

args = strings.Split(strings.Trim(string(stdin), "\n"), " ")
args, depositData, err := verifyAndGetArgs(cmd)
if err != nil {
shared.Exit(fmt.Sprintf("%v", err))
}

log := shared.QuietLogger{Quiet: shortFlag}
signingOutput, err := cli.Sign(shared.Uniq(append(args, operatorFlag...)), depositData, log)
if err != nil {
shared.Exit(fmt.Sprintf("%v", err))
}

if inputPathFlag == "" {
shared.Exit("input path cannot be empty")
log.MaybeLog(fmt.Sprintf("✅ received signed deposit data! sessionID: %s", hex.EncodeToString(signingOutput.SessionID)))
log.Log(base64.StdEncoding.EncodeToString(signingOutput.GroupSignature))

path := cli.CreateFilename(stateDirectory, signingOutput)
bytes, err := cli.StoreStateIfNotExists(path, signingOutput)
if err != nil {
log.Log(fmt.Sprintf("⚠️ there was an error storing the state; you should store it somewhere for resharing. Error: %v", err))
log.Log(string(bytes))
}
}

depositData, err := os.ReadFile(inputPathFlag)
func verifyAndGetArgs(cmd *cobra.Command) ([]string, []byte, error) {
// if the operator flag isn't passed, we consume operator addresses from stdin
operators, err := arrayOrReader(operatorFlag, cmd.InOrStdin())
if err != nil {
shared.Exit(fmt.Sprintf("error reading the deposit data file: %v", err))
return nil, nil, errors.New("you must provider either the --operator flag or operators via stdin")
}

if inputPathFlag == "" {
return nil, nil, errors.New("input path cannot be empty")
}

// there is a default value, so this shouldn't really happen
if stateDirectory == "" {
return nil, nil, errors.New("you must provide a state directory")
}

groupSignature, err := cli.Sign(shared.Uniq(append(args, operatorFlag...)), depositData, log)
depositData, err := os.ReadFile(inputPathFlag)
if err != nil {
shared.Exit(fmt.Sprintf("%v", err))
return nil, nil, fmt.Errorf("error reading the deposit data file: %v", err)
}

log.MaybeLog("✅ received signed deposit data!")
log.Log(base64.StdEncoding.EncodeToString(groupSignature))
return operators, depositData, nil
}
106 changes: 106 additions & 0 deletions cli/internal/cmd/sign_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmd

import (
"os"
"path"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestSignCommand(t *testing.T) {
tmp := t.TempDir()
filepath := path.Join(tmp, "testfile")
createJunkFile(t, filepath)

tests := []struct {
name string
args []string
stdin *strings.Reader
shouldError bool
}{
{
name: "expected flags succeeds",
shouldError: false,
args: []string{
"ssv-dkg",
"sign",
"--input", filepath,
"--output", filepath,
"--operator", "1,http://127.0.0.1:8081",
"--operator", "2,http://127.0.0.1:8082",
"--operator", "3,http://127.0.0.1:8083",
},
},
{
name: "operators from stdin works",
shouldError: false,
args: []string{
"ssv-dkg",
"sign",
"--input", filepath,
"--output", filepath,
"--operator", "1,http://127.0.0.1:8081",
"--operator", "2,http://127.0.0.1:8082",
"--operator", "3,http://127.0.0.1:8083",
},
stdin: strings.NewReader("1,http://127.0.0.1:8081 2,http://127.0.0.1:8082 3,http://127.0.0.1:8083"),
},
{
name: "no input returns error",
shouldError: true,
args: []string{
"ssv-dkg",
"sign",
"--output", filepath,
"--operator", "1,http://127.0.0.1:8081",
"--operator", "2,http://127.0.0.1:8082",
"--operator", "3,http://127.0.0.1:8083",
},
},
{
name: "no operators returns error",
shouldError: true,
args: []string{
"ssv-dkg",
"sign",
"--input", filepath,
"--output", filepath,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.stdin == nil {
test.stdin = strings.NewReader("")
}
signCmd.SetIn(test.stdin)
signCmd.SetArgs(test.args)
err := signCmd.ParseFlags(test.args)
require.NoError(t, err)
_, _, err = verifyAndGetArgs(signCmd)

t.Cleanup(func() {
operatorFlag = nil
inputPathFlag = ""
shortFlag = false
stateDirectory = ""
})
if test.shouldError && err == nil {
t.Fatalf("expected err but got nil")
} else if !test.shouldError && err != nil {
t.Fatalf("expected no err but got: %v", err)
}
})
}
}

func createJunkFile(t *testing.T, filepath string) {
file, err := os.Create(filepath)
require.NoError(t, err)
_, err = file.Write([]byte("hello"))
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
}
Loading

0 comments on commit cd8ee94

Please sign in to comment.