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

testutil/compose: implement v0 define command #569

Merged
merged 5 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
63 changes: 63 additions & 0 deletions testutil/compose/compose/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright © 2022 Obol Labs Inc.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

// Command compose provides a tool to run, test, debug local charon clusters
// using docker-compose.
//
// It consists of three steps:
// - compose define: Creates compose.yml (and p2pkeys) that defines a desired cluster including keygen.
// - compose lock: Creates docker-compose.yml to generates keys and cluster lock file.
// - compose run: Creates docker-compose.yml that runs the cluster.
package main

import (
"time"

"github.com/spf13/cobra"

"github.com/obolnetwork/charon/testutil/compose"
)

func main() {
cobra.CheckErr(newRootCmd().Execute())
}

func newRootCmd() *cobra.Command {
root := &cobra.Command{
Use: "compose",
Short: "Charon Compose - Run, test, and debug a developer-focussed insecure local charon cluster using docker-compose",
}

root.AddCommand(newDefineCmd())

return root
}

func newDefineCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "define",
Short: "Define a cluster; including both keygen and running definitions",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
Short: "Define a cluster; including both keygen and running definitions",
Short: "Define a cluster, including both generating keys and running definitions",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is keygen definition, not actual key generation

}

dir := cmd.Flags().String("compose-dir", "", "Directory to use for compose artifacts")
clean := cmd.Flags().Bool("clean", true, "Clean compose dir before defining a new cluster")
seed := cmd.Flags().Int("seed", int(time.Now().UnixNano()), "Randomness seed")

cmd.RunE = func(cmd *cobra.Command, _ []string) error {
return compose.Define(cmd.Context(), *dir, *clean, *seed)
}

return cmd
}
80 changes: 80 additions & 0 deletions testutil/compose/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright © 2022 Obol Labs Inc.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

package compose

import (
"github.com/obolnetwork/charon/cluster"
)

const (
version = "obol/charon/compose/1.0.0"
composeFile = "compose.yml"
defaultImageTag = "latest"
defaultBeaconNode = "mock"
defaultNumVals = 1
defaultNumNodes = 4
defaultThreshold = 3
)

// vcType defines a validator client type.
type vcType string

const (
vcMock vcType = "mock"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can remove vc prefix since it's already part of the type

Suggested change
vcMock vcType = "mock"
mock vcType = "mock"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to use common prefixes for enum

vcTeku vcType = "teku"
vcLighthouse vcType = "lighthouse"
)

// keyGen defines a key generation process.
type keyGen string

const (
keyGenDKG keyGen = "dkg"
keyGenCreate keyGen = "create"
keyGenSplit keyGen = "split"
)

// config defines a local compose cluster; including both keygen and running a cluster.
type config struct {
// Version defines the compose config version.
Version string `json:"version"`

// ImageTag defines the charon docker image tag: ghcr.io/obolnetwork/charon:{ImageTag}.
ImageTag string `json:"image_tag"`

// VCs define the types of validator clients to use.
VCs []vcType `json:"validator_clients"`

// keyGen defines the key generation process.
KeyGen keyGen `json:"key_gen"`

// BeaconNode url endpoint or "mock" for simnet.
BeaconNode string `json:"beacon_node"`

// Def is the cluster definition.
Def cluster.Definition `json:"definition"`
}

// newDefaultConfig returns a new default config excluding cluster definition.
func newDefaultConfig() config {
return config{
Version: version,
ImageTag: defaultImageTag,
VCs: []vcType{vcTeku, vcLighthouse, vcMock},
KeyGen: keyGenDKG,
BeaconNode: defaultBeaconNode,
}
}
100 changes: 100 additions & 0 deletions testutil/compose/define.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright © 2022 Obol Labs Inc.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

package compose

import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"testing"

"github.com/ethereum/go-ethereum/crypto"
"github.com/goccy/go-yaml"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/cluster"
)

