Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate format of ethereum deposit data #10

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions cli/internal/cmd/reshare.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,6 @@ func Reshare(cmd *cobra.Command, _ []string) {
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 {
Expand Down
27 changes: 18 additions & 9 deletions cli/internal/cmd/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package cmd

import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"

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

Expand Down Expand Up @@ -44,41 +45,49 @@ func Sign(cmd *cobra.Command, _ []string) {
}

log := shared.QuietLogger{Quiet: shortFlag}
// TODO: this should probably sign something more than just the deposit data root
signingOutput, err := cli.Sign(shared.Uniq(append(args, operatorFlag...)), depositData, log)
if err != nil {
shared.Exit(fmt.Sprintf("%v", err))
}

log.MaybeLog(fmt.Sprintf("✅ received signed deposit data! sessionID: %s", hex.EncodeToString(signingOutput.SessionID)))
path := cli.CreateFilename(stateDirectory, signingOutput)

log.MaybeLog(fmt.Sprintf("✅ received signed deposit data! stored state in %s", path))
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))
}
}

func verifyAndGetArgs(cmd *cobra.Command) ([]string, []byte, error) {
func verifyAndGetArgs(cmd *cobra.Command) ([]string, api.UnsignedDepositData, error) {
// if the operator flag isn't passed, we consume operator addresses from stdin
operators, err := arrayOrReader(operatorFlag, cmd.InOrStdin())
if err != nil {
return nil, nil, errors.New("you must provider either the --operator flag or operators via stdin")
return nil, api.UnsignedDepositData{}, 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")
return nil, api.UnsignedDepositData{}, 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")
return nil, api.UnsignedDepositData{}, errors.New("you must provide a state directory")
}

depositBytes, err := os.ReadFile(inputPathFlag)
if err != nil {
return nil, api.UnsignedDepositData{}, fmt.Errorf("error reading the deposit data file: %v", err)
}

depositData, err := os.ReadFile(inputPathFlag)
var depositData api.UnsignedDepositData
err = json.Unmarshal(depositBytes, &depositData)
if err != nil {
return nil, nil, fmt.Errorf("error reading the deposit data file: %v", err)
return nil, api.UnsignedDepositData{}, err
}

return operators, depositData, nil
Expand Down
20 changes: 17 additions & 3 deletions cli/internal/cmd/sign_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package cmd

import (
"encoding/json"
"os"
"path"
"strings"
"testing"

"github.com/randa-mu/ssv-dkg/shared/api"
"github.com/stretchr/testify/require"
)

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

tests := []struct {
name string
Expand Down Expand Up @@ -96,10 +98,22 @@ func TestSignCommand(t *testing.T) {
}
}

func createJunkFile(t *testing.T, filepath string) {
func createdUnsignedDepositData(t *testing.T, filepath string) {
data := api.UnsignedDepositData{
WithdrawalCredentials: []byte("hello worldhello worldhello worl"), // must be 32 bytes
DepositDataRoot: []byte("hello world"),
DepositMessageRoot: []byte("hello world"),
Amount: 1,
ForkVersion: "somefork",
NetworkName: "somenetwork",
DepositCLIVersion: "somecli",
}

bytes, err := json.Marshal(data)
require.NoError(t, err)
file, err := os.Create(filepath)
require.NoError(t, err)
_, err = file.Write([]byte("hello"))
_, err = file.Write(bytes)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
Expand Down
22 changes: 15 additions & 7 deletions cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/randa-mu/ssv-dkg/shared/crypto"
)

func Sign(operators []string, depositData []byte, log shared.QuietLogger) (api.SigningOutput, error) {
func Sign(operators []string, depositData api.UnsignedDepositData, log shared.QuietLogger) (api.SigningOutput, error) {
numOfNodes := len(operators)
if numOfNodes != 3 && numOfNodes != 5 && numOfNodes != 7 {
return api.SigningOutput{}, errors.New("you must pass either 3, 5, or 7 operators to ensure a majority threshold")
Expand Down Expand Up @@ -119,7 +119,7 @@ func parseOperator(input string) (uint32, string, error) {
return uint32(validatorNonce), parts[1], nil
}

func runDKG(suite crypto.ThresholdScheme, sessionID []byte, identities []crypto.Identity, depositData []byte) ([]api.OperatorResponse, error) {
func runDKG(suite crypto.ThresholdScheme, sessionID []byte, identities []crypto.Identity, depositData api.UnsignedDepositData) ([]api.OperatorResponse, error) {
dkgResponses := shared.SafeList[api.OperatorResponse]{}
errs := make(chan error, len(identities))
wg := sync.WaitGroup{}
Expand Down Expand Up @@ -171,7 +171,7 @@ func createSessionID() ([]byte, error) {
return s.Sum(nil), nil
}

func getVerifiedPartial(suite crypto.ThresholdScheme, identity crypto.Identity, depositData []byte, identities []crypto.Identity, sessionID []byte) (api.SignResponse, error) {
func getVerifiedPartial(suite crypto.ThresholdScheme, identity crypto.Identity, depositData api.UnsignedDepositData, identities []crypto.Identity, sessionID []byte) (api.SignResponse, error) {
client := api.NewSidecarClient(identity.Address)
data := api.SignRequest{
ValidatorNonce: identity.ValidatorNonce,
Expand All @@ -184,8 +184,12 @@ func getVerifiedPartial(suite crypto.ThresholdScheme, identity crypto.Identity,
return api.SignResponse{}, fmt.Errorf("error signing: %w", err)
}

message, err := crypto.DepositDataMessage(depositData.ExtractRequired(), response.PublicPolynomial)
if err != nil {
return api.SignResponse{}, fmt.Errorf("error building final message: %w", err)
}
// verify that the signature over the deposit data verifies for the reported public key
if err = suite.VerifyPartial(response.PublicPolynomial, depositData, response.DepositDataPartialSignature); err != nil {
if err = suite.VerifyPartial(response.PublicPolynomial, message, response.DepositDataPartialSignature); err != nil {
return api.SignResponse{}, fmt.Errorf("signature did not verify for the signed deposit data for node %s: %w", identity.Address, err)
}
return response, nil
Expand All @@ -196,7 +200,7 @@ type groupSignature struct {
publicKey []byte
}

func aggregateGroupSignature(suite crypto.ThresholdScheme, responses []api.OperatorResponse, depositData []byte) (groupSignature, error) {
func aggregateGroupSignature(suite crypto.ThresholdScheme, responses []api.OperatorResponse, depositData api.UnsignedDepositData) (groupSignature, error) {
// ensure everybody came up with the same polynomials
err := verifyPublicPolynomialSame(responses)
if err != nil {
Expand All @@ -205,15 +209,19 @@ func aggregateGroupSignature(suite crypto.ThresholdScheme, responses []api.Opera

// as all the group public keys are the same, we can use the first to verify all the partials
groupPK := responses[0].Response.PublicPolynomial
depositDataMessage, err := crypto.DepositDataMessage(depositData.ExtractRequired(), groupPK)
if err != nil {
return groupSignature{}, err
}

partials := extractPartials(responses)

signature, err := suite.RecoverSignature(depositData, groupPK, partials, len(responses))
signature, err := suite.RecoverSignature(depositDataMessage, groupPK, partials, len(responses))
if err != nil {
return groupSignature{}, fmt.Errorf("error aggregating signature: %v", err)
}

err = suite.VerifyRecovered(depositData, groupPK, signature)
err = suite.VerifyRecovered(depositDataMessage, groupPK, signature)
if err != nil {
return groupSignature{}, fmt.Errorf("error verifying deposit data signature: %v", err)
}
Expand Down
20 changes: 16 additions & 4 deletions internal/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestSuccessfulSigningAndResharing(t *testing.T) {
return fmt.Sprintf("%d,http://127.0.0.1:%d", o, o)
})

depositData := []byte("hello world")
depositData := createUnsignedDepositData()

log := shared.QuietLogger{Quiet: false}
signingOutput, err := cli.Sign(operators, depositData, log)
Expand Down Expand Up @@ -74,7 +74,7 @@ func TestResharingNewNode(t *testing.T) {
return fmt.Sprintf("%d,http://127.0.0.1:%d", o, o)
})

depositData := []byte("hello world")
depositData := createUnsignedDepositData()

log := shared.QuietLogger{Quiet: false}
signingOutput, err := cli.Sign(operators, depositData, log)
Expand Down Expand Up @@ -107,7 +107,7 @@ func TestErroneousNodeOnStartup(t *testing.T) {
return fmt.Sprintf("%d,http://127.0.0.1:%d", o, o)
})

depositData := []byte("hello world")
depositData := createUnsignedDepositData()

_, err := cli.Sign(operators, depositData, shared.QuietLogger{Quiet: false})
require.Error(t, err)
Expand All @@ -125,7 +125,7 @@ func TestErroneousNodeOnRunningDKG(t *testing.T) {
return fmt.Sprintf("%d,http://127.0.0.1:%d", o, o)
})

depositData := []byte("hello world")
depositData := createUnsignedDepositData()

_, err := cli.Sign(operators, depositData, shared.QuietLogger{Quiet: false})
require.Error(t, err)
Expand Down Expand Up @@ -241,3 +241,15 @@ func awaitHealthy(port uint) error {
}
return err
}

func createUnsignedDepositData() api.UnsignedDepositData {
return api.UnsignedDepositData{
WithdrawalCredentials: []byte("hello worldhello worldhello worl"), // must be 32 bytes
DepositDataRoot: []byte("hello world"),
DepositMessageRoot: []byte("hello world"),
Amount: 1,
ForkVersion: "somefork",
NetworkName: "somenetwork",
DepositCLIVersion: "somecli",
}
}
36 changes: 31 additions & 5 deletions shared/api/cli.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
package api

import "github.com/randa-mu/ssv-dkg/shared/crypto"
import (
"github.com/randa-mu/ssv-dkg/shared/crypto"
)

type UnsignedDepositData struct {
WithdrawalCredentials []byte `json:"withdrawal_credentials"`
DepositDataRoot []byte `json:"deposit_data_root"`
DepositMessageRoot []byte `json:"deposit_message_root,omitempty"`
Amount uint64 `json:"amount,omitempty"`
ForkVersion string `json:"fork_version,omitempty"`
NetworkName string `json:"network_name,omitempty"`
DepositCLIVersion string `json:"deposit_cli_version,omitempty"`
}

type SignedDepositData struct {
UnsignedDepositData
PubKey []byte `json:"pubkey"`
Signature []byte `json:"signature"`
}

type SigningOutput struct {
SessionID []byte `json:"session_id"`
GroupSignature []byte `json:"group_signature"`
PolynomialCommitments []byte `json:"group_public_commitments"`
OperatorShares []OperatorShare `json:"operator_shares"`
SessionID []byte `json:"session_id"`
GroupSignature []byte `json:"group_signature"`
PolynomialCommitments []byte `json:"group_public_commitments"`
OperatorShares []OperatorShare `json:"operator_shares"`
DepositData SignedDepositData `json:"deposit_data"`
}

type OperatorShare struct {
Expand All @@ -18,3 +37,10 @@ type OperatorResponse struct {
Identity crypto.Identity
Response SignResponse
}

func (u UnsignedDepositData) ExtractRequired() crypto.RequiredDepositFields {
return crypto.RequiredDepositFields{
WithdrawalCredentials: u.WithdrawalCredentials,
Amount: u.Amount,
}
}
8 changes: 4 additions & 4 deletions shared/api/sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ type Sidecar interface {
}

type SignRequest struct {
SessionID []byte `json:"session_id"`
ValidatorNonce uint32 `json:"validator_nonce"`
Data []byte `json:"data"`
Operators []crypto.Identity `json:"operators"`
SessionID []byte `json:"session_id"`
ValidatorNonce uint32 `json:"validator_nonce"`
Data UnsignedDepositData `json:"data"`
Operators []crypto.Identity `json:"operators"`
}

type SignResponse struct {
Expand Down
18 changes: 14 additions & 4 deletions shared/api/sidecar_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ package api

import (
"errors"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/require"
"net/http"
"testing"

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

var baseUrl = "https://example.org"
var client = NewSidecarClient(baseUrl)
var depositData = UnsignedDepositData{
WithdrawalCredentials: []byte("hello worldhello worldhello worl"), // must be 32 bytes
DepositDataRoot: []byte("cafebabe"),
DepositMessageRoot: []byte("b00b00b"),
Amount: 1,
ForkVersion: "myfork123",
NetworkName: "holesky",
DepositCLIVersion: "1.2.3",
}

func TestSidecarHealthUp(t *testing.T) {
httpmock.Activate()
Expand Down Expand Up @@ -49,7 +59,7 @@ func TestSidecarSignErrReturned(t *testing.T) {

expectedErr := errors.New("downstream")
httpmock.RegisterResponder("POST", "https://example.org/sign", httpmock.NewErrorResponder(expectedErr))
signRequest := SignRequest{Data: []byte("deadbeef")}
signRequest := SignRequest{Data: depositData}
_, err := client.Sign(signRequest)
require.Error(t, err)
}
Expand All @@ -59,7 +69,7 @@ func TestSidecarInvalidJsonDoesntPanic(t *testing.T) {
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder("POST", "https://example.org/sign", httpmock.NewStringResponder(http.StatusOK, "{ invalid Json }"))
signRequest := SignRequest{Data: []byte("deadbeef")}
signRequest := SignRequest{Data: depositData}
_, err := client.Sign(signRequest)
require.Error(t, err)
}
Loading
Loading