Skip to content

Commit 94c347e

Browse files
Jennifer Johnsonjenlij
authored andcommitted
satellite/reputation: check node age in addition to number of audits to determine vetted status
Change-Id: I139a0093922cf80c19281e34059313eddc476fc2
1 parent 502cede commit 94c347e

File tree

6 files changed

+80
-9
lines changed

6 files changed

+80
-9
lines changed

satellite/reputation/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Config struct {
3333
SuspensionGracePeriod time.Duration `help:"the time period that must pass before suspended nodes will be disqualified" releaseDefault:"168h" devDefault:"1h"`
3434
SuspensionDQEnabled bool `help:"whether nodes will be disqualified if they have been suspended for longer than the suspended grace period" releaseDefault:"false" devDefault:"true"`
3535
AuditCount int64 `help:"the number of times a node has been audited to not be considered a New Node" releaseDefault:"100" devDefault:"0"`
36+
MinimumNodeAge time.Duration `help:"the minimum age a node must have since creation before being vetted" releaseDefault:"504h" devDefault:"0h"`
3637
AuditHistory AuditHistoryConfig
3738
FlushInterval time.Duration `help:"the maximum amount of time that should elapse before cached reputation writes are flushed to the database (if 0, no reputation cache is used)" releaseDefault:"2h" devDefault:"2m"`
3839
ErrorRetryInterval time.Duration `help:"the amount of time that should elapse before the cache retries failed database operations" releaseDefault:"1m" devDefault:"5s"`

satellite/reputation/db_test.go

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,63 @@ func TestUpdate(t *testing.T) {
6969
})
7070
}
7171

