Skip to content

Commit 349d5a6

Browse files
authored
Merge pull request #175 from ChainSafe/tuyen/peer-score-performance
Cache peer score
2 parents 1125869 + 2029715 commit 349d5a6

File tree

5 files changed

+260
-3
lines changed

5 files changed

+260
-3
lines changed

test/accept-from.spec.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const {expect} = require('chai')
2+
const sinon = require('sinon')
3+
const Gossipsub = require('../src')
4+
5+
describe('Gossipsub acceptFrom', () => {
6+
let gossipsub
7+
let sandbox
8+
let scoreSpy
9+
10+
beforeEach(async () => {
11+
sandbox = sinon.createSandbox()
12+
sandbox.useFakeTimers(Date.now())
13+
gossipsub = new Gossipsub({}, { emitSelf: false })
14+
// stubbing PeerScore causes some pending issue in firefox browser environment
15+
// we can only spy it
16+
// using scoreSpy.withArgs("peerA").calledOnce causes the pending issue in firefox
17+
// while spy.getCall() is fine
18+
scoreSpy = sandbox.spy(gossipsub.score, "score")
19+
})
20+
21+
afterEach(() => {
22+
sandbox.restore()
23+
})
24+
25+
it('should only white list peer with positive score', () => {
26+
// by default the score is 0
27+
gossipsub._acceptFrom("peerA")
28+
// 1st time, we have to compute score
29+
expect(scoreSpy.getCall(0).args[0]).to.be.equal("peerA")
30+
expect(scoreSpy.getCall(0).returnValue).to.be.equal(0)
31+
expect(scoreSpy.getCall(1)).to.be.undefined
32+
// 2nd time, use a cached score since it's white listed
33+
gossipsub._acceptFrom("peerA")
34+
expect(scoreSpy.getCall(1)).to.be.undefined
35+
})
36+
37+
it('should recompute score after 1s', () => {
38+
// by default the score is 0
39+
gossipsub._acceptFrom("peerA")
40+
// 1st time, we have to compute score
41+
expect(scoreSpy.getCall(0).args[0]).to.be.equal("peerA")
42+
expect(scoreSpy.getCall(1)).to.be.undefined
43+
gossipsub._acceptFrom("peerA")
44+
// score is cached
45+
expect(scoreSpy.getCall(1)).to.be.undefined
46+
47+
// after 1s
48+
sandbox.clock.tick(1001)
49+
50+
gossipsub._acceptFrom("peerA")
51+
expect(scoreSpy.getCall(1).args[0]).to.be.equal("peerA")
52+
expect(scoreSpy.getCall(2)).to.be.undefined
53+
})
54+
55+
it('should recompute score after max messages accepted', () => {
56+
// by default the score is 0
57+
gossipsub._acceptFrom("peerA")
58+
// 1st time, we have to compute score
59+
expect(scoreSpy.getCall(0).args[0]).to.be.equal("peerA")
60+
expect(scoreSpy.getCall(1)).to.be.undefined
61+
62+
for (let i = 0; i < 128; i++) {
63+
gossipsub._acceptFrom("peerA")
64+
}
65+
expect(scoreSpy.getCall(1)).to.be.undefined
66+
67+
// max messages reached
68+
gossipsub._acceptFrom("peerA")
69+
expect(scoreSpy.getCall(1).args[0]).to.be.equal("peerA")
70+
expect(scoreSpy.getCall(2)).to.be.undefined
71+
})
72+
73+
// TODO: run this in a unit test setup
74+
// this causes the test to not finish in firefox environment
75+
it.skip('should NOT white list peer with negative score', () => {
76+
// peerB is not white listed since score is negative
77+
scoreStub.score.withArgs("peerB").returns(-1)
78+
gossipsub._acceptFrom("peerB")
79+
// 1st time, we have to compute score
80+
expect(scoreStub.score.withArgs("peerB").calledOnce).to.be.true
81+
// 2nd time, still have to compute score since it's NOT white listed
82+
gossipsub._acceptFrom("peerB")
83+
expect(scoreStub.score.withArgs("peerB").calledTwice).to.be.true
84+
})
85+
})

test/peer-score.spec.js

+69-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
const sinon = require('sinon')
12
const { expect } = require('chai')
23
const PeerId = require('peer-id')
34
const delay = require('delay')
45

