-
Notifications
You must be signed in to change notification settings - Fork 49
Cache peer score #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cache peer score #175
Changes from all commits
a8a810d
a964fa9
6716d26
53b4e35
e7d49b0
41d12bf
17c8976
aac55a1
f395258
2029715
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
const {expect} = require('chai') | ||
const sinon = require('sinon') | ||
const Gossipsub = require('../src') | ||
|
||
describe('Gossipsub acceptFrom', () => { | ||
let gossipsub | ||
let sandbox | ||
let scoreSpy | ||
|
||
beforeEach(async () => { | ||
sandbox = sinon.createSandbox() | ||
sandbox.useFakeTimers(Date.now()) | ||
gossipsub = new Gossipsub({}, { emitSelf: false }) | ||
// stubbing PeerScore causes some pending issue in firefox browser environment | ||
// we can only spy it | ||
// using scoreSpy.withArgs("peerA").calledOnce causes the pending issue in firefox | ||
// while spy.getCall() is fine | ||
scoreSpy = sandbox.spy(gossipsub.score, "score") | ||
}) | ||
|
||
afterEach(() => { | ||
sandbox.restore() | ||
}) | ||
|
||
it('should only white list peer with positive score', () => { | ||
// by default the score is 0 | ||
gossipsub._acceptFrom("peerA") | ||
// 1st time, we have to compute score | ||
expect(scoreSpy.getCall(0).args[0]).to.be.equal("peerA") | ||
expect(scoreSpy.getCall(0).returnValue).to.be.equal(0) | ||
expect(scoreSpy.getCall(1)).to.be.undefined | ||
// 2nd time, use a cached score since it's white listed | ||
gossipsub._acceptFrom("peerA") | ||
expect(scoreSpy.getCall(1)).to.be.undefined | ||
}) | ||
|
||
it('should recompute score after 1s', () => { | ||
// by default the score is 0 | ||
gossipsub._acceptFrom("peerA") | ||
// 1st time, we have to compute score | ||
expect(scoreSpy.getCall(0).args[0]).to.be.equal("peerA") | ||
expect(scoreSpy.getCall(1)).to.be.undefined | ||
gossipsub._acceptFrom("peerA") | ||
// score is cached | ||
expect(scoreSpy.getCall(1)).to.be.undefined | ||
|
||
// after 1s | ||
sandbox.clock.tick(1001) | ||
|
||
gossipsub._acceptFrom("peerA") | ||
expect(scoreSpy.getCall(1).args[0]).to.be.equal("peerA") | ||
expect(scoreSpy.getCall(2)).to.be.undefined | ||
}) | ||
|
||
it('should recompute score after max messages accepted', () => { | ||
// by default the score is 0 | ||
gossipsub._acceptFrom("peerA") | ||
// 1st time, we have to compute score | ||
expect(scoreSpy.getCall(0).args[0]).to.be.equal("peerA") | ||
expect(scoreSpy.getCall(1)).to.be.undefined | ||
|
||
for (let i = 0; i < 128; i++) { | ||
gossipsub._acceptFrom("peerA") | ||
} | ||
expect(scoreSpy.getCall(1)).to.be.undefined | ||
|
||
// max messages reached | ||
gossipsub._acceptFrom("peerA") | ||
expect(scoreSpy.getCall(1).args[0]).to.be.equal("peerA") | ||
expect(scoreSpy.getCall(2)).to.be.undefined | ||
}) | ||
|
||
// TODO: run this in a unit test setup | ||
// this causes the test to not finish in firefox environment | ||
it.skip('should NOT white list peer with negative score', () => { | ||
// peerB is not white listed since score is negative | ||
scoreStub.score.withArgs("peerB").returns(-1) | ||
gossipsub._acceptFrom("peerB") | ||
// 1st time, we have to compute score | ||
expect(scoreStub.score.withArgs("peerB").calledOnce).to.be.true | ||
// 2nd time, still have to compute score since it's NOT white listed | ||
gossipsub._acceptFrom("peerB") | ||
expect(scoreStub.score.withArgs("peerB").calledTwice).to.be.true | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import PeerId = require('peer-id') | |
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
import Envelope = require('libp2p/src/record/envelope') | ||
import { ACCEPT_FROM_WHITELIST_DURATION_MS, ACCEPT_FROM_WHITELIST_MAX_MESSAGES, ACCEPT_FROM_WHITELIST_THRESHOLD_SCORE } from './constants' | ||
|
||
interface GossipInputOptions { | ||
emitSelf: boolean | ||
|
@@ -84,10 +85,18 @@ interface GossipOptions extends GossipInputOptions { | |
scoreThresholds: PeerScoreThresholds | ||
} | ||
|
||
interface AcceptFromWhitelistEntry { | ||
/** number of messages accepted since recomputing the peer's score */ | ||
messagesAccepted: number | ||
/** have to recompute score after this time */ | ||
acceptUntil: number | ||
} | ||
|
||
class Gossipsub extends Pubsub { | ||
peers: Map<string, PeerStreams> | ||
direct: Set<string> | ||
seenCache: SimpleTimeCache | ||
acceptFromWhitelist: Map<string, AcceptFromWhitelistEntry> | ||
topics: Map<string, Set<string>> | ||
mesh: Map<string, Set<string>> | ||
fanout: Map<string, Set<string>> | ||
|
@@ -182,6 +191,13 @@ class Gossipsub extends Pubsub { | |
*/ | ||
this.direct = new Set(opts.directPeers.map(p => p.id.toB58String())) | ||
|
||
/** | ||
* Map of peer id and AcceptRequestWhileListEntry | ||
* | ||
* @type {Map<string, AcceptFromWhitelistEntry} | ||
*/ | ||
this.acceptFromWhitelist = new Map() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need a place where these entries get routinely pruned so they don't build up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also remove the peer's entry in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @wemeetagain so I removed it in
I don't think it's necessary as we only do the prune in |
||
|
||
// set direct peer addresses in the address book | ||
opts.directPeers.forEach(p => { | ||
libp2p.peerStore.addressBook.add(p.id, p.addrs) | ||
|
@@ -374,6 +390,8 @@ class Gossipsub extends Pubsub { | |
// Remove from peer scoring | ||
this.score.removePeer(id) | ||
|
||
this.acceptFromWhitelist.delete(id) | ||
|
||
return peerStreams | ||
} | ||
|
||
|
@@ -449,7 +467,33 @@ class Gossipsub extends Pubsub { | |
* @returns {boolean} | ||
*/ | ||
_acceptFrom (id: string): boolean { | ||
return this.direct.has(id) || this.score.score(id) >= this._options.scoreThresholds.graylistThreshold | ||
if (this.direct.has(id)) { | ||
return true | ||
} | ||
|
||
const now = Date.now() | ||
const entry = this.acceptFromWhitelist.get(id) | ||
|
||
if (entry && | ||
entry.messagesAccepted < ACCEPT_FROM_WHITELIST_MAX_MESSAGES && | ||
entry.acceptUntil >= now) { | ||
entry.messagesAccepted += 1 | ||
return true | ||
} | ||
|
||
const score = this.score.score(id) | ||
if (score >= ACCEPT_FROM_WHITELIST_THRESHOLD_SCORE) { | ||
// peer is unlikely to be able to drop its score to `graylistThreshold` | ||
// after 128 messages or 1s | ||
this.acceptFromWhitelist.set(id, { | ||
messagesAccepted: 0, | ||
acceptUntil: now + ACCEPT_FROM_WHITELIST_DURATION_MS | ||
}) | ||
} else { | ||
this.acceptFromWhitelist.delete(id) | ||
} | ||
|
||
return score >= this._options.scoreThresholds.graylistThreshold | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's an issue that sinon causes the test to never finish in
firefox
environment. I think current test set up is like an e2e test where we'll run it in different OSs and browsers, and sinon is not expected to run on it, we need a separatetest:unit
where we can stub any functions