72+
func TestUpdateWithMinimumNodeAge(t *testing.T) {
73+
testplanet.Run(t, testplanet.Config{
74+
SatelliteCount: 1, StorageNodeCount: 1,
75+
Reconfigure: testplanet.Reconfigure{
76+
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
77+
config.Reputation.AuditCount = 2
78+
config.Reputation.MinimumNodeAge = 24 * time.Hour // 1 day minimum age
79+
},
80+
},
81+
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
82+
node := planet.StorageNodes[0]
83+
node.Contact.Chore.Pause(ctx)
84+
85+
db := planet.Satellites[0].DB.Reputation()
86+
87+
// Test 1: Node meets audit count but is too young -> should not vet
88+
updateReq := reputation.UpdateRequest{
89+
NodeID: node.ID(),
90+
AuditOutcome: reputation.AuditOffline,
91+
Config: reputation.Config{
92+
AuditCount: planet.Satellites[0].Config.Reputation.AuditCount,
93+
MinimumNodeAge: planet.Satellites[0].Config.Reputation.MinimumNodeAge,
94+
AuditHistory: testAuditHistoryConfig(),
95+
},
96+
}
97+
98+
// First audit - this creates the reputation record
99+
now := time.Now()
100+
nodeStats, err := db.Update(ctx, updateReq, now)
101+
require.NoError(t, err)
102+
assert.Nil(t, nodeStats.VettedAt, "node should not be vetted after 1 audit")
103+
104+
// Get the actual CreatedAt from the database
105+
startTime := nodeStats.CreatedAt
106+
require.NotNil(t, startTime)
107+
108+
// Second audit 1 hr (meets audit count but still too young, needs 24 hours)
109+
secondAuditTime := startTime.Add(time.Hour)
110+
nodeStats, err = db.Update(ctx, updateReq, secondAuditTime)
111+
require.NoError(t, err)
112+
assert.Nil(t, nodeStats.VettedAt, "node should not be vetted yet - too young despite meeting audit count")
113+
114+
// Test 2: Node is now old enough and meets audit count -> should vet
115+
// Simulate time passing by doing an audit 25 hours after node creation
116+
oldEnoughTime := startTime.Add(25 * time.Hour)
117+
nodeStats, err = db.Update(ctx, updateReq, oldEnoughTime)
118+
require.NoError(t, err)
119+
assert.NotNil(t, nodeStats.VettedAt, "node should be vetted now - old enough and meets audit count")
120+
121+
// Test 3: Verify vetted_at timestamp is not overwritten on subsequent audits
122+
nodeStats2, err := db.Update(ctx, updateReq, oldEnoughTime.Add(time.Hour))
123+
require.NoError(t, err)
124+
assert.NotNil(t, nodeStats2.VettedAt)
125+
assert.Equal(t, nodeStats.VettedAt, nodeStats2.VettedAt, "vetted_at should not change on subsequent audits")
126+
})
127+
}
128+
72129
// testApplyUpdatesEquivalentToMultipleUpdates checks that the ApplyUpdates call
73130
// is equivalent to making multiple separate Update() calls (modulo some details
74131
// like exact-time-of-disqualification).
@@ -91,7 +148,7 @@ func testApplyUpdatesEquivalentToMultipleUpdates(ctx context.Context, t *testing
91148
t.Run(testDef.name, func(t *testing.T) {
92149
node1 := testrand.NodeID()
93150
node2 := testrand.NodeID()
94-
startTime := time.Now().Add(-time.Hour)
151+
startTime := time.Now()
95152
var (
96153
info1, info2 *reputation.Info
97154
err error
@@ -190,6 +247,7 @@ func TestApplyUpdatesEquivalentToMultipleUpdates(t *testing.T) {
190247
SuspensionGracePeriod: 20 * time.Minute,
191248
SuspensionDQEnabled: true,
192249
AuditCount: 3,
250+
MinimumNodeAge: 0,
193251
AuditHistory: reputation.AuditHistoryConfig{
194252
WindowSize: 10 * time.Minute,
195253
TrackingPeriod: 1 * time.Hour,
@@ -220,6 +278,7 @@ func TestApplyUpdatesEquivalentToMultipleUpdatesCached(t *testing.T) {
220278
SuspensionGracePeriod: 20 * time.Minute,
221279
SuspensionDQEnabled: true,
222280
AuditCount: 3,
281+
MinimumNodeAge: 0,
223282
AuditHistory: reputation.AuditHistoryConfig{
224283
WindowSize: 10 * time.Minute,
225284
TrackingPeriod: 1 * time.Hour,
@@ -353,7 +412,7 @@ func TestDBDisqualificationNodeOffline(t *testing.T) {
353412
SuspensionDQEnabled: false,
354413
AuditCount: 0,
355414
AuditHistory: reputation.AuditHistoryConfig{
356-
WindowSize: 0,
415+
WindowSize: 1 * time.Second,
357416
TrackingPeriod: 1 * time.Second,
358417
GracePeriod: 0,
359418
OfflineThreshold: 1,

satellite/reputation/service.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type DB interface {
3636
type Info struct {
3737
AuditSuccessCount int64
3838
TotalAuditCount int64
39+
CreatedAt *time.Time
3940
VettedAt *time.Time
4041
UnknownAuditSuspended *time.Time
4142
OfflineSuspended *time.Time
@@ -53,6 +54,7 @@ type Info struct {
5354
// Copy creates a deep copy of the Info object.
5455
func (i *Info) Copy() *Info {
5556
i2 := *i
57+
i2.CreatedAt = cloneTime(i.CreatedAt)
5658
i2.VettedAt = cloneTime(i.VettedAt)
5759
i2.UnknownAuditSuspended = cloneTime(i.UnknownAuditSuspended)
5860
i2.OfflineSuspended = cloneTime(i.OfflineSuspended)

satellite/reputation/writecache.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,15 @@ func (cdb *CachingDB) ApplyUpdates(ctx context.Context, nodeID storj.NodeID, upd
185185
cachedInfo.TotalAuditCount += int64(updates.PositiveResults + updates.FailureResults + updates.OfflineResults + updates.UnknownResults)
186186
cachedInfo.OnlineScore = cachedInfo.AuditHistory.Score
187187

188-
if cachedInfo.VettedAt == nil && cachedInfo.TotalAuditCount >= config.AuditCount {
189-
cachedInfo.VettedAt = &now
190-
// if we think the node is newly vetted, perform a sync to
191-
// have the best chance of propagating that information to
192-
// other satellite services.
193-
doRequestSync = true
188+
if cachedInfo.CreatedAt != nil {
189+
timeSinceCreation := now.Sub(*cachedInfo.CreatedAt)
190+
if cachedInfo.VettedAt == nil && timeSinceCreation >= config.MinimumNodeAge && cachedInfo.TotalAuditCount >= config.AuditCount {
191+
cachedInfo.VettedAt = &now
192+
// if we think the node is newly vetted, perform a sync to
193+
// have the best chance of propagating that information to
194+
// other satellite services.
195+
doRequestSync = true
196+
}
194197
}
195198

196199
// for audit failure, only update normal alpha/beta

satellite/satellite-config.yaml.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,6 +1829,9 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
18291829
# the value to which a beta reputation value should be initialized
18301830
# reputation.initial-beta: 0
18311831

1832+
# the minimum age a node must have since creation before being vetted
1833+
# reputation.minimum-node-age: 504h0m0s
1834+
18321835
# whether nodes will be disqualified if they have been suspended for longer than the suspended grace period
18331836
# reputation.suspension-dq-enabled: false
18341837

satellite/satellitedb/reputations.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ func (reputations *reputations) Get(ctx context.Context, nodeID storj.NodeID) (*
170170
return &reputation.Info{
171171
AuditSuccessCount: res.AuditSuccessCount,
172172
TotalAuditCount: res.TotalAuditCount,
173+
CreatedAt: &res.CreatedAt,
173174
VettedAt: res.VettedAt,
174175
UnknownAuditSuspended: res.UnknownAuditSuspended,
175176
OfflineSuspended: res.OfflineSuspended,
@@ -546,7 +547,8 @@ func (reputations *reputations) populateUpdateNodeStats(dbNode *dbx.Reputation,
546547
OnlineScore: float64Field{set: true, value: historyResponse.NewScore},
547548
}
548549

549-
if vettedAt == nil && updatedTotalAuditCount >= config.AuditCount {
550+
timeSinceCreation := now.Sub(dbNode.CreatedAt)
551+
if vettedAt == nil && timeSinceCreation >= config.MinimumNodeAge && updatedTotalAuditCount >= config.AuditCount {
550552
updateFields.VettedAt = timeField{set: true, value: now}
551553
}
552554

@@ -696,6 +698,7 @@ func dbxToReputationInfo(dbNode *dbx.Reputation) (reputation.Info, error) {
696698
info := reputation.Info{
697699
AuditSuccessCount: dbNode.AuditSuccessCount,
698700
TotalAuditCount: dbNode.TotalAuditCount,
701+
CreatedAt: &dbNode.CreatedAt,
699702
VettedAt: dbNode.VettedAt,
700703
UnknownAuditSuspended: dbNode.UnknownAuditSuspended,
701704
OfflineSuspended: dbNode.OfflineSuspended,

0 commit comments

Comments
 (0)