@@ -86,13 +86,10 @@ describe('gossip', () => {
8686 } )
8787
8888 it ( 'should send idontwant to peers in topic' , async function ( ) {
89- // This test checks that idontwants and idontwantsCounts are correctly incrmemented
90- // - idontwantCounts should track the number of idontwant messages received from a peer for a single heartbeat
91- // - it should increment on receive of idontwant msgs (up to limit)
92- // - it should be emptied after heartbeat
93- // - idontwants should track the idontwant messages received from a peer along with the heartbeatId when received
94- // - it should increment on receive of idontwant msgs (up to limit)
95- // - it should be emptied after mcacheLength heartbeats
89+ // This integration test checks IDONTWANT lifecycle behavior under network traffic:
90+ // - publishing messages in a connected topic causes peers to track IDONTWANT state
91+ // - retained idontwants stay bounded while entries are tracked across heartbeats
92+ // - idontwantCounts are cleared at the next heartbeat
9693 this . timeout ( 10e4 )
9794 const nodeA = nodes [ 0 ]
9895 const otherNodes = nodes . slice ( 1 )
@@ -121,70 +118,99 @@ describe('gossip', () => {
121118 ] )
122119 await nodeA . pubsub . publish ( topic , msg )
123120 }
124- // track the heartbeat when each node received the last message
125-
126- const ticks = otherNodes . map ( ( n ) => n . pubsub [ 'heartbeatTicks' ] )
127-
128- // there's no event currently implemented to await, so just wait a bit - flaky :(
129- // TODO figure out something more robust
130- await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) )
121+ // wait for one heartbeat so IDONTWANT handling has happened on all peers
122+ await Promise . all ( otherNodes . map ( async ( n ) => pEvent ( n . pubsub , 'gossipsub:heartbeat' ) ) )
131123
132- // other nodes should have received idontwant messages
133- // check that idontwants <= GossipsubIdontwantMaxMessages
124+ // other nodes should have tracked idontwant messages
125+ // check retained idontwants are bounded over mcacheLength heartbeats
134126 for ( let i = 0 ; i < otherNodes . length ; i ++ ) {
135127 const node = otherNodes [ i ]
136128
137- const currentTick = node . pubsub [ 'heartbeatTicks' ]
138-
139- const idontwantCounts = node . pubsub [ 'idontwantCounts' ]
140- let minCount = Infinity
141- let maxCount = 0
142- for ( const count of idontwantCounts . values ( ) ) {
143- minCount = Math . min ( minCount , count )
144- maxCount = Math . max ( maxCount , count )
145- }
146- // expect(minCount).to.be.greaterThan(0)
147- expect ( maxCount ) . to . be . lessThanOrEqual ( idontwantMaxMessages )
148-
149129 const idontwants = node . pubsub [ 'idontwants' ]
150- let minIdontwants = Infinity
151130 let maxIdontwants = 0
152131 for ( const idontwant of idontwants . values ( ) ) {
153- minIdontwants = Math . min ( minIdontwants , idontwant . size )
154132 maxIdontwants = Math . max ( maxIdontwants , idontwant . size )
155133 }
156- // expect(minIdontwants).to.be.greaterThan(0)
157- expect ( maxIdontwants ) . to . be . lessThanOrEqual ( idontwantMaxMessages )
158-
159- // sanity check that the idontwantCount matches idontwants.size
160- // only the case if there hasn't been a heartbeat
161- if ( currentTick === ticks [ i ] ) {
162- expect ( minCount ) . to . be . equal ( minIdontwants )
163- expect ( maxCount ) . to . be . equal ( maxIdontwants )
164- }
134+
135+ expect ( maxIdontwants ) . to . be . lessThanOrEqual ( idontwantMaxMessages * node . pubsub . opts . mcacheLength )
165136 }
166137
167138 await Promise . all ( otherNodes . map ( async ( n ) => pEvent ( n . pubsub , 'gossipsub:heartbeat' ) ) )
168139
169140 // after a heartbeat
170141 // idontwants are still tracked
171142 // but idontwantCounts have been cleared
172- for ( const node of nodes ) {
143+ for ( const node of otherNodes ) {
173144 const idontwantCounts = node . pubsub [ 'idontwantCounts' ]
174- for ( const count of idontwantCounts . values ( ) ) {
175- expect ( count ) . to . be . equal ( 0 )
176- }
145+ expect ( idontwantCounts . size ) . to . equal ( 0 )
177146
178147 const idontwants = node . pubsub [ 'idontwants' ]
179- let minIdontwants = Infinity
180148 let maxIdontwants = 0
181149 for ( const idontwant of idontwants . values ( ) ) {
182- minIdontwants = Math . min ( minIdontwants , idontwant . size )
183150 maxIdontwants = Math . max ( maxIdontwants , idontwant . size )
184151 }
185- // expect(minIdontwants).to.be.greaterThan(0)
186- expect ( maxIdontwants ) . to . be . lessThanOrEqual ( idontwantMaxMessages )
152+ expect ( maxIdontwants ) . to . be . lessThanOrEqual ( idontwantMaxMessages * node . pubsub . opts . mcacheLength )
153+ }
154+ } )
155+
156+ it ( 'should cap idontwant tracking per peer per heartbeat' , async function ( ) {
157+ // `should send idontwant to peers in topic` exercises this path indirectly, this
158+ // test verifies the cap deterministically with controlled input and exact assertions.
159+ // This test directly exercises handleIdontwant to verify per-heartbeat cap semantics:
160+ // - idontwantCounts and idontwants stop growing at idontwantMaxMessages
161+ // - counts reset on heartbeat and start again next heartbeat
162+ const nodeA = nodes [ 0 ]
163+ const pubsub = nodeA . pubsub as unknown as Partial < GossipSubClass > & {
164+ handleIdontwant : GossipSubClass [ 'handleIdontwant' ]
165+ idontwantCounts : Map < string , number >
166+ idontwants : Map < string , Map < string , number > >
187167 }
168+ const peerId = 'peer-a'
169+ const idontwantMaxMessages = nodeA . pubsub . opts . idontwantMaxMessages
170+
171+ pubsub . handleIdontwant ( peerId , [ {
172+ messageIDs : Array . from ( { length : idontwantMaxMessages * 2 } , ( _ , i ) => uint8ArrayFromString ( `msg-${ i } ` ) )
173+ } ] )
174+
175+ expect ( pubsub . idontwantCounts . get ( peerId ) ) . to . equal ( idontwantMaxMessages )
176+ expect ( pubsub . idontwants . get ( peerId ) ?. size ) . to . equal ( idontwantMaxMessages )
177+
178+ pubsub . handleIdontwant ( peerId , [ { messageIDs : [ uint8ArrayFromString ( 'overflow' ) ] } ] )
179+
180+ expect ( pubsub . idontwantCounts . get ( peerId ) ) . to . equal ( idontwantMaxMessages )
181+ expect ( pubsub . idontwants . get ( peerId ) ?. size ) . to . equal ( idontwantMaxMessages )
182+
183+ await nodeA . pubsub . heartbeat ( )
184+
185+ expect ( pubsub . idontwantCounts . get ( peerId ) ) . to . equal ( undefined )
186+
187+ pubsub . handleIdontwant ( peerId , [ { messageIDs : [ uint8ArrayFromString ( 'next-heartbeat' ) ] } ] )
188+
189+ expect ( pubsub . idontwantCounts . get ( peerId ) ) . to . equal ( 1 )
190+ } )
191+
192+ it ( 'should prune tracked idontwants after mcacheLength heartbeats' , async function ( ) {
193+ const nodeA = nodes [ 0 ]
194+ const pubsub = nodeA . pubsub as unknown as Partial < GossipSubClass > & {
195+ handleIdontwant : GossipSubClass [ 'handleIdontwant' ]
196+ idontwants : Map < string , Map < string , number > >
197+ }
198+ const peerId = 'peer-b'
199+ const mcacheLength = nodeA . pubsub . opts . mcacheLength
200+
201+ pubsub . handleIdontwant ( peerId , [ { messageIDs : [ uint8ArrayFromString ( 'msg-to-prune' ) ] } ] )
202+ expect ( pubsub . idontwants . get ( peerId ) ?. size ) . to . equal ( 1 )
203+
204+ for ( let i = 0 ; i < mcacheLength - 1 ; i ++ ) {
205+ await nodeA . pubsub . heartbeat ( )
206+ }
207+
208+ if ( mcacheLength > 1 ) {
209+ expect ( pubsub . idontwants . get ( peerId ) ?. size ) . to . equal ( 1 )
210+ }
211+
212+ await nodeA . pubsub . heartbeat ( )
213+ expect ( pubsub . idontwants . get ( peerId ) ?. size ) . to . equal ( 0 )
188214 } )
189215
190216 it ( 'Should allow publishing to zero peers if flag is passed' , async function ( ) {
0 commit comments