diff --git a/cmd/main.go b/cmd/main.go index f8f5c7a..a2a3927 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,17 +9,15 @@ import ( "time" "github.com/dappnode/validator-tracker/internal/adapters/beacon" + "github.com/dappnode/validator-tracker/internal/adapters/brain" "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() @@ -35,7 +33,7 @@ func main() { cfg.Network, cfg.SignerDnpName, ) - web3Signer := web3signer.NewWeb3SignerAdapter(cfg.Web3SignerEndpoint) + brain := brain.NewBrainAdapter(cfg.BrainUrl) // TODO: do not err beacon, err := beacon.NewBeaconAdapter(cfg.BeaconEndpoint) if err != nil { @@ -50,7 +48,7 @@ func main() { // Start the duties checker service in a goroutine dutiesChecker := &services.DutiesChecker{ Beacon: beacon, - Signer: web3Signer, + Brain: brain, Notifier: notifier, Dappmanager: dappmanager, PollInterval: 1 * time.Minute, diff --git a/internal/adapters/brain/brain.go b/internal/adapters/brain/brain.go new file mode 100644 index 0000000..e2facbe --- /dev/null +++ b/internal/adapters/brain/brain.go @@ -0,0 +1,82 @@ +package brain + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/dappnode/validator-tracker/internal/application/ports" +) + +// This adapter is required to be used due to the web3signer blocklisting any host requesting its API that is not whitelisted. +// See https://github.com/dappnode/DAppNodePackage-web3signer-generic/blob/e50e36e6fe213f274cceefc2a089552fa6042be4/services/web3signer/entrypoint.sh#L41C28-L41C42 + +type BrainAdapter struct { + BaseURL string + client *http.Client +} + +type brainValidatorsResponse map[string][]string + +func NewBrainAdapter(baseURL string) ports.BrainAdapter { + // Always append :5000 if not present + u, err := url.Parse(baseURL) + if err == nil && u.Port() == "" { + if u.Scheme == "" { + baseURL = fmt.Sprintf("%s:5000", baseURL) + } else { + u.Host = fmt.Sprintf("%s:5000", u.Host) + baseURL = u.String() + } + } else if err != nil && !strings.HasSuffix(baseURL, ":5000") { + baseURL = fmt.Sprintf("%s:5000", baseURL) + } + return &BrainAdapter{ + BaseURL: baseURL, + client: &http.Client{Timeout: 3 * time.Second}, + } +} + +// GetValidatorPubkeys queries /api/v0/brain/validators?format=pubkey and merges all arrays in the response +func (b *BrainAdapter) GetValidatorPubkeys() ([]string, error) { + endpoint := fmt.Sprintf("%s/api/v0/brain/validators", b.BaseURL) + + u, err := url.Parse(endpoint) + if err != nil { + return nil, fmt.Errorf("invalid brain endpoint: %w", err) + } + q := u.Query() + q.Set("format", "pubkey") + u.RawQuery = q.Encode() + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, fmt.Errorf("creating brain request: %w", err) + } + + resp, err := b.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error sending brain request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected brain status %d: %s", resp.StatusCode, string(body)) + } + + var result brainValidatorsResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("error decoding brain response: %w", err) + } + + var pubkeys []string + for _, arr := range result { + pubkeys = append(pubkeys, arr...) + } + return pubkeys, nil +} diff --git a/internal/adapters/web3signer/web3signer.go b/internal/adapters/web3signer/web3signer.go deleted file mode 100644 index 1e300ad..0000000 --- a/internal/adapters/web3signer/web3signer.go +++ /dev/null @@ -1,59 +0,0 @@ -package web3signer - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/dappnode/validator-tracker/internal/application/ports" -) - -// Web3SignerAdapter implements ports.Web3SignerAdapter -type Web3SignerAdapter struct { - Endpoint string -} - -// KeystoreResponse models the expected JSON from /eth/v1/keystores -type KeystoreResponse struct { - Data []struct { - ValidatingPubkey string `json:"validating_pubkey"` - } `json:"data"` -} - -func NewWeb3SignerAdapter(endpoint string) ports.Web3SignerAdapter { - return &Web3SignerAdapter{Endpoint: endpoint} -} - -func (w *Web3SignerAdapter) GetValidatorPubkeys() ([]string, error) { - url := fmt.Sprintf("%s/eth/v1/keystores", w.Endpoint) - client := &http.Client{Timeout: 30 * time.Second} - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("creating Web3Signer request: %w", err) - } - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending Web3Signer request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("unexpected Web3Signer status %d: %s", resp.StatusCode, string(body)) - } - - var keystoreResp KeystoreResponse - if err := json.NewDecoder(resp.Body).Decode(&keystoreResp); err != nil { - return nil, fmt.Errorf("error decoding Web3Signer response: %w", err) - } - - pubkeys := make([]string, 0, len(keystoreResp.Data)) - for _, item := range keystoreResp.Data { - pubkeys = append(pubkeys, item.ValidatingPubkey) - } - return pubkeys, nil -} diff --git a/internal/application/ports/brain.go b/internal/application/ports/brain.go new file mode 100644 index 0000000..374a7a3 --- /dev/null +++ b/internal/application/ports/brain.go @@ -0,0 +1,6 @@ +package ports + +// BrainAdapter exposes the same method as Web3SignerAdapter for validator pubkeys +type BrainAdapter interface { + GetValidatorPubkeys() ([]string, error) +} diff --git a/internal/application/ports/web3signer.go b/internal/application/ports/web3signer.go deleted file mode 100644 index dfa1822..0000000 --- a/internal/application/ports/web3signer.go +++ /dev/null @@ -1,5 +0,0 @@ -package ports - -type Web3SignerAdapter interface { - GetValidatorPubkeys() ([]string, error) -} diff --git a/internal/application/services/dutieschecker_service.go b/internal/application/services/dutieschecker_service.go index d236067..e403f9b 100644 --- a/internal/application/services/dutieschecker_service.go +++ b/internal/application/services/dutieschecker_service.go @@ -11,7 +11,7 @@ import ( type DutiesChecker struct { Beacon ports.BeaconChainAdapter - Signer ports.Web3SignerAdapter + Brain ports.BrainAdapter Notifier ports.NotifierPort Dappmanager ports.DappManagerPort @@ -59,9 +59,9 @@ func (a *DutiesChecker) performChecks(ctx context.Context, justifiedEpoch domain logger.Warn("Error fetching notifications enabled, notification will not be sent: %v", err) } - pubkeys, err := a.Signer.GetValidatorPubkeys() + pubkeys, err := a.Brain.GetValidatorPubkeys() if err != nil { - logger.Error("Error fetching pubkeys from web3signer: %v", err) + logger.Error("Error fetching pubkeys from brain: %v", err) return err } diff --git a/internal/config/config_loader.go b/internal/config/config_loader.go index ae7c395..880f7f9 100644 --- a/internal/config/config_loader.go +++ b/internal/config/config_loader.go @@ -30,7 +30,7 @@ func LoadConfig() Config { web3SignerEndpoint := fmt.Sprintf("http://web3signer.%s.dncore.dappnode:9000", network) dappmanagerEndpoint := "http://dappmanager.dappnode" notifierEndpoint := "http://notifier.dappnode:8080" - brainEndpoint := fmt.Sprintf("http://brain.web3signer-%s.dappnode:5000", network) + brainEndpoint := fmt.Sprintf("http://brain.web3signer-%s.dappnode", network) // Allow override via environment variables if envBeacon := os.Getenv("BEACON_ENDPOINT"); envBeacon != "" { @@ -58,7 +58,7 @@ func LoadConfig() Config { var dnpName string if network == "mainnet" { dnpName = "web3signer.dnp.dappnode.eth" - brainEndpoint = "http://brain.web3signer.dnp.dappnode.eth:8080" + brainEndpoint = "http://brain.web3signer.dappnode" } else { dnpName = fmt.Sprintf("web3signer-%s.dnp.dappnode.eth", network) }