From 5506e0e3782661ccdbdc3631db4dd1136a7b6ee8 Mon Sep 17 00:00:00 2001 From: corverroos Date: Fri, 20 May 2022 09:54:10 +0200 Subject: [PATCH 1/5] testutil/compose: implement v0 define command --- testutil/compose/compose/main.go | 63 +++++++++++++++++++ testutil/compose/config.go | 80 +++++++++++++++++++++++++ testutil/compose/define.go | 100 +++++++++++++++++++++++++++++++ testutil/compose/define_test.go | 43 +++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 testutil/compose/compose/main.go create mode 100644 testutil/compose/config.go create mode 100644 testutil/compose/define.go create mode 100644 testutil/compose/define_test.go diff --git a/testutil/compose/compose/main.go b/testutil/compose/compose/main.go new file mode 100644 index 000000000..69eb91088 --- /dev/null +++ b/testutil/compose/compose/main.go @@ -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 . + +// 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 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", + } + + dir := cmd.Flags().String("compose-dir", "", "Folder to use for compose artifacts") + clean := cmd.Flags().Bool("clean", true, "Clean folder before defining a new cluster") + seed := cmd.Flags().Int("seed", int(time.Now().UnixNano()), "Seed randomness") + + cmd.RunE = func(cmd *cobra.Command, _ []string) error { + return compose.Define(cmd.Context(), *dir, *clean, *seed) + } + + return cmd +} diff --git a/testutil/compose/config.go b/testutil/compose/config.go new file mode 100644 index 000000000..d8a559c77 --- /dev/null +++ b/testutil/compose/config.go @@ -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 . + +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" + 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, + } +} diff --git a/testutil/compose/define.go b/testutil/compose/define.go new file mode 100644 index 000000000..6d21b4180 --- /dev/null +++ b/testutil/compose/define.go @@ -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 . + +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) +} diff --git a/testutil/compose/define_test.go b/testutil/compose/define_test.go new file mode 100644 index 000000000..82967b1bd --- /dev/null +++ b/testutil/compose/define_test.go @@ -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 . + +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) +} From 330444d090125b13a661f1f695e58c7a9ced930d Mon Sep 17 00:00:00 2001 From: corverroos Date: Fri, 20 May 2022 09:57:00 +0200 Subject: [PATCH 2/5] cleanup --- testutil/compose/testdata/TestDefine.golden | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 testutil/compose/testdata/TestDefine.golden diff --git a/testutil/compose/testdata/TestDefine.golden b/testutil/compose/testdata/TestDefine.golden new file mode 100644 index 000000000..319e834d8 --- /dev/null +++ b/testutil/compose/testdata/TestDefine.golden @@ -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 From 19ef97537ad47d27bbebb0fde4a3969b948536df Mon Sep 17 00:00:00 2001 From: corverroos Date: Fri, 20 May 2022 10:01:52 +0200 Subject: [PATCH 3/5] cleanup --- testutil/compose/compose/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testutil/compose/compose/main.go b/testutil/compose/compose/main.go index 69eb91088..d2803ec5d 100644 --- a/testutil/compose/compose/main.go +++ b/testutil/compose/compose/main.go @@ -37,7 +37,7 @@ func main() { func newRootCmd() *cobra.Command { root := &cobra.Command{ Use: "compose", - Short: "Charon Compose - Run, test, and debug a local charon cluster using docker-compose", + Short: "Charon Compose - Run, test, and debug a developer-focussed insecure local charon cluster using docker-compose", } root.AddCommand(newDefineCmd()) @@ -51,9 +51,9 @@ func newDefineCmd() *cobra.Command { Short: "Define a cluster; including both keygen and running definitions", } - dir := cmd.Flags().String("compose-dir", "", "Folder to use for compose artifacts") - clean := cmd.Flags().Bool("clean", true, "Clean folder before defining a new cluster") - seed := cmd.Flags().Int("seed", int(time.Now().UnixNano()), "Seed randomness") + 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) From 1ea373991321d7275e5ab3a1eee23d7f2de799a7 Mon Sep 17 00:00:00 2001 From: corverroos Date: Fri, 20 May 2022 10:05:38 +0200 Subject: [PATCH 4/5] cleanup --- testutil/compose/config.go | 4 ++-- testutil/compose/define.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testutil/compose/config.go b/testutil/compose/config.go index d8a559c77..ce4bf13be 100644 --- a/testutil/compose/config.go +++ b/testutil/compose/config.go @@ -43,8 +43,8 @@ type keyGen string const ( keyGenDKG keyGen = "dkg" - keyGenCreate keyGen = "create" - keyGenSplit keyGen = "split" + keyGenCreate keyGen = "create" //nolint:deadcode,varcheck + keyGenSplit keyGen = "split" //nolint:deadcode,varcheck ) // config defines a local compose cluster; including both keygen and running a cluster. diff --git a/testutil/compose/define.go b/testutil/compose/define.go index 6d21b4180..c1b06699e 100644 --- a/testutil/compose/define.go +++ b/testutil/compose/define.go @@ -84,7 +84,7 @@ func Define(ctx context.Context, dir string, clean bool, seed int) error { return errors.Wrap(err, "yaml config") } - err = os.WriteFile(path.Join(dir, composeFile), b, 0o755) + err = os.WriteFile(path.Join(dir, composeFile), b, 0o755) //nolint:gosec if err != nil { return errors.Wrap(err, "write config") } From c838e89c3ed0a3d44cd174ef492239fa6e066510 Mon Sep 17 00:00:00 2001 From: corverroos Date: Fri, 20 May 2022 10:06:53 +0200 Subject: [PATCH 5/5] cleanup --- testutil/compose/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testutil/compose/config.go b/testutil/compose/config.go index ce4bf13be..b125a1482 100644 --- a/testutil/compose/config.go +++ b/testutil/compose/config.go @@ -24,6 +24,7 @@ const ( composeFile = "compose.yml" defaultImageTag = "latest" defaultBeaconNode = "mock" + defaultKeyGen = keyGenDKG defaultNumVals = 1 defaultNumNodes = 4 defaultThreshold = 3 @@ -74,7 +75,7 @@ func newDefaultConfig() config { Version: version, ImageTag: defaultImageTag, VCs: []vcType{vcTeku, vcLighthouse, vcMock}, - KeyGen: keyGenDKG, + KeyGen: defaultKeyGen, BeaconNode: defaultBeaconNode, } }