56
const { PeerScore, createPeerScoreParams, createTopicScoreParams } = require('../src/score')
7+
const computeScoreModule = require('../src/score/compute-score')
68
const { ERR_TOPIC_VALIDATOR_IGNORE, ERR_TOPIC_VALIDATOR_REJECT } = require('../src/constants')
79
const { makeTestMessage, getMsgId } = require('./utils')
810

@@ -642,7 +644,6 @@ describe('PeerScore', () => {
642644
const ps = new PeerScore(params, connectionManager, getMsgId)
643645
ps.addPeer(peerA)
644646
ps.graft(peerA, mytopic)
645-
646647
// score should equal -1000 (app-specific score)
647648
const expected = -1000
648649
ps._refreshScores()
@@ -665,3 +666,70 @@ describe('PeerScore', () => {
665666
expect(aScore).to.equal(0)
666667
})
667668
})
669+
670+
describe('PeerScore score cache', function () {
671+
const peerA = '16Uiu2HAmMkH6ZLen2tbhiuNCTZLLvrZaDgufNdT5MPjtC9Hr9YNG'
672+
let sandbox
673+
let computeStoreStub
674+
const params = createPeerScoreParams({
675+
appSpecificScore: () => -1000,
676+
appSpecificWeight: 1,
677+
retainScore: 800,
678+
decayInterval: 1000,
679+
topics: {a: {topicWeight: 10}}
680+
})
681+
const ps2 = new PeerScore(params, connectionManager, getMsgId)
682+
683+
beforeEach(() => {
684+
sandbox = sinon.createSandbox()
685+
const now = Date.now()
686+
sandbox.useFakeTimers(now)
687+
computeStoreStub = sandbox.stub(computeScoreModule, 'computeScore')
688+
})
689+
690+
afterEach(() => {
691+
sandbox.restore()
692+
})
693+
694+
it('should compute first time', function () {
695+
computeStoreStub.returns(10)
696+
ps2.addPeer(peerA)
697+
expect(computeStoreStub.calledOnce).to.be.false
698+
ps2.score(peerA)
699+
expect(computeStoreStub.calledOnce).to.be.true
700+
// this time peerA score is cached
701+
ps2.score(peerA)
702+
expect(computeStoreStub.calledOnce).to.be.true
703+
})
704+
705+
const testCases = [
706+
{name: 'decayInterval timeout', fun: () => sandbox.clock.tick(params.decayInterval)},
707+
{name: '_refreshScores', fun: () => ps2._refreshScores()},
708+
{name: 'addPenalty', fun: () => ps2.addPenalty(peerA, 10)},
709+
{name: 'graft', fun: () => ps2.graft(peerA, 'a')},
710+
{name: 'prune', fun: () => ps2.prune(peerA, 'a')},
711+
{name: '_markInvalidMessageDelivery', fun: () => ps2._markInvalidMessageDelivery(peerA, {topicIDs: ['a']})},
712+
{name: '_markFirstMessageDelivery', fun: () => ps2._markFirstMessageDelivery(peerA, {topicIDs: ['a']})},
713+
{name: '_markDuplicateMessageDelivery', fun: () => ps2._markDuplicateMessageDelivery(peerA, {topicIDs: ['a']})},
714+
{name: '_setIPs', fun: () => ps2._setIPs(peerA, [], ['127.0.0.1'])},
715+
{name: '_removeIPs', fun: () => ps2._removeIPs(peerA, ['127.0.0.1'])},
716+
{name: '_updateIPs', fun: () => ps2._updateIPs()},
717+
]
718+
719+
for (const {name, fun} of testCases) {
720+
it(`should invalidate the cache after ${name}`, function () {
721+
computeStoreStub.returns(10)
722+
ps2.addPeer(peerA)
723+
ps2.score(peerA)
724+
expect(computeStoreStub.calledOnce).to.be.true
725+
// the score is cached
726+
ps2.score(peerA)
727+
expect(computeStoreStub.calledOnce).to.be.true
728+
// invalidate the cache
729+
fun()
730+
// should not use the cache
731+
ps2.score(peerA)
732+
expect(computeStoreStub.calledTwice).to.be.true
733+
})
734+
}
735+
})

ts/constants.ts

+18
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,21 @@ export const TimeCacheDuration = 120 * 1000
220220