// Define defines a compose cluster; including both keygen and running definitions.
func Define(ctx context.Context, dir string, clean bool, seed int) error {
ctx = log.WithTopic(ctx, "define")

if clean {
files, err := filepath.Glob(path.Join(dir, "*"))
if err != nil {
return errors.Wrap(err, "glob dir")
}
log.Info(ctx, "Cleaning compose dir", z.Int("files", len(files)))
for _, file := range files {
if err := os.RemoveAll(file); err != nil {
return errors.Wrap(err, "remove file")
}
}
}

// TODO(corver): Serve a web UI to allow configuration of default values.

log.Info(ctx, "Using default config")

lock, p2pkeys, _ := cluster.NewForT(&testing.T{}, defaultNumVals, defaultThreshold, defaultNumNodes, seed)
conf := newDefaultConfig()
conf.Def = lock.Definition
conf.Def.Name = "compose"
conf.Def.FeeRecipientAddress = ""
conf.Def.WithdrawalAddress = ""
for i := 0; i < len(conf.Def.Operators); i++ {
conf.Def.Operators[i].Address = ""
}

for i, key := range p2pkeys {
// Best effort creation of folder, rather fail when saving p2pkey file next.
_ = os.MkdirAll(nodeFile(dir, i, ""), 0o755)

err := crypto.SaveECDSA(nodeFile(dir, i, "p2pkey"), key)
if err != nil {
return errors.Wrap(err, "save p2pkey")
}
}

b, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return errors.Wrap(err, "marshal config")
}

b, err = yaml.JSONToYAML(b)
if err != nil {
return errors.Wrap(err, "yaml config")
}

err = os.WriteFile(path.Join(dir, composeFile), b, 0o755)
if err != nil {
return errors.Wrap(err, "write config")
}

log.Info(ctx, "Created config.yml and p2pkeys")

return nil
}

// nodeFile returns the path to a file in a node folder.
func nodeFile(dir string, i int, file string) string {
return path.Join(dir, fmt.Sprintf("node%d", i), file)
}
43 changes: 43 additions & 0 deletions testutil/compose/define_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright © 2022 Obol Labs Inc.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

package compose_test

import (
"context"
"os"
"path"
"testing"

"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/testutil"
"github.com/obolnetwork/charon/testutil/compose"
)

//go:generate go test . -update -clean

func TestDefine(t *testing.T) {
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)

err = compose.Define(context.Background(), dir, false, 1)
require.NoError(t, err)

conf, err := os.ReadFile(path.Join(dir, "compose.yml"))
require.NoError(t, err)

testutil.RequireGoldenBytes(t, conf)
}
35 changes: 35 additions & 0 deletions testutil/compose/testdata/TestDefine.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: obol/charon/compose/1.0.0
image_tag: latest
validator_clients:
- teku
- lighthouse
- mock
key_gen: dkg
beacon_node: mock
definition:
name: compose
operators:
- address: ""
enr: enr:-Ie4QO_2Hr_cSEDspVO3eY2kE6GObcL-F7ouWhwH8PIalvjrZqwTnGWRampMnWFQrE4-LE5EtVdiVn40fq8zdkacQLiAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQI9dJg9StMlwLpNijInaqLqMzVJKmBt7S2NOKZC-uPj5oN0Y3ABg3VkcAI=
nonce: 0
enr_signature: null
- address: ""
enr: enr:-Ie4QND0b1o5R2-iUhEcRkip-QY8l-xF67L1IC4sn3oRJEpYQYYNjDKCEEwUqBE9XwbUjNsI3rh_IoaFHYtf9UGDoKaAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQLOVPSAl78GlDurrLEzUEw-z669q19TmiWQ5wlmjJEoI4N0Y3ADg3VkcAQ=
nonce: 0
enr_signature: null
- address: ""
enr: enr:-Ie4QD3pENsSBFa2mNy9lntGWbyaB3PZqxQg6qfcRsibv2SGVZaUSIM0nH7Ger2Y5u4M5iITz48WUdfN4w8NGkTItSmAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMIF0e-yqxgr5e_leu0Qql06t0gv1WRDg4S5Iaxe_loSIN0Y3AFg3VkcAY=
nonce: 0
enr_signature: null
- address: ""
enr: enr:-Ie4QChc9X-TUNf5Teo1M0ar00cYhSGcbR5ddMxW8z7Tnah4CbDUcjkdgp4JiB3n60UONJ4KNOXuo9StqebRql9yAhaAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMwtBj_k3MB-Yjgz6Qexa5dPzPbVTD-39k3MhHXnQhc5YN0Y3AHg3VkcAg=
nonce: 0
enr_signature: null
uuid: 3BEA6F5B-3AF6-DE03-7436-6C4719E43A1B
version: v1.0.0
num_validators: 1
threshold: 3
dkg_algorithm: default
fork_version: "0x0000000"
definition_hash: kHuiCBpi3oeTEMUQIf4lubbyH7d3BBcCsGjFuh4kaJM=
operator_signatures: null