-
Notifications
You must be signed in to change notification settings - Fork 54
feat: add iwant request tracking #107
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
Changes from all commits
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,68 @@ | ||
| const { expect } = require('chai') | ||
| const delay = require('delay') | ||
| const { utils } = require('libp2p-pubsub') | ||
|
|
||
| const { IWantTracer } = require('../src/tracer') | ||
| const constants = require('../src/constants') | ||
| const { makeTestMessage } = require('./utils') | ||
|
|
||
| const getMsgId = (msg) => utils.msgId(msg.from, msg.seqno) | ||
|
|
||
| describe('IWantTracer', () => { | ||
| it('should track broken promises', async function () { | ||
| // tests that unfullfilled promises are tracked correctly | ||
| this.timeout(6000) | ||
| const t = new IWantTracer(getMsgId) | ||
| const peerA = 'A' | ||
| const peerB = 'B' | ||
|
|
||
| const msgIds = [] | ||
| for (let i = 0; i < 100; i++) { | ||
| const m = makeTestMessage(i) | ||
| m.from = Buffer.from(peerA) | ||
| msgIds.push(getMsgId(m)) | ||
| } | ||
|
|
||
| t.addPromise(peerA, msgIds) | ||
| t.addPromise(peerB, msgIds) | ||
|
|
||
| // no broken promises yet | ||
| let brokenPromises = t.getBrokenPromises() | ||
| expect(brokenPromises.size).to.be.equal(0) | ||
|
|
||
| // make promises break | ||
| await delay(constants.GossipsubIWantFollowupTime + 10) | ||
|
|
||
| brokenPromises = t.getBrokenPromises() | ||
| expect(brokenPromises.size).to.be.equal(2) | ||
| expect(brokenPromises.get(peerA)).to.be.equal(1) | ||
| expect(brokenPromises.get(peerB)).to.be.equal(1) | ||
| }) | ||
| it('should track unbroken promises', async function () { | ||
| // like above, but this time we deliver messages to fullfil the promises | ||
| this.timeout(6000) | ||
| const t = new IWantTracer(getMsgId) | ||
| const peerA = 'A' | ||
| const peerB = 'B' | ||
|
|
||
| const msgs = [] | ||
| const msgIds = [] | ||
| for (let i = 0; i < 100; i++) { | ||
| const m = makeTestMessage(i) | ||
| m.from = Buffer.from(peerA) | ||
| msgs.push(m) | ||
| msgIds.push(getMsgId(m)) | ||
| } | ||
|
|
||
| t.addPromise(peerA, msgIds) | ||
| t.addPromise(peerB, msgIds) | ||
|
|
||
| msgs.forEach(msg => t.deliverMessage(peerA, msg)) | ||
|
|
||
| await delay(constants.GossipsubIWantFollowupTime + 10) | ||
|
|
||
| // there should be no broken promises | ||
| const brokenPromises = t.getBrokenPromises() | ||
| expect(brokenPromises.size).to.be.equal(0) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| const makeTestMessage = (i, topicIDs = []) => { | ||
| return { | ||
| seqno: Buffer.alloc(8, i), | ||
| data: Buffer.from([i]), | ||
| from: "test", | ||
| topicIDs | ||
| } | ||
| } | ||
|
|
||
| module.exports.makeTestMessage = makeTestMessage |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { Message } from './message' | ||
| import { GossipsubIWantFollowupTime } from './constants' | ||
|
|
||
| /** | ||
| * IWantTracer is an internal tracer that tracks IWANT requests in order to penalize | ||
| * peers who don't follow up on IWANT requests after an IHAVE advertisement. | ||
| * The tracking of promises is probabilistic to avoid using too much memory. | ||
| * | ||
| * Note: Do not confuse these 'promises' with JS Promise objects. | ||
| * These 'promises' are merely expectations of a peer's behavior. | ||
| */ | ||
| export class IWantTracer { | ||
| getMsgId: (msg: Message) => string | ||
| /** | ||
| * Promises to deliver a message | ||
| * Map per message id, per peer, promise expiration time | ||
| */ | ||
| promises: Map<string, Map<string, number>> | ||
| constructor (getMsgId: (msg: Message) => string) { | ||
| this.getMsgId = getMsgId | ||
| this.promises = new Map() | ||
| } | ||
|
|
||
| /** | ||
| * Track a promise to deliver a message from a list of msgIDs we are requesting | ||
| * @param {string} p peer id | ||
| * @param {string[]} msgIds | ||
| * @returns {void} | ||
| */ | ||
| addPromise (p: string, msgIds: string[]): void { | ||
| // pick msgId randomly from the list | ||
| const ix = Math.floor(Math.random() * msgIds.length) | ||
| const msgId = msgIds[ix] | ||
|
|
||
| let peers = this.promises.get(msgId) | ||
| if (!peers) { | ||
| peers = new Map() | ||
| this.promises.set(msgId, peers) | ||
| } | ||
|
|
||
| if (!peers.has(p)) { | ||
| peers.set(p, Date.now() + GossipsubIWantFollowupTime) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the number of broken promises for each peer who didn't follow up on an IWANT request. | ||
| * @returns {Map<string, number>} | ||
| */ | ||
| getBrokenPromises (): Map<string, number> { | ||
| const now = Date.now() | ||
| const result = new Map<string, number>() | ||
|
|
||
| this.promises.forEach((peers, msgId) => { | ||
| peers.forEach((expire, p) => { | ||
| // the promise has been broken | ||
| if (expire < now) { | ||
| // add 1 to result | ||
| result.set(p, (result.get(p) || 0) + 1) | ||
| // delete from tracked promises | ||
| peers.delete(p) | ||
| } | ||
| }) | ||
| // clean up empty promises for a msgId | ||
| if (!peers.size) { | ||
| this.promises.delete(msgId) | ||
| } | ||
| }) | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| /** | ||
| * Someone delivered a message, stop tracking promises for it | ||
| * @param {string} p peer id | ||
|
Collaborator
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. It seems that we are not using the peerID
Member
Author
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. The idea is to have the same function signature as They have some sort of registration for each of their tracers, then all tracers get called on a single call to
Collaborator
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. ok, sounds good |
||
| * @param {Message} msg | ||
| * @returns {void} | ||
| */ | ||
| deliverMessage (p: string, msg: Message): void { | ||
| const msgId = this.getMsgId(msg) | ||
| this.promises.delete(msgId) | ||
| } | ||
|
|
||
| /** | ||
| * A message got rejected, so we can stop tracking promises and let the score penalty apply from invalid message delivery, | ||
| * unless its an obviously invalid message. | ||
| * @param {string} p peer id | ||
| * @param {Message} msg | ||
| * @returns {void} | ||
| */ | ||
| rejectMessage (p: string, msg: Message): void { | ||
| const msgId = this.getMsgId(msg) | ||
| this.promises.delete(msgId) | ||
| } | ||
|
|
||
| clear (): void { | ||
| this.promises.clear() | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.