diff --git a/client/obol.go b/client/obol.go new file mode 100644 index 000000000..75ee449eb --- /dev/null +++ b/client/obol.go @@ -0,0 +1,62 @@ +package client + +import ( + "github.com/rocket-pool/node-manager-core/api/client" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +type ObolRequester struct { + context client.IRequesterContext +} + +func NewObolRequester(context client.IRequesterContext) *ObolRequester { + return &ObolRequester{ + context: context, + } +} + +func (r *ObolRequester) GetName() string { + return "DvTest" +} +func (r *ObolRequester) GetRoute() string { + return "dvtest" +} +func (r *ObolRequester) GetContext() client.IRequesterContext { + return r.context +} + +// Trigger a DV exit broadcast +func (r *ObolRequester) DvExitBroadcast() (*types.ApiResponse[api.DvExitBroadcastData], error) { + return client.SendGetRequest[api.DvExitBroadcastData](r, "obol/dv-exit-broadcast", "DvExitBroadcast", nil) +} + +// Trigger a DV exit sign +func (r *ObolRequester) DvExitSign() (*types.ApiResponse[api.DvExitSignData], error) { + return client.SendGetRequest[api.DvExitSignData](r, "obol/dv-exit-sign", "DvExitSign", nil) +} + +// Retrieve validator public keys +func (r *ObolRequester) GetValidatorPublicKeys() (*types.ApiResponse[api.GetValidatorPublicKeysData], error) { + return client.SendGetRequest[api.GetValidatorPublicKeysData](r, "obol/get-validator-public-keys", "GetValidatorPublicKeys", nil) +} + +// Creates cluster - DKG +func (r *ObolRequester) CharonDkg() (*types.ApiResponse[api.CharonDkgData], error) { + return client.SendGetRequest[api.CharonDkgData](r, "obol/charon-dkg", "CharonDkg", nil) +} + +// Creates ENR +func (r *ObolRequester) CreateENR() (*types.ApiResponse[api.CreateENRData], error) { + return client.SendGetRequest[api.CreateENRData](r, "obol/create-enr", "CreateENR", nil) +} + +// Manages Charon service +func (r *ObolRequester) ManageCharonService() (*types.ApiResponse[api.ManageCharonServiceData], error) { + return client.SendGetRequest[api.ManageCharonServiceData](r, "obol/manage-charon-service", "ManageCharonService", nil) +} + +// Retrieve Charon service health +func (r *ObolRequester) GetCharonHealth() (*types.ApiResponse[api.GetCharonHealthData], error) { + return client.SendGetRequest[api.GetCharonHealthData](r, "obol/get-charon-health", "GetCharonHealth", nil) +} diff --git a/rocketpool-cli/commands/obol/cluster.go b/rocketpool-cli/commands/obol/cluster.go new file mode 100644 index 000000000..66d2a5d4a --- /dev/null +++ b/rocketpool-cli/commands/obol/cluster.go @@ -0,0 +1,75 @@ +package obol + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/client" + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils/tx" +) + +func charonDkg(c *cli.Context) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + response, err := rp.Api.Obol.CharonDkg() + if err != nil { + return fmt.Errorf("Error creating a DV cluster: %w", err) + } + // Log & return + fmt.Println("Successfully triggered cluster DKG creation.") + return nil +} + +func createENR(c *cli.Context) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + response, err := rp.Api.Obol.CreateENR() + if err != nil { + return fmt.Errorf("Error creating ENR: %w", err) + } + // Log & return + fmt.Println("Successfully created ENR") + return nil +} + +func manageCharonService(c *cli.Context) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + response, err := rp.Api.Obol.ManageCharonService() + if err != nil { + return fmt.Errorf("Error managinig charon service: %w", err) + } + // Log & return + fmt.Println("Successfully updated Charon service state") + return nil +} + +func getCharonHealth(c *cli.Context) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + response, err := rp.Api.Obol.GetCharonHealth() + if err != nil { + return fmt.Errorf("Error fetching charon health: %w", err) + } + // Log & return + fmt.Println("Successfully fetched charon health") + return nil +} + diff --git a/rocketpool-cli/commands/obol/commands.go b/rocketpool-cli/commands/obol/commands.go new file mode 100644 index 000000000..d2f8240b3 --- /dev/null +++ b/rocketpool-cli/commands/obol/commands.go @@ -0,0 +1,103 @@ +package obol + +import ( + "github.com/urfave/cli/v2" + + cliutils "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils" + "github.com/rocket-pool/smartnode/v2/shared/utils" +) + +// Register commands +func RegisterCommands(app *cli.App, name string, aliases []string) { + app.Commands = append(app.Commands, &cli.Command{ + Name: name, + Aliases: aliases, + Usage: "Manage Obol Distributed Validator", + Subcommands: []*cli.Command{ + { + Name: "health", + Aliases: []string{"h"}, + Usage: "Get Charon service health", + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 0) + + // Run + return getCharonHealth(c) + }, + }, + { + Name: "manage-charon-service", + Aliases: []string{"s"}, + Usage: "Start, stop or restart the Charon service", + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 0) + + // Run + return manageCharonService(c) + }, + }, + { + Name: "create-enr", + Aliases: []string{"n"}, + Usage: "Create ENR for a Charon client", + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 0) + + // Run + return createENR(c) + }, + }, + { + Name: "charon-dkg", + Aliases: []string{"d"}, + Usage: "Run the Distributed Key Generation (DKG) ceremony", + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 0) + + // Run + return runCharonDkg(c) + }, + }, + { + Name: "get-validator-public-keys", + Aliases: []string{"k"}, + Usage: "Retrieves validator public keys", + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 0) + + // Run + return getValidatorPublicKeys(c) + }, + }, + { + Name: "dv-exit-sign", + Aliases: []string{"e"}, + Usage: "Sign a partial DV exit", + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 0) + + // Run + return dvExitSign(c) + }, + }, + { + Name: "dv-exit-broadcast", + Aliases: []string{"b"}, + Usage: "Publish a DV exit", + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 0) + + // Run + return dvExitBroadcast(c) + }, + } + }, + }) +} diff --git a/rocketpool-cli/commands/obol/dv-exit.go b/rocketpool-cli/commands/obol/dv-exit.go new file mode 100644 index 000000000..877150e4d --- /dev/null +++ b/rocketpool-cli/commands/obol/dv-exit.go @@ -0,0 +1,45 @@ +package obol + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/client" + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils/tx" +) + +func dvExitBroadcast(c *cli.Context) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + // Check lot can be created + response, err := rp.Api.Obol.DvExitBroadcast() + if err != nil { + return fmt.Errorf("Error triggering broadcast for a DV exit: %w", err) + } + // Log & return + fmt.Println("Successfully triggered DV exit broadcast.") + return nil +} + +func dvExitSign(c *cli.Context) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + // Check lot can be created + response, err := rp.Api.Obol.DvExitSign() + if err != nil { + return fmt.Errorf("Error signing for a DV exit: %w", err) + } + // Log & return + fmt.Println("Successfully signed for a DV exit.") + return nil +} + diff --git a/rocketpool-cli/commands/obol/public-keys.go b/rocketpool-cli/commands/obol/public-keys.go new file mode 100644 index 000000000..91c9fd967 --- /dev/null +++ b/rocketpool-cli/commands/obol/public-keys.go @@ -0,0 +1,27 @@ +package obol + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/client" + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils/tx" +) + +func getValidatorPublicKeys(c *cli.Context) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + // Check lot can be created + response, err := rp.Api.Obol.GetValidatorPublicKeys() + if err != nil { + return fmt.Errorf("Error fetching validator public keys: %w", err) + } + // Log & return + fmt.Println("Successfully fetched validator public keys.") + return nil +} diff --git a/rocketpool-daemon/api/obol/charon-dkg-handler.go b/rocketpool-daemon/api/obol/charon-dkg-handler.go new file mode 100644 index 000000000..42f35b281 --- /dev/null +++ b/rocketpool-daemon/api/obol/charon-dkg-handler.go @@ -0,0 +1,82 @@ +package obol + +import ( + "fmt" + "net/url" + + "log" + + "os/exec" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +// =============== +// === Factory === +// =============== + +type CharonDkgContextFactory struct { + handler *ObolHandler +} + +func (f *CharonDkgContextFactory) Create(args url.Values) (*CharonDkgContext, error) { + c := &CharonDkgContext{ + handler: f.handler, + } + return c, nil +} + +func (f *CharonDkgContextFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*CharonDkgContext, api.CharonDkgData]( + router, "obol/charon-dkg", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type CharonDkgContext struct { + handler *ObolHandler + rp *rocketpool.RocketPool + + password string +} + +func (c *CharonDkgContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + + // Requirements + status, err := sp.RequireNodeRegistered(c.handler.ctx) + if err != nil { + return status, err + } + + return types.ResponseStatus_Success, nil +} + +func (c *CharonDkgContext) PrepareData(data *api.CharonDkgData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + cmd := exec.Command( + "docker", "run", "--rm", + "-v", fmt.Sprintf("%s:/opt/charon", c.password), + "obolnetwork/charon:v1.1.0", + "dkg", "--publish", + ) + + output, err := cmd.CombinedOutput() + + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error running docker command: %s", err) + } + log.Printf("Command output: %s", string(output)) + return types.ResponseStatus_Success, nil +} + diff --git a/rocketpool-daemon/api/obol/create-enr-handler.go b/rocketpool-daemon/api/obol/create-enr-handler.go new file mode 100644 index 000000000..3eff7f11a --- /dev/null +++ b/rocketpool-daemon/api/obol/create-enr-handler.go @@ -0,0 +1,82 @@ +package obol + +import ( + "fmt" + "net/url" + + "log" + + "os/exec" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +// =============== +// === Factory === +// =============== + +type CreateENRContextFactory struct { + handler *ObolHandler +} + +func (f *CreateENRContextFactory) Create(args url.Values) (*CreateENRContext, error) { + c := &CreateENRContext{ + handler: f.handler, + } + return c, nil +} + +func (f *CreateENRContextFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*CreateENRContext, api.CreateENRData]( + router, "obol/create-enr", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type CreateENRContext struct { + handler *ObolHandler + rp *rocketpool.RocketPool + + password string +} + +func (c *CreateENRContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + + // Requirements + status, err := sp.RequireNodeRegistered(c.handler.ctx) + if err != nil { + return status, err + } + + return types.ResponseStatus_Success, nil +} + +func (c *CreateENRContext) PrepareData(data *api.CreateENRData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + cmd := exec.Command( + "docker", "run", "--rm", + "-v", fmt.Sprintf("%s:/opt/charon", c.password), + "obolnetwork/charon:v1.1.0", + "create", "enr", + ) + + output, err := cmd.CombinedOutput() + + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error running docker command: %s", err) + } + log.Printf("Command output: %s", string(output)) + return types.ResponseStatus_Success, nil +} + diff --git a/rocketpool-daemon/api/obol/dv-exit-broadcast-handler.go b/rocketpool-daemon/api/obol/dv-exit-broadcast-handler.go new file mode 100644 index 000000000..05dd6b323 --- /dev/null +++ b/rocketpool-daemon/api/obol/dv-exit-broadcast-handler.go @@ -0,0 +1,88 @@ +package obol + +import ( + "fmt" + "net/url" + + "log" + + "os/exec" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +// =============== +// === Factory === +// =============== + +type DvExitBroadcastContextFactory struct { + handler *ObolHandler +} + +func (f *DvExitBroadcastContextFactory) Create(args url.Values) (*DvExitBroadcastContext, error) { + c := &DvExitBroadcastContext{ + handler: f.handler, + } + return c, nil +} + +func (f *DvExitBroadcastContextFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*DvExitBroadcastContext, api.DvExitBroadcastData]( + router, "obol/dv-exit-broadcast", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type DvExitBroadcastContext struct { + handler *ObolHandler + rp *rocketpool.RocketPool + + endpoint string + validatorPublicKey string + publishTimeout string +} + +func (c *DvExitBroadcastContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + + // Requirements + status, err := sp.RequireNodeRegistered(c.handler.ctx) + if err != nil { + return status, err + } + + return types.ResponseStatus_Success, nil +} + +func (c *DvExitBroadcastContext) PrepareData(data *api.DvExitBroadcastData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + cmd := exec.Command( + "docker", "exec", "-it", "charon-distributed-validator-node-charon-1", + "/bin/sh", "-c", + fmt.Sprintf(`charon exit sign --beacon-node-endpoints="%s" --validator-public-key="%s" --publish-timeout="%s"`, + c.endpoint, + c.validatorPublicKey, + c.publishTimeout, + ), + ) + + + output, err := cmd.CombinedOutput() + + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error running docker command: %s", err) + } + log.Printf("Command output: %s", string(output)) + return types.ResponseStatus_Success, nil +} + diff --git a/rocketpool-daemon/api/obol/dv-exit-sign-handler.go b/rocketpool-daemon/api/obol/dv-exit-sign-handler.go new file mode 100644 index 000000000..075260091 --- /dev/null +++ b/rocketpool-daemon/api/obol/dv-exit-sign-handler.go @@ -0,0 +1,84 @@ +package obol + +import ( + "fmt" + // "strings" + "net/url" + + "log" + + "os/exec" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +// =============== +// === Factory === +// =============== + +type DvExitSignContextFactory struct { + handler *ObolHandler +} + +func (f *DvExitSignContextFactory) Create(args url.Values) (*DvExitSignContext, error) { + c := &DvExitSignContext{ + handler: f.handler, + } + return c, nil +} + +func (f *DvExitSignContextFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*DvExitSignContext, api.DvExitSignData]( + router, "obol/dv-exit-sign", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type DvExitSignContext struct { + handler *ObolHandler + rp *rocketpool.RocketPool + + endpoint string +} + +func (c *DvExitSignContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + + // Requirements + status, err := sp.RequireNodeRegistered(c.handler.ctx) + if err != nil { + return status, err + } + + // Bindings + + return types.ResponseStatus_Success, nil +} + +func (c *DvExitSignContext) PrepareData(data *api.DvExitSignData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + cmd := exec.Command( + "docker", "exec", "-it", "charon-distributed-validator-node-charon-1", + "/bin/sh", "-c", + fmt.Sprintf(`charon exit active-validator-list --beacon-node-endpoints="%s"`, c.endpoint), + ) + + output, err := cmd.CombinedOutput() + + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error running docker command: %s", err) + } + log.Printf("Command output: %s", string(output)) + return types.ResponseStatus_Success, nil +} + diff --git a/rocketpool-daemon/api/obol/get-charon-health.go b/rocketpool-daemon/api/obol/get-charon-health.go new file mode 100644 index 000000000..430249bcf --- /dev/null +++ b/rocketpool-daemon/api/obol/get-charon-health.go @@ -0,0 +1,88 @@ +package obol + +import ( + "fmt" + "net/url" + "net/http" + + "log" + "io" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +// =============== +// === Factory === +// =============== + +type GetCharonHealthContextFactory struct { + handler *ObolHandler +} + +func (f *GetCharonHealthContextFactory) Create(args url.Values) (*GetCharonHealthContext, error) { + c := &GetCharonHealthContext{ + handler: f.handler, + } + return c, nil +} + +func (f *GetCharonHealthContextFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*GetCharonHealthContext, api.GetCharonHealthData]( + router, "obol/get-charon-health", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type GetCharonHealthContext struct { + handler *ObolHandler + rp *rocketpool.RocketPool + + endpoint string +} + +func (c *GetCharonHealthContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + + // Requirements + status, err := sp.RequireNodeRegistered(c.handler.ctx) + if err != nil { + return status, err + } + + return types.ResponseStatus_Success, nil +} + +func (c *GetCharonHealthContext) PrepareData(data *api.GetCharonHealthData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + // The URL for the health check + url := fmt.Sprintf(`http://%s/health`, c.endpoint) + + resp, err := http.Get(url) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error running health check command: %s", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return types.ResponseStatus_Error, fmt.Errorf("Request failed with status code: %s", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %s", err) + } + + fmt.Printf("Response body:\n%s\n", body) + return types.ResponseStatus_Success, nil +} + diff --git a/rocketpool-daemon/api/obol/get-validator-public-keys-handler.go b/rocketpool-daemon/api/obol/get-validator-public-keys-handler.go new file mode 100644 index 000000000..55b59d334 --- /dev/null +++ b/rocketpool-daemon/api/obol/get-validator-public-keys-handler.go @@ -0,0 +1,80 @@ +package obol + +import ( + "fmt" + "net/url" + + "log" + + "os/exec" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +// =============== +// === Factory === +// =============== + +type GetValidatorPublicKeysContextFactory struct { + handler *ObolHandler +} + +func (f *GetValidatorPublicKeysContextFactory) Create(args url.Values) (*GetValidatorPublicKeysContext, error) { + c := &GetValidatorPublicKeysContext{ + handler: f.handler, + } + return c, nil +} + +func (f *GetValidatorPublicKeysContextFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*GetValidatorPublicKeysContext, api.GetValidatorPublicKeysData]( + router, "obol/get-validator-public-keys", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type GetValidatorPublicKeysContext struct { + handler *ObolHandler + rp *rocketpool.RocketPool + + endpoint string +} + +func (c *GetValidatorPublicKeysContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + + // Requirements + status, err := sp.RequireNodeRegistered(c.handler.ctx) + if err != nil { + return status, err + } + + return types.ResponseStatus_Success, nil +} + +func (c *GetValidatorPublicKeysContext) PrepareData(data *api.GetValidatorPublicKeysData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + cmd := exec.Command( + "docker", "exec", "-it", "charon-distributed-validator-node-charon-1", + "/bin/sh", "-c", + fmt.Sprintf(`charon exit active-validator-list --beacon-node-endpoints="%s"`, c.endpoint), + ) + output, err := cmd.CombinedOutput() + + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error running docker command: %s", err) + } + log.Printf("Command output: %s", string(output)) + return types.ResponseStatus_Success, nil +} + diff --git a/rocketpool-daemon/api/obol/handler.go b/rocketpool-daemon/api/obol/handler.go new file mode 100644 index 000000000..1f9c23af5 --- /dev/null +++ b/rocketpool-daemon/api/obol/handler.go @@ -0,0 +1,43 @@ +package obol + +import ( + "context" + + "github.com/gorilla/mux" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/log" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/services" +) + +type ObolHandler struct { + logger *log.Logger + ctx context.Context + serviceProvider *services.ServiceProvider + factories []server.IContextFactory +} + +func NewObolHandler(logger *log.Logger, ctx context.Context, serviceProvider *services.ServiceProvider) *ObolHandler { + h := &ObolHandler{ + logger: logger, + ctx: ctx, + serviceProvider: serviceProvider, + } + h.factories = []server.IContextFactory{ + &CharonDkgContextFactory{h}, + &CreateENRContextFactory{h}, + &DvExitBroadcastContextFactory{h}, + &DvExitSignContextFactory{h}, + &GetCharonHealthContextFactory{h}, + &GetValidatorPublicKeysContextFactory{h}, + &ManageCharonServiceContextFactory{h}, + } + return h +} + +func (h *ObolHandler) RegisterRoutes(router *mux.Router) { + subrouter := router.PathPrefix("/obol").Subrouter() + for _, factory := range h.factories { + factory.RegisterRoute(subrouter) + } +} diff --git a/rocketpool-daemon/api/obol/manage-charon-service.go b/rocketpool-daemon/api/obol/manage-charon-service.go new file mode 100644 index 000000000..85f0c24c3 --- /dev/null +++ b/rocketpool-daemon/api/obol/manage-charon-service.go @@ -0,0 +1,93 @@ +package obol + +import ( + "fmt" + "net/url" + + // "log" + + "os/exec" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/smartnode/v2/shared/types/api" +) + +// =============== +// === Factory === +// =============== + +type ManageCharonServiceContextFactory struct { + handler *ObolHandler +} + +func (f *ManageCharonServiceContextFactory) Create(args url.Values) (*ManageCharonServiceContext, error) { + c := &ManageCharonServiceContext{ + handler: f.handler, + } + return c, nil +} + +func (f *ManageCharonServiceContextFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*ManageCharonServiceContext, api.ManageCharonServiceData]( + router, "obol/manage-charon-service", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type ManageCharonServiceContext struct { + handler *ObolHandler + rp *rocketpool.RocketPool + + serviceName string + action string +} + +func (c *ManageCharonServiceContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + + // Requirements + status, err := sp.RequireNodeRegistered(c.handler.ctx) + if err != nil { + return status, err + } + + // Bindings + + return types.ResponseStatus_Success, nil +} + +func (c *ManageCharonServiceContext) PrepareData(data *api.ManageCharonServiceData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + var cmd *exec.Cmd + + // Choose the appropriate command based on the action + switch c.action { + case "start": + cmd = exec.Command("docker", "start", c.serviceName) + case "stop": + cmd = exec.Command("docker", "stop", c.serviceName) + case "restart": + cmd = exec.Command("docker", "restart", c.serviceName) + default: + return types.ResponseStatus_Error, fmt.Errorf("invalid action: %s. Use start, stop, or restart", c.action) + } + + // Execute the command + output, err := cmd.CombinedOutput() + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("failed to %s service %s: %v, output: %s", c.action, c.serviceName, err, output) + } + + fmt.Printf("Service %s %sed successfully.\n", c.serviceName, c.action) + return types.ResponseStatus_Success, nil +} + diff --git a/shared/types/api/obol.go b/shared/types/api/obol.go new file mode 100644 index 000000000..d9a67098b --- /dev/null +++ b/shared/types/api/obol.go @@ -0,0 +1,34 @@ +package api + + + +type GetValidatorPublicKeysData struct { + endpoint string `json:"endpoint"` +} + +type ManageCharonServiceData struct { + action string `json:"action"` + serviceName string `json:"serviceName"` +} + +type GetCharonHealthData struct { + endpoint string `json:"endpoint"` +} + +type DvExitSignData struct { + endpoint string `json:"endpoint"` +} + +type DvExitBroadcastData struct { + endpoint string `json:"endpoint"` + validatorPublicKey string `json:"validatorPublicKey"` + publishTimeout string `json:"publishTimeout"` +} + +type CharonDkgData struct { + password string `json:"password"` +} + +type CreateENRData struct { + password string `json:"password"` +} \ No newline at end of file