Skip to content
Merged
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
45 changes: 22 additions & 23 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,51 @@ import (
"syscall"
"time"

"github.com/dappnode/validator-tracker/internal/adapters"
"github.com/dappnode/validator-tracker/internal/adapters/beacon"
"github.com/dappnode/validator-tracker/internal/adapters/dappmanager"
"github.com/dappnode/validator-tracker/internal/adapters/notifier"
"github.com/dappnode/validator-tracker/internal/adapters/web3signer"
"github.com/dappnode/validator-tracker/internal/application/domain"
"github.com/dappnode/validator-tracker/internal/application/services"
"github.com/dappnode/validator-tracker/internal/config"
"github.com/dappnode/validator-tracker/internal/logger"
)

//TODO: Implement dev mode with commands example

func main() {
// Load config
cfg := config.LoadConfig()
logger.Info("Loaded config: network=%s, beaconEndpoint=%s, web3SignerEndpoint=%s",
cfg.Network, cfg.BeaconEndpoint, cfg.Web3SignerEndpoint)

// Fetch validator pubkeys
web3Signer := adapters.NewWeb3SignerAdapter(cfg.Web3SignerEndpoint)
pubkeys, err := web3Signer.GetValidatorPubkeys()
if err != nil {
logger.Fatal("Failed to get validator pubkeys from web3signer: %v", err)
}
logger.Info("Fetched %d pubkeys from web3signer", len(pubkeys))

// Initialize beacon chain adapter
adapter, err := adapters.NewBeaconAttestantAdapter(cfg.BeaconEndpoint)
// Initialize adapters
dappmanager := dappmanager.NewDappManagerAdapter(cfg.DappmanagerUrl, cfg.SignerDnpName)
notifier := notifier.NewNotifier(
cfg.NotifierUrl,
cfg.BeaconchaUrl,
cfg.Network,
cfg.SignerDnpName,
)
web3Signer := web3signer.NewWeb3SignerAdapter(cfg.Web3SignerEndpoint)
beacon, err := beacon.NewBeaconAdapter(cfg.BeaconEndpoint)
if err != nil {
logger.Fatal("Failed to initialize beacon adapter: %v", err)
}

// Get validator indices from pubkeys
indices, err := adapter.GetValidatorIndicesByPubkeys(context.Background(), pubkeys)
if err != nil {
logger.Fatal("Failed to get validator indices: %v", err)
}
logger.Info("Found %d validator indices active", len(indices))

// Prepare context and WaitGroup for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup

// Start the duties checker service in a goroutine
logger.Info("Starting duties checker for %d validators", len(indices))
dutiesChecker := &services.DutiesChecker{
BeaconAdapter: adapter,
Web3SignerAdapter: web3Signer,
PollInterval: 1 * time.Minute,
CheckedEpochs: make(map[domain.ValidatorIndex]domain.Epoch),
Beacon: beacon,
Signer: web3Signer,
Notifier: notifier,
Dappmanager: dappmanager,
PollInterval: 1 * time.Minute,
CheckedEpochs: make(map[domain.ValidatorIndex]domain.Epoch),
}
wg.Add(1)
go func() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
// internal/adapters/beaconchain_adapter.go
package adapters
package beacon

import (
"context"
"encoding/hex"
"errors"
"fmt"
nethttp "net/http"
"net/http"
"time"

apiv1 "github.com/attestantio/go-eth2-client/api/v1"
v1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/dappnode/validator-tracker/internal/application/domain"
"github.com/dappnode/validator-tracker/internal/application/ports"
"github.com/rs/zerolog"

"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/http"
_http "github.com/attestantio/go-eth2-client/http"
"github.com/attestantio/go-eth2-client/spec/phase0"
)

// TODO: implement slash method

type beaconAttestantClient struct {
client *http.Service
client *_http.Service
}

func NewBeaconAttestantAdapter(endpoint string) (ports.BeaconChainAdapter, error) {
func NewBeaconAdapter(endpoint string) (ports.BeaconChainAdapter, error) {
zerolog.SetGlobalLevel(zerolog.WarnLevel)

customHttpClient := &nethttp.Client{
customHttpClient := &http.Client{
Timeout: 2000 * time.Second,
}

client, err := http.New(context.Background(),
http.WithAddress(endpoint),
http.WithHTTPClient(customHttpClient),
http.WithTimeout(20*time.Second), // important as attestant API overrides my timeout TODO: investigate how
client, err := _http.New(context.Background(),
_http.WithAddress(endpoint),
_http.WithHTTPClient(customHttpClient),
_http.WithTimeout(20*time.Second), // important as attestant API overrides my timeout TODO: investigate how
)
if err != nil {
return nil, err
}

return &beaconAttestantClient{client: client.(*http.Service)}, nil
return &beaconAttestantClient{client: client.(*_http.Service)}, nil
}

// GetFinalizedEpoch retrieves the latest finalized epoch from the beacon chain.
Expand Down Expand Up @@ -178,10 +179,10 @@ func (b *beaconAttestantClient) GetValidatorIndicesByPubkeys(ctx context.Context
validators, err := b.client.Validators(ctx, &api.ValidatorsOpts{
State: "head",
PubKeys: beaconPubkeys,
ValidatorStates: []apiv1.ValidatorState{
apiv1.ValidatorStateActiveOngoing,
apiv1.ValidatorStateActiveExiting,
apiv1.ValidatorStateActiveSlashed,
ValidatorStates: []v1.ValidatorState{
v1.ValidatorStateActiveOngoing,
v1.ValidatorStateActiveExiting,
v1.ValidatorStateActiveSlashed,
},
})
if err != nil {
Expand Down Expand Up @@ -257,3 +258,38 @@ func (b *beaconAttestantClient) GetValidatorsLiveness(ctx context.Context, epoch
}
return livenessMap, nil
}

// enum for consensus client
type ConsensusClient string

const (
Unknown ConsensusClient = "unknown"
Nimbus ConsensusClient = "nimbus"
Lighthouse ConsensusClient = "lighthouse"
Teku ConsensusClient = "teku"
Prysm ConsensusClient = "prysm"
Lodestar ConsensusClient = "lodestar"
)

// GetConsensusClient see https://ethereum.github.io/beacon-APIs/#/Node/getNodeVersion. Does not throw an error if the client is not available
func (b *beaconAttestantClient) GetConsensusClient(ctx context.Context) ConsensusClient {
resp, err := b.client.NodeClient(ctx)
if err != nil || resp == nil {
return Unknown
}

switch resp.Data {
case "nimbus":
return Nimbus
case "lighthouse":
return Lighthouse
case "teku":
return Teku
case "prysm":
return Prysm
case "lodestar":
return Lodestar
default:
return Unknown
}
}
99 changes: 99 additions & 0 deletions internal/adapters/dappmanager/dappmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package dappmanager

import (
"context"
"encoding/json"
"fmt"
"net/http"

"github.com/dappnode/validator-tracker/internal/application/domain"
)

// DappManagerAdapter is the adapter to interact with the DappManager API
type DappManagerAdapter struct {
baseURL string
signerDnpName string
client *http.Client
}

// NewDappManagerAdapter creates a new DappManagerAdapter
func NewDappManagerAdapter(baseURL string, dnpName string) *DappManagerAdapter {
return &DappManagerAdapter{
baseURL: baseURL,
signerDnpName: dnpName,
client: &http.Client{},
}
}

// Manifest represents the manifest of a package
type manifest struct {
Notifications struct {
CustomEndpoints []CustomEndpoint `json:"customEndpoints"`
} `json:"notifications"`
}

type CustomEndpoint struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Description string `json:"description"`
IsBanner bool `json:"isBanner"`
CorrelationId string `json:"correlationId"`
Metric *struct {
Treshold float64 `json:"treshold"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Unit string `json:"unit"`
} `json:"metric,omitempty"`
}

// GetNotificationsEnabled retrieves the notifications from the DappManager API
func (d *DappManagerAdapter) GetNotificationsEnabled(ctx context.Context) (domain.ValidatorNotificationsEnabled, error) {
customEndpoints, err := d.getSignerManifestNotifications(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get notifications from signer manifest: %w", err)
}

// Build a set of valid correlation IDs from domain.ValidatorNotification
validCorrelationIDs := map[string]struct{}{
string(domain.ValidatorLiveness): {},
string(domain.ValidatorSlashed): {},
string(domain.BlockProposal): {},
}

notifications := make(domain.ValidatorNotificationsEnabled)
for _, endpoint := range customEndpoints {
if _, ok := validCorrelationIDs[endpoint.CorrelationId]; ok {
notifications[domain.ValidatorNotification(endpoint.CorrelationId)] = endpoint.Enabled
}
}

return notifications, nil
}

// getSignerManifestNotifications gets the notifications from the Signer package manifest
func (d *DappManagerAdapter) getSignerManifestNotifications(ctx context.Context) ([]CustomEndpoint, error) {
url := d.baseURL + "/package-manifest/" + d.signerDnpName

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for package %s: %w", d.signerDnpName, err)
}

resp, err := d.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch manifest for package %s: %w", d.signerDnpName, err)
}
defer resp.Body.Close()

// This covers all 2xx status codes. If its not 2xx, we dont bother parsing the manifest and return an error
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected status code %d for package %s", resp.StatusCode, d.signerDnpName)
}

var manifest manifest
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
return nil, fmt.Errorf("failed to decode manifest for package %s: %w", d.signerDnpName, err)
}

return manifest.Notifications.CustomEndpoints, nil
}
Loading