221221
export const ERR_TOPIC_VALIDATOR_REJECT = 'ERR_TOPIC_VALIDATOR_REJECT'
222222
export const ERR_TOPIC_VALIDATOR_IGNORE = 'ERR_TOPIC_VALIDATOR_IGNORE'
223+
224+
/**
225+
* If peer score is better than this, we accept messages from this peer
226+
* within ACCEPT_FROM_WHITELIST_DURATION_MS from the last time computing score.
227+
**/
228+
export const ACCEPT_FROM_WHITELIST_THRESHOLD_SCORE = 0
229+
230+
/**
231+
* If peer score >= ACCEPT_FROM_WHITELIST_THRESHOLD_SCORE, accept up to this
232+
* number of messages from that peer.
233+
*/
234+
export const ACCEPT_FROM_WHITELIST_MAX_MESSAGES = 128
235+
236+
/**
237+
* If peer score >= ACCEPT_FROM_WHITELIST_THRESHOLD_SCORE, accept messages from
238+
* this peer up to this time duration.
239+
*/
240+
export const ACCEPT_FROM_WHITELIST_DURATION_MS = 1000

ts/index.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PeerId = require('peer-id')
1717
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1818
// @ts-ignore
1919
import Envelope = require('libp2p/src/record/envelope')
20+
import { ACCEPT_FROM_WHITELIST_DURATION_MS, ACCEPT_FROM_WHITELIST_MAX_MESSAGES, ACCEPT_FROM_WHITELIST_THRESHOLD_SCORE } from './constants'
2021

2122
interface GossipInputOptions {
2223
emitSelf: boolean
@@ -84,10 +85,18 @@ interface GossipOptions extends GossipInputOptions {
8485
scoreThresholds: PeerScoreThresholds
8586
}
8687

88+
interface AcceptFromWhitelistEntry {
89+
/** number of messages accepted since recomputing the peer's score */
90+
messagesAccepted: number
91+
/** have to recompute score after this time */
92+
acceptUntil: number
93+
}
94+
8795
class Gossipsub extends Pubsub {
8896
peers: Map<string, PeerStreams>
8997
direct: Set<string>
9098
seenCache: SimpleTimeCache
99+
acceptFromWhitelist: Map<string, AcceptFromWhitelistEntry>
91100
topics: Map<string, Set<string>>
92101
mesh: Map<string, Set<string>>
93102
fanout: Map<string, Set<string>>
@@ -182,6 +191,13 @@ class Gossipsub extends Pubsub {
182191
*/
183192
this.direct = new Set(opts.directPeers.map(p => p.id.toB58String()))
184193

194+
/**
195+
* Map of peer id and AcceptRequestWhileListEntry
196+
*
197+
* @type {Map<string, AcceptFromWhitelistEntry}
198+
*/
199+
this.acceptFromWhitelist = new Map()
200+
185201
// set direct peer addresses in the address book
186202
opts.directPeers.forEach(p => {
187203
libp2p.peerStore.addressBook.add(p.id, p.addrs)
@@ -374,6 +390,8 @@ class Gossipsub extends Pubsub {
374390
// Remove from peer scoring
375391
this.score.removePeer(id)
376392

393+
this.acceptFromWhitelist.delete(id)
394+
377395
return peerStreams
378396
}
379397

@@ -449,7 +467,33 @@ class Gossipsub extends Pubsub {
449467
* @returns {boolean}
450468
*/
451469
_acceptFrom (id: string): boolean {
452-
return this.direct.has(id) || this.score.score(id) >= this._options.scoreThresholds.graylistThreshold
470+
if (this.direct.has(id)) {
471+
return true
472+
}
473+
474+
const now = Date.now()
475+
const entry = this.acceptFromWhitelist.get(id)
476+
477+
if (entry &&
478+
entry.messagesAccepted < ACCEPT_FROM_WHITELIST_MAX_MESSAGES &&
479+
entry.acceptUntil >= now) {
480+
entry.messagesAccepted += 1
481+
return true
482+
}
483+
484+
const score = this.score.score(id)
485+
if (score >= ACCEPT_FROM_WHITELIST_THRESHOLD_SCORE) {
486+
// peer is unlikely to be able to drop its score to `graylistThreshold`
487+
// after 128 messages or 1s
488+
this.acceptFromWhitelist.set(id, {
489+
messagesAccepted: 0,
490+
acceptUntil: now + ACCEPT_FROM_WHITELIST_DURATION_MS
491+
})
492+
} else {
493+
this.acceptFromWhitelist.delete(id)
494+
}
495+
496+
return score >= this._options.scoreThresholds.graylistThreshold
453497
}
454498

455499
/**

0 commit comments

Comments
 (0)