diff --git a/cmd/main.go b/cmd/main.go index 124f60a..c48a6b7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,6 +12,7 @@ import ( "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" @@ -46,11 +47,12 @@ func main() { // Start the duties checker service in a goroutine dutiesChecker := &services.DutiesChecker{ - Beacon: beacon, - Signer: web3Signer, - Notifier: notifier, - Dappmanager: dappmanager, - PollInterval: 1 * time.Minute, + Beacon: beacon, + Signer: web3Signer, + Notifier: notifier, + Dappmanager: dappmanager, + PollInterval: 1 * time.Minute, + SlashedNotified: make(map[domain.ValidatorIndex]bool), } wg.Add(1) go func() { diff --git a/internal/adapters/beacon/beacon.go b/internal/adapters/beacon/beacon.go index 4a36207..6e27f0a 100644 --- a/internal/adapters/beacon/beacon.go +++ b/internal/adapters/beacon/beacon.go @@ -178,7 +178,7 @@ func (b *beaconAttestantClient) GetValidatorIndicesByPubkeys(ctx context.Context // Only get validators in active states // TODO: why do I need apiv1 for this struct? is there something newer? validators, err := b.client.Validators(ctx, &api.ValidatorsOpts{ - State: "head", + State: "justified", PubKeys: beaconPubkeys, ValidatorStates: []v1.ValidatorState{ v1.ValidatorStateActiveOngoing, @@ -260,6 +260,28 @@ func (b *beaconAttestantClient) GetValidatorsLiveness(ctx context.Context, epoch return livenessMap, nil } +// GetSlashedValidators retrieves the indices of slashed validators. In the justified state. +func (b *beaconAttestantClient) GetSlashedValidators(ctx context.Context, indices []domain.ValidatorIndex) ([]domain.ValidatorIndex, error) { + slashed, err := b.client.Validators(ctx, &api.ValidatorsOpts{ + State: "justified", + // Only get validators in slashed states + ValidatorStates: []v1.ValidatorState{ + v1.ValidatorStateActiveSlashed, + v1.ValidatorStateExitedSlashed, + }, + Indices: make([]phase0.ValidatorIndex, len(indices)), + }) + + if err != nil { + return nil, err + } + slashedIndices := make([]domain.ValidatorIndex, 0, len(slashed.Data)) + for _, v := range slashed.Data { + slashedIndices = append(slashedIndices, domain.ValidatorIndex(v.Index)) + } + return slashedIndices, nil +} + // enum for consensus client type ConsensusClient string diff --git a/internal/application/ports/beaconchain.go b/internal/application/ports/beaconchain.go index d320388..60bd533 100644 --- a/internal/application/ports/beaconchain.go +++ b/internal/application/ports/beaconchain.go @@ -14,6 +14,7 @@ type BeaconChainAdapter interface { GetCommitteeSizeMap(ctx context.Context, slot domain.Slot) (domain.CommitteeSizeMap, error) GetBlockAttestations(ctx context.Context, slot domain.Slot) ([]domain.Attestation, error) GetValidatorIndicesByPubkeys(ctx context.Context, pubkeys []string) ([]domain.ValidatorIndex, error) + GetSlashedValidators(ctx context.Context, indices []domain.ValidatorIndex) ([]domain.ValidatorIndex, error) GetProposerDuties(ctx context.Context, epoch domain.Epoch, indices []domain.ValidatorIndex) ([]domain.ProposerDuty, error) DidProposeBlock(ctx context.Context, slot domain.Slot) (bool, error) diff --git a/internal/application/services/dutieschecker_service.go b/internal/application/services/dutieschecker_service.go index 1a7b727..4df93e4 100644 --- a/internal/application/services/dutieschecker_service.go +++ b/internal/application/services/dutieschecker_service.go @@ -19,6 +19,8 @@ type DutiesChecker struct { lastJustifiedEpoch domain.Epoch lastLivenessState *bool lastRunHadError bool + + SlashedNotified map[domain.ValidatorIndex]bool } func (a *DutiesChecker) Run(ctx context.Context) { @@ -115,6 +117,27 @@ func (a *DutiesChecker) performChecks(ctx context.Context, justifiedEpoch domain } } + slashed, err := a.Beacon.GetSlashedValidators(ctx, indices) + if err != nil { + logger.Error("Error fetching slashed validators: %v", err) + return err + } + + // Notify about slashed validators only if they haven't been notified before + var toNotify []domain.ValidatorIndex + for _, index := range slashed { + if !a.SlashedNotified[index] { + toNotify = append(toNotify, index) + a.SlashedNotified[index] = true + } + } + + if len(toNotify) > 0 && notificationsEnabled[domain.ValidatorSlashed] { + if err := a.Notifier.SendValidatorsSlashedNot(toNotify); err != nil { + logger.Warn("Error sending validator slashed notification: %v", err) + } + } + return nil }