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

Add initial nimbus implementation #388

Open
wants to merge 14 commits into
base: develop
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New command `lido-status` to display data of Lido Node Operator.
- Monitoring stack setup with Grafana, Prometheus, and Node Exporter.
- Security policy.
- Support for Nimbus as Consensus and Validator client.

### Changed
- Update Go version from 1.21 to 1.22.
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,12 @@ read more about it in [our documentation](https://docs.sedge.nethermind.io/docs/
### Mainnet

| Execution | Consensus | Validator |
| ---------- | ---------- | ---------- |
| ---------- |------------|------------|
| Geth | Lighthouse | Lighthouse |
| Nethermind | Lodestar | Lodestar |
| Erigon | Prysm | Prysm |
| Besu | Teku | Teku |
| | Nimbus | Nimbus |

### Sepolia

Expand All @@ -155,6 +156,7 @@ read more about it in [our documentation](https://docs.sedge.nethermind.io/docs/
| Nethermind | Lodestar | Lodestar |
| Erigon | Prysm | Prysm |
| Besu | Teku | Teku |
| | Nimbus | Nimbus |

### Holesky

Expand All @@ -164,6 +166,7 @@ read more about it in [our documentation](https://docs.sedge.nethermind.io/docs/
| Nethermind | Lodestar | Lodestar |
| Erigon | Teku | Teku |
| Besu | Prysm | Prysm |
| | Nimbus | Nimbus |

### Gnosis

Expand All @@ -172,23 +175,26 @@ read more about it in [our documentation](https://docs.sedge.nethermind.io/docs/
| Nethermind | Lighthouse | Lighthouse |
| Erigon | Lodestar | Lodestar |
| | Teku | Teku |
| | Nimbus | Nimbus |

### Chiado (Gnosis testnet)

| Execution | Consensus | Validator |
| ------------- | ---------- | ---------- |
|---------------| ---------- | ---------- |
| Nethermind | Lighthouse | Lighthouse |
| Erigon (soon) | Lodestar | Lodestar |
| | Teku | Teku |
| | Nimbus | Nimbus |

### CL clients with Mev-Boost

| Client | Mev-Boost | Networks |
| ---------- | --------- |---------------------------|
| Lighthouse | yes | Mainnet, Sepolia, Holesky |
| Lodestar | yes | Mainnet, Sepolia, Holesky |
| Prysm | yes | Mainnet, Sepolia, Holesky |
| Teku | yes | Mainnet, Sepolia, Holesky |
| Client | Mev-Boost | Networks |
|------------|------------|---------------------------|
| Lighthouse | yes | Mainnet, Sepolia, Holesky |
| Lodestar | yes | Mainnet, Sepolia, Holesky |
| Prysm | yes | Mainnet, Sepolia, Holesky |
| Teku | yes | Mainnet, Sepolia, Holesky |
| Nimbus | yes | Mainnet, Sepolia, Holesky |

## Supported Linux flavours for dependency installation

Expand Down Expand Up @@ -256,9 +262,9 @@ The following roadmap covers the main features and ideas we want to implement bu

- [x] Support Erigon on Gnosis
- [x] Support for Lido CSM
- [x] Support for Nimbus client as Consensus and Validator
- [ ] Include monitoring tool for alerting, tracking validator balance, and tracking sync progress and status of nodes
- [ ] More tests!!!
- [ ] Support for Nimbus client


## 💪 Want to contribute?
Expand Down
6 changes: 5 additions & 1 deletion cli/actions/generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,11 @@ func TestGenerateDockerCompose(t *testing.T) {
}
// Check that Checkpoint Sync URL is set
if tc.genData.CheckpointSyncUrl != "" {
assert.True(t, contains(t, cmpData.Services.Consensus.Command, tc.genData.CheckpointSyncUrl), "Checkpoint Sync URL not found in consensus service command: %s", cmpData.Services.Consensus.Command)
if tc.genData.ConsensusClient != nil && tc.genData.ConsensusClient.Name == "nimbus" {
assert.True(t, contains(t, cmpData.Services.ConsensusSync.Command, tc.genData.CheckpointSyncUrl), "Checkpoint Sync URL not found in consensus service command: %s", cmpData.Services.ConsensusSync.Command)
} else {
assert.True(t, contains(t, cmpData.Services.Consensus.Command, tc.genData.CheckpointSyncUrl), "Checkpoint Sync URL not found in consensus service command: %s", cmpData.Services.Consensus.Command)
}
}

// Check ccImage has the right format
Expand Down
165 changes: 164 additions & 1 deletion cli/actions/importKeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/NethermindEth/sedge/configs"
"github.com/NethermindEth/sedge/internal/images/validator-import/lighthouse"
Expand All @@ -35,6 +37,7 @@ import (
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/otiai10/copy"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
)

var ErrInterrupted = errors.New("interrupt")
Expand Down Expand Up @@ -110,6 +113,12 @@ func (s *sedgeActions) ImportValidatorKeys(options ImportValidatorKeysOptions) e
return err
}
ctID = prysmCtID
case "nimbus":
nimbusCtID, err := setupNimbusValidatorImport(s.dockerClient, s.dockerServiceManager, options)
if err != nil {
return err
}
ctID = nimbusCtID
case "lodestar":
lodestarCtID, err := setupLodestarValidatorImport(s.dockerClient, s.dockerServiceManager, options)
if err != nil {
Expand All @@ -132,7 +141,12 @@ func (s *sedgeActions) ImportValidatorKeys(options ImportValidatorKeysOptions) e
return fmt.Errorf("%w: %s", ErrUnsupportedValidatorClient, options.ValidatorClient)
}
log.Info("Importing validator keys")
runErr := runAndWaitImportKeys(s.dockerClient, s.dockerServiceManager, ctID)
var runErr error
if options.ValidatorClient == "nimbus" {
runErr = runAndWaitImportKeysNimbus(s.dockerClient, s.dockerServiceManager, ctID)
} else {
runErr = runAndWaitImportKeys(s.dockerClient, s.dockerServiceManager, ctID)
}
// Run validator again
if (previouslyRunning && !options.StopValidator) || options.StartValidator {
log.Info("The validator container is being restarted")
Expand Down Expand Up @@ -208,6 +222,69 @@ func setupPrysmValidatorImportContainer(dockerClient client.APIClient, dockerSer
return ct.ID, nil
}

func setupNimbusValidatorImport(dockerClient client.APIClient, dockerServiceManager DockerServiceManager, options ImportValidatorKeysOptions) (string, error) {
var (
// In the case of Nimbus, it's the consensus client the one that import the keys.
consensusCtName = services.ContainerNameWithTag(services.DefaultSedgeConsensusClient, options.ContainerTag)
validatorCtName = services.ContainerNameWithTag(services.DefaultSedgeValidatorClient, options.ContainerTag)
validatorImportCtName = services.ContainerNameWithTag(services.ServiceCtValidatorImport, options.ContainerTag)
)
validatorImage, err := dockerServiceManager.Image(consensusCtName)
if err != nil {
return "", err
}
// Mounts
mounts := []mount.Mount{
{
Type: mount.TypeBind,
Source: options.From,
Target: "/keystore",
},
}
// CMD
cmd := []string{
"deposits",
"import",
"--data-dir=/data",
"--method=single-salt",
"/keystore",
}
// Custom options
if options.CustomConfig.NetworkConfigPath != "" {
mounts = append(mounts, mount.Mount{
Type: mount.TypeBind,
Source: options.CustomConfig.NetworkConfigPath,
Target: "/network_config/config.yml",
})
cmd = append(cmd, "--config-file=/network_config/config.yml")
} else {
cmd = append(cmd, "--network="+options.Network)
}
log.Debugf("Creating %s container", validatorImportCtName)
ct, err := dockerClient.ContainerCreate(context.Background(),
&container.Config{
Image: validatorImage,
Cmd: cmd,
AttachStdin: true,
AttachStderr: true,
AttachStdout: true,
OpenStdin: true,
Tty: true,
},
&container.HostConfig{
Mounts: mounts,
VolumesFrom: []string{consensusCtName, validatorCtName},
},
&network.NetworkingConfig{},
&v1.Platform{},
validatorImportCtName,
)
if err != nil {
return "", err
}
return ct.ID, nil
}

func setupLodestarValidatorImport(dockerClient client.APIClient, dockerServiceManager DockerServiceManager, options ImportValidatorKeysOptions) (string, error) {
var (
validatorCtName = services.ContainerNameWithTag(services.DefaultSedgeValidatorClient, options.ContainerTag)
Expand Down Expand Up @@ -437,3 +514,89 @@ func runAndWaitImportKeys(dockerClient client.APIClient, dockerServiceManager Do
}
}
}

// runAndWaitImportKeysNimbus starts the container in interactive mode and waits for it to finish.
func runAndWaitImportKeysNimbus(dockerClient client.APIClient, dockerServiceManager DockerServiceManager, ctID string) error {
log.Debugf("Starting interactive container with id: %s", ctID)

// Attach to the container's input/output for direct interaction
resp, err := dockerClient.ContainerAttach(context.Background(), ctID, container.AttachOptions{
Stream: true,
Stdin: true,
Stdout: true,
Stderr: true,
Logs: false, // Don't attach previous logs
})
if err != nil {
return err
}
defer resp.Close()

// Put the terminal in raw mode for proper TTY handling
oldState, err := terminal.MakeRaw(int(syscall.Stdin))
if err != nil {
return err
}
defer func() {
// Restore the terminal state immediately when the container finishes
terminal.Restore(int(syscall.Stdin), oldState)
// Clear the line again before printing the final success logs
fmt.Print("\033[2K\r") // Clear the current line in the terminal
}()

// Start the container
if err := dockerClient.ContainerStart(context.Background(), ctID, container.StartOptions{}); err != nil {
return err
}

// Use goroutines to pipe stdin, stdout, and stderr directly to the user's terminal
go func() {
// Pipe container stdout and stderr to the terminal
_, err := io.Copy(os.Stdout, resp.Reader)
if err != nil {
log.Errorf("Error piping container output: %v", err)
}
}()
go func() {
// Pipe terminal input to the container stdin
_, err := io.Copy(resp.Conn, os.Stdin)
if err != nil {
log.Errorf("Error piping user input: %v", err)
}
}()

// Wait for the container to finish execution
ctExit, errChan := dockerServiceManager.Wait(ctID, container.WaitConditionNextExit)

// Handle OS interrupts (e.g., Ctrl+C) to gracefully stop the container
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)

select {
case exitResult := <-ctExit:
if err = deleteContainer(dockerClient, ctID); err != nil {
return err
}
// Wait for the container to exit normally
if exitResult.StatusCode != 0 {
log.Errorf("Container exited with non-zero status: %d", exitResult.StatusCode)
return newValidatorImportCtBadExitCodeError(ctID, exitResult.StatusCode, "Container logs...")
}

case <-osSignals:
// If the user interrupts (e.g., Ctrl+C), stop the container
log.Infof("Received interrupt signal, stopping container %s", ctID)
if err := stopContainer(dockerClient, ctID); err != nil {
log.Errorf("Error stopping container: %v", err)
}
if err = deleteContainer(dockerClient, ctID); err != nil {
return err
}
return ErrInterrupted

case err := <-errChan:
return err
}

return nil
}
39 changes: 32 additions & 7 deletions cli/actions/slashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type SlashingImportOptions struct {

func (s *sedgeActions) ImportSlashingInterchangeData(options SlashingImportOptions) error {
validatorContainerName := services.ContainerNameWithTag(services.DefaultSedgeValidatorClient, options.ContainerTag)
consensusContainerName := services.ContainerNameWithTag(services.DefaultSedgeConsensusClient, options.ContainerTag)
slashingContainerName := services.ContainerNameWithTag(services.ServiceCtSlashingData, options.ContainerTag)
// Check validator container exists
_, err := s.dockerServiceManager.ContainerID(validatorContainerName)
Expand Down Expand Up @@ -105,11 +106,18 @@ func (s *sedgeActions) ImportSlashingInterchangeData(options SlashingImportOptio
"--data-path=/data",
"--from=/data/slashing_protection.json",
}
case "nimbus":
cmd = []string{
"slashingdb",
"import",
"/data/slashing_protection.json",
"--validators-dir=/data",
}
default:
return fmt.Errorf("%w: %s", ErrUnsupportedValidatorClient, options.ValidatorClient)
}
log.Infof("Importing slashing data to client %s from %s", options.ValidatorClient, options.From)
if err := runSlashingContainer(s.dockerClient, s.dockerServiceManager, cmd, validatorContainerName, slashingContainerName); err != nil {
if err := runSlashingContainer(s.dockerClient, s.dockerServiceManager, cmd, validatorContainerName, consensusContainerName, slashingContainerName, options.ValidatorClient == "nimbus"); err != nil {
return err
}

Expand All @@ -136,6 +144,7 @@ type SlashingExportOptions struct {

func (s *sedgeActions) ExportSlashingInterchangeData(options SlashingExportOptions) error {
validatorContainerName := services.ContainerNameWithTag(services.DefaultSedgeValidatorClient, options.ContainerTag)
consensusContainerName := services.ContainerNameWithTag(services.DefaultSedgeConsensusClient, options.ContainerTag)
slashingContainerName := services.ContainerNameWithTag(services.ServiceCtSlashingData, options.ContainerTag)
// Check validator container exists
_, err := s.dockerServiceManager.ContainerID(validatorContainerName)
Expand Down Expand Up @@ -187,11 +196,18 @@ func (s *sedgeActions) ExportSlashingInterchangeData(options SlashingExportOptio
"--data-path=/data",
"--to=/data/slashing_protection.json",
}
case "nimbus":
cmd = []string{
"slashingdb",
"export",
"/data/slashing_protection.json",
"--validators-dir=/data",
}
default:
return fmt.Errorf("%w: %s", ErrUnsupportedValidatorClient, options.ValidatorClient)
}
log.Infof("Exporting slashing data from client %s", options.ValidatorClient)
if err := runSlashingContainer(s.dockerClient, s.dockerServiceManager, cmd, validatorContainerName, slashingContainerName); err != nil {
if err := runSlashingContainer(s.dockerClient, s.dockerServiceManager, cmd, validatorContainerName, consensusContainerName, slashingContainerName, options.ValidatorClient == "nimbus"); err != nil {
return err
}
copyFrom := filepath.Join(options.GenerationPath, configs.ValidatorDir, "slashing_protection.json")
Expand All @@ -212,16 +228,25 @@ func (s *sedgeActions) ExportSlashingInterchangeData(options SlashingExportOptio
}

func runSlashingContainer(dockerClient client.APIClient, dockerServiceManager DockerServiceManager, cmd []string,
validatorContainerName string, slashingContainerName string,
validatorContainerName string, consensusContainerName string, slashingContainerName string, isNimbus bool,
) error {
validatorImage, err := dockerServiceManager.Image(validatorContainerName)
if err != nil {
return err
slashingImage := ""
var err error
if isNimbus {
slashingImage, err = dockerServiceManager.Image(consensusContainerName)
if err != nil {
return err
}
} else {
slashingImage, err = dockerServiceManager.Image(validatorContainerName)
if err != nil {
return err
}
}
log.Debugf("Creating %s container", services.ServiceCtSlashingData)
ct, err := dockerClient.ContainerCreate(context.Background(),
&container.Config{
Image: validatorImage,
Image: slashingImage,
Cmd: cmd,
},
&container.HostConfig{
Expand Down
Loading
Loading