2021-06-17 15:01:21 +01:00
|
|
|
// Copyright (C) 2021 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package reputation
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
2022-05-04 22:12:01 +01:00
|
|
|
"storj.io/common/pb"
|
2021-06-17 15:01:21 +01:00
|
|
|
"storj.io/common/storj"
|
|
|
|
"storj.io/storj/satellite/overlay"
|
|
|
|
)
|
|
|
|
|
|
|
|
// DB is an interface for storing reputation data.
|
|
|
|
type DB interface {
|
2022-04-20 18:00:25 +01:00
|
|
|
Update(ctx context.Context, request UpdateRequest, now time.Time) (_ *Info, err error)
|
2021-06-17 15:01:21 +01:00
|
|
|
Get(ctx context.Context, nodeID storj.NodeID) (*Info, error)
|
2021-06-23 00:09:39 +01:00
|
|
|
|
|
|
|
// UnsuspendNodeUnknownAudit unsuspends a storage node for unknown audits.
|
2021-07-13 23:27:50 +01:00
|
|
|
UnsuspendNodeUnknownAudit(ctx context.Context, nodeID storj.NodeID) (err error)
|
2021-06-23 00:09:39 +01:00
|
|
|
// DisqualifyNode disqualifies a storage node.
|
2022-04-20 17:59:47 +01:00
|
|
|
DisqualifyNode(ctx context.Context, nodeID storj.NodeID, disqualifiedAt time.Time, reason overlay.DisqualificationReason) (err error)
|
2021-06-23 00:09:39 +01:00
|
|
|
// SuspendNodeUnknownAudit suspends a storage node for unknown audits.
|
2021-07-13 23:27:50 +01:00
|
|
|
SuspendNodeUnknownAudit(ctx context.Context, nodeID storj.NodeID, suspendedAt time.Time) (err error)
|
2021-06-17 15:01:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Info contains all reputation data to be stored in DB.
|
|
|
|
type Info struct {
|
|
|
|
AuditSuccessCount int64
|
|
|
|
TotalAuditCount int64
|
|
|
|
VettedAt *time.Time
|
|
|
|
UnknownAuditSuspended *time.Time
|
|
|
|
OfflineSuspended *time.Time
|
|
|
|
UnderReview *time.Time
|
2021-10-25 21:40:41 +01:00
|
|
|
Disqualified *time.Time
|
2022-04-20 17:59:47 +01:00
|
|
|
DisqualificationReason overlay.DisqualificationReason
|
2021-06-17 15:01:21 +01:00
|
|
|
OnlineScore float64
|
2022-05-04 22:12:01 +01:00
|
|
|
AuditHistory *pb.AuditHistory
|
2021-06-17 15:01:21 +01:00
|
|
|
AuditReputationAlpha float64
|
|
|
|
AuditReputationBeta float64
|
|
|
|
UnknownAuditReputationAlpha float64
|
|
|
|
UnknownAuditReputationBeta float64
|
|
|
|
}
|
|
|
|
|
|
|
|
// Service handles storing node reputation data and updating
|
|
|
|
// the overlay cache when a node's status changes.
|
|
|
|
type Service struct {
|
|
|
|
log *zap.Logger
|
2021-07-13 23:27:50 +01:00
|
|
|
overlay overlay.DB
|
2021-06-17 15:01:21 +01:00
|
|
|
db DB
|
|
|
|
config Config
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewService creates a new reputation service.
|
2021-07-13 23:27:50 +01:00
|
|
|
func NewService(log *zap.Logger, overlay overlay.DB, db DB, config Config) *Service {
|
2021-06-17 15:01:21 +01:00
|
|
|
return &Service{
|
|
|
|
log: log,
|
|
|
|
overlay: overlay,
|
|
|
|
db: db,
|
|
|
|
config: config,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ApplyAudit receives an audit result and applies it to the relevant node in DB.
|
2021-11-08 20:51:04 +00:00
|
|
|
func (service *Service) ApplyAudit(ctx context.Context, nodeID storj.NodeID, reputation overlay.ReputationStatus, result AuditType) (err error) {
|
2021-08-02 18:48:55 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2021-06-17 15:01:21 +01:00
|
|
|
|
2021-11-08 20:51:04 +00:00
|
|
|
now := time.Now()
|
|
|
|
statusUpdate, err := service.db.Update(ctx, UpdateRequest{
|
2021-06-17 15:01:21 +01:00
|
|
|
NodeID: nodeID,
|
|
|
|
AuditOutcome: result,
|
2022-05-07 02:34:56 +01:00
|
|
|
Config: service.config,
|
2021-11-08 20:51:04 +00:00
|
|
|
}, now)
|
2021-06-17 15:01:21 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-11-08 20:51:04 +00:00
|
|
|
// only update node if its health status has changed, or it's a newly vetted
|
|
|
|
// node.
|
|
|
|
// this prevents the need to require caller of ApplyAudit() to always know
|
|
|
|
// the VettedAt time for a node.
|
|
|
|
// Due to inconsistencies in the precision of time.Now() on different platforms and databases, the time comparison
|
|
|
|
// for the VettedAt status is done using time values that are truncated to second precision.
|
|
|
|
if hasReputationChanged(*statusUpdate, reputation, now) {
|
2022-04-20 18:00:25 +01:00
|
|
|
reputationUpdate := &overlay.ReputationUpdate{
|
|
|
|
Disqualified: statusUpdate.Disqualified,
|
|
|
|
DisqualificationReason: statusUpdate.DisqualificationReason,
|
|
|
|
UnknownAuditSuspended: statusUpdate.UnknownAuditSuspended,
|
|
|
|
OfflineSuspended: statusUpdate.OfflineSuspended,
|
|
|
|
VettedAt: statusUpdate.VettedAt,
|
|
|
|
}
|
|
|
|
err = service.overlay.UpdateReputation(ctx, nodeID, *reputationUpdate)
|
2021-06-23 00:09:39 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-06-17 15:01:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get returns a node's reputation info from DB.
|
2021-08-02 18:48:55 +01:00
|
|
|
// If a node is not found in the DB, default reputation information is returned.
|
2021-06-17 15:01:21 +01:00
|
|
|
func (service *Service) Get(ctx context.Context, nodeID storj.NodeID) (info *Info, err error) {
|
2021-08-02 18:48:55 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
info, err = service.db.Get(ctx, nodeID)
|
|
|
|
if err != nil {
|
|
|
|
if ErrNodeNotFound.Has(err) {
|
|
|
|
// if there is no audit reputation for the node, that's fine and we
|
|
|
|
// return default reputation values
|
|
|
|
info = &Info{
|
|
|
|
UnknownAuditReputationAlpha: 1,
|
|
|
|
AuditReputationAlpha: 1,
|
|
|
|
OnlineScore: 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
return info, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return info, nil
|
2021-06-17 15:01:21 +01:00
|
|
|
}
|
|
|
|
|
2021-07-07 20:20:23 +01:00
|
|
|
// TestSuspendNodeUnknownAudit suspends a storage node for unknown audits.
|
|
|
|
func (service *Service) TestSuspendNodeUnknownAudit(ctx context.Context, nodeID storj.NodeID, suspendedAt time.Time) (err error) {
|
2021-07-13 23:27:50 +01:00
|
|
|
err = service.db.SuspendNodeUnknownAudit(ctx, nodeID, suspendedAt)
|
2021-06-23 00:09:39 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-07-15 15:14:13 +01:00
|
|
|
return service.overlay.TestSuspendNodeUnknownAudit(ctx, nodeID, suspendedAt)
|
2021-06-17 15:01:21 +01:00
|
|
|
}
|
2021-06-23 00:09:39 +01:00
|
|
|
|
2021-07-13 23:27:50 +01:00
|
|
|
// TestDisqualifyNode disqualifies a storage node.
|
2021-10-27 11:58:29 +01:00
|
|
|
func (service *Service) TestDisqualifyNode(ctx context.Context, nodeID storj.NodeID, reason overlay.DisqualificationReason) (err error) {
|
|
|
|
disqualifiedAt := time.Now()
|
|
|
|
|
2022-04-20 17:59:47 +01:00
|
|
|
err = service.db.DisqualifyNode(ctx, nodeID, disqualifiedAt, reason)
|
2021-06-23 00:09:39 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-10-27 11:58:29 +01:00
|
|
|
return service.overlay.DisqualifyNode(ctx, nodeID, disqualifiedAt, reason)
|
2021-06-23 00:09:39 +01:00
|
|
|
}
|
|
|
|
|
2021-07-07 20:20:23 +01:00
|
|
|
// TestUnsuspendNodeUnknownAudit unsuspends a storage node for unknown audits.
|
|
|
|
func (service *Service) TestUnsuspendNodeUnknownAudit(ctx context.Context, nodeID storj.NodeID) (err error) {
|
2021-07-13 23:27:50 +01:00
|
|
|
err = service.db.UnsuspendNodeUnknownAudit(ctx, nodeID)
|
2021-06-23 00:09:39 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-07-15 15:14:13 +01:00
|
|
|
return service.overlay.TestUnsuspendNodeUnknownAudit(ctx, nodeID)
|
2021-06-23 00:09:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close closes resources.
|
|
|
|
func (service *Service) Close() error { return nil }
|
2021-11-08 20:51:04 +00:00
|
|
|
|
|
|
|
// hasReputationChanged determines if the current node reputation is different from the newly updated reputation. This
|
|
|
|
// function will only consider the Disqualified, UnknownAudiSuspended and OfflineSuspended statuses for changes.
|
2022-04-20 18:00:25 +01:00
|
|
|
func hasReputationChanged(updated Info, current overlay.ReputationStatus, now time.Time) bool {
|
2021-11-08 20:51:04 +00:00
|
|
|
if statusChanged(current.Disqualified, updated.Disqualified) ||
|
|
|
|
statusChanged(current.UnknownAuditSuspended, updated.UnknownAuditSuspended) ||
|
|
|
|
statusChanged(current.OfflineSuspended, updated.OfflineSuspended) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
// check for newly vetted nodes.
|
|
|
|
// Due to inconsistencies in the precision of time.Now() on different platforms and databases, the time comparison
|
|
|
|
// for the VettedAt status is done using time values that are truncated to second precision.
|
|
|
|
if updated.VettedAt != nil && updated.VettedAt.Truncate(time.Second).Equal(now.Truncate(time.Second)) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// statusChanged determines if the two given statuses are different.
|
|
|
|
func statusChanged(s1, s2 *time.Time) bool {
|
|
|
|
if s1 == nil && s2 == nil {
|
|
|
|
return false
|
|
|
|
} else if s1 != nil && s2 != nil {
|
|
|
|
return !s1.Equal(*s1)
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|