@@ -63,7 +63,7 @@ import { removeFirstNItemsFromSet, removeItemsFromSet } from './utils/set.js'
63
63
import { SimpleTimeCache } from './utils/time-cache.js'
64
64
import type { GossipsubOptsSpec } from './config.js'
65
65
import type {
66
- Connection , Stream , PeerId , Peer , PeerStore ,
66
+ Connection , Direction , Stream , PeerId , Peer , PeerStore ,
67
67
Message ,
68
68
PublishResult ,
69
69
PubSub ,
@@ -189,6 +189,11 @@ export interface GossipsubOpts extends GossipsubOptsSpec, PubSubInit {
189
189
* Limits to bound protobuf decoding
190
190
*/
191
191
decodeRpcLimits ?: DecodeRPCLimits
192
+
193
+ /**
194
+ * If true, will utilize the libp2p connection manager tagging system to prune/graft connections to peers, defaults to true
195
+ */
196
+ tagMeshPeers : boolean
192
197
}
193
198
194
199
export interface GossipsubMessage {
@@ -197,9 +202,17 @@ export interface GossipsubMessage {
197
202
msg : Message
198
203
}
199
204
205
+ export interface MeshPeer {
206
+ peerId : string
207
+ topic : string
208
+ direction : Direction
209
+ }
210
+
200
211
export interface GossipsubEvents extends PubSubEvents {
201
212
'gossipsub:heartbeat' : CustomEvent
202
213
'gossipsub:message' : CustomEvent < GossipsubMessage >
214
+ 'gossipsub:graft' : CustomEvent < MeshPeer >
215
+ 'gossipsub:prune' : CustomEvent < MeshPeer >
203
216
}
204
217
205
218
enum GossipStatusCode {
@@ -408,6 +421,7 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
408
421
fallbackToFloodsub : true ,
409
422
floodPublish : true ,
410
423
batchPublish : false ,
424
+ tagMeshPeers : true ,
411
425
doPX : false ,
412
426
directPeers : [ ] ,
413
427
D : constants . GossipsubD ,
@@ -635,6 +649,11 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
635
649
} )
636
650
} , constants . GossipsubDirectConnectInitialDelay )
637
651
652
+ if ( this . opts . tagMeshPeers ) {
653
+ this . addEventListener ( 'gossipsub:graft' , this . tagMeshPeer )
654
+ this . addEventListener ( 'gossipsub:prune' , this . untagMeshPeer )
655
+ }
656
+
638
657
this . log ( 'started' )
639
658
}
640
659
@@ -652,6 +671,11 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
652
671
const { registrarTopologyIds } = this . status
653
672
this . status = { code : GossipStatusCode . stopped }
654
673
674
+ if ( this . opts . tagMeshPeers ) {
675
+ this . removeEventListener ( 'gossipsub:graft' , this . tagMeshPeer )
676
+ this . removeEventListener ( 'gossipsub:prune' , this . untagMeshPeer )
677
+ }
678
+
655
679
// unregister protocol and handlers
656
680
const registrar = this . components . registrar
657
681
await Promise . all ( this . multicodecs . map ( async ( multicodec ) => registrar . unhandle ( multicodec ) ) )
@@ -1507,6 +1531,7 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
1507
1531
if ( topicID == null ) {
1508
1532
return
1509
1533
}
1534
+
1510
1535
const peersInMesh = this . mesh . get ( topicID )
1511
1536
if ( peersInMesh == null ) {
1512
1537
// don't do PX when there is an unknown topic to avoid leaking our peers
@@ -1520,38 +1545,38 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
1520
1545
return
1521
1546
}
1522
1547
1548
+ const backoffExpiry = this . backoff . get ( topicID ) ?. get ( id )
1549
+
1550
+ // This if/else chain contains the various cases of valid (and semi-valid) GRAFTs
1551
+ // Most of these cases result in a PRUNE immediately being sent in response
1552
+
1523
1553
// we don't GRAFT to/from direct peers; complain loudly if this happens
1524
1554
if ( this . direct . has ( id ) ) {
1525
1555
this . log ( 'GRAFT: ignoring request from direct peer %s' , id )
1526
1556
// this is possibly a bug from a non-reciprical configuration; send a PRUNE
1527
1557
prune . push ( topicID )
1528
1558
// but don't px
1529
1559
doPX = false
1530
- return
1531
- }
1532
1560
1533
- // make sure we are not backing off that peer
1534
- const expire = this . backoff . get ( topicID ) ?. get ( id )
1535
- if ( typeof expire === 'number' && now < expire ) {
1561
+ // make sure we are not backing off that peer
1562
+ } else if ( typeof backoffExpiry === 'number' && now < backoffExpiry ) {
1536
1563
this . log ( 'GRAFT: ignoring backed off peer %s' , id )
1537
1564
// add behavioral penalty
1538
1565
this . score . addPenalty ( id , 1 , ScorePenalty . GraftBackoff )
1539
1566
// no PX
1540
1567
doPX = false
1541
1568
// check the flood cutoff -- is the GRAFT coming too fast?
1542
- const floodCutoff = expire + this . opts . graftFloodThreshold - this . opts . pruneBackoff
1569
+ const floodCutoff = backoffExpiry + this . opts . graftFloodThreshold - this . opts . pruneBackoff
1543
1570
if ( now < floodCutoff ) {
1544
1571
// extra penalty
1545
1572
this . score . addPenalty ( id , 1 , ScorePenalty . GraftBackoff )
1546
1573
}
1547
1574
// refresh the backoff
1548
1575
this . addBackoff ( id , topicID )
1549
1576
prune . push ( topicID )
1550
- return
1551
- }
1552
1577
1553
- // check the score
1554
- if ( score < 0 ) {
1578
+ // check the score
1579
+ } else if ( score < 0 ) {
1555
1580
// we don't GRAFT peers with negative score
1556
1581
this . log ( 'GRAFT: ignoring peer %s with negative score: score=%d, topic=%s' , id , score , topicID )
1557
1582
// we do send them PRUNE however, because it's a matter of protocol correctness
@@ -1560,23 +1585,24 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
1560
1585
doPX = false
1561
1586
// add/refresh backoff so that we don't reGRAFT too early even if the score decays
1562
1587
this . addBackoff ( id , topicID )
1563
- return
1564
- }
1565
1588
1566
- // check the number of mesh peers; if it is at (or over) Dhi, we only accept grafts
1567
- // from peers with outbound connections; this is a defensive check to restrict potential
1568
- // mesh takeover attacks combined with love bombing
1569
- if ( peersInMesh . size >= this . opts . Dhi && ! ( this . outbound . get ( id ) ?? false ) ) {
1589
+ // check the number of mesh peers; if it is at (or over) Dhi, we only accept grafts
1590
+ // from peers with outbound connections; this is a defensive check to restrict potential
1591
+ // mesh takeover attacks combined with love bombing
1592
+ } else if ( peersInMesh . size >= this . opts . Dhi && ! ( this . outbound . get ( id ) ?? false ) ) {
1570
1593
prune . push ( topicID )
1571
1594
this . addBackoff ( id , topicID )
1572
- return
1573
- }
1574
1595
1575
- this . log ( 'GRAFT: Add mesh link from %s in %s' , id , topicID )
1576
- this . score . graft ( id , topicID )
1577
- peersInMesh . add ( id )
1596
+ // valid graft
1597
+ } else {
1598
+ this . log ( 'GRAFT: Add mesh link from %s in %s' , id , topicID )
1599
+ this . score . graft ( id , topicID )
1600
+ peersInMesh . add ( id )
1601
+
1602
+ this . metrics ?. onAddToMesh ( topicID , InclusionReason . Subscribed , 1 )
1603
+ }
1578
1604
1579
- this . metrics ?. onAddToMesh ( topicID , InclusionReason . Subscribed , 1 )
1605
+ this . safeDispatchEvent < MeshPeer > ( 'gossipsub:graft' , { detail : { peerId : id , topic : topicID , direction : 'inbound' } } )
1580
1606
} )
1581
1607
1582
1608
if ( prune . length === 0 ) {
@@ -1627,10 +1653,12 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
1627
1653
score ,
1628
1654
topicID
1629
1655
)
1630
- continue
1656
+ } else {
1657
+ await this . pxConnect ( peers )
1631
1658
}
1632
- await this . pxConnect ( peers )
1633
1659
}
1660
+
1661
+ this . safeDispatchEvent < MeshPeer > ( 'gossipsub:prune' , { detail : { peerId : id , topic : topicID , direction : 'inbound' } } )
1634
1662
}
1635
1663
}
1636
1664
@@ -2325,6 +2353,21 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
2325
2353
2326
2354
this . metrics ?. onRpcSent ( rpc , rpcBytes . length )
2327
2355
2356
+ if ( rpc . control ?. graft != null ) {
2357
+ for ( const topic of rpc . control ?. graft ) {
2358
+ if ( topic . topicID != null ) {
2359
+ this . safeDispatchEvent < MeshPeer > ( 'gossipsub:graft' , { detail : { peerId : id , topic : topic . topicID , direction : 'outbound' } } )
2360
+ }
2361
+ }
2362
+ }
2363
+ if ( rpc . control ?. prune != null ) {
2364
+ for ( const topic of rpc . control ?. prune ) {
2365
+ if ( topic . topicID != null ) {
2366
+ this . safeDispatchEvent < MeshPeer > ( 'gossipsub:prune' , { detail : { peerId : id , topic : topic . topicID , direction : 'outbound' } } )
2367
+ }
2368
+ }
2369
+ }
2370
+
2328
2371
return true
2329
2372
}
2330
2373
@@ -3023,6 +3066,26 @@ export class GossipSub extends TypedEventEmitter<GossipsubEvents> implements Pub
3023
3066
3024
3067
metrics . registerScoreWeights ( sw )
3025
3068
}
3069
+
3070
+ private readonly tagMeshPeer = ( evt : CustomEvent < MeshPeer > ) : void => {
3071
+ const { peerId, topic } = evt . detail
3072
+ this . components . peerStore . merge ( peerIdFromString ( peerId ) , {
3073
+ tags : {
3074
+ [ topic ] : {
3075
+ value : 100
3076
+ }
3077
+ }
3078
+ } ) . catch ( ( err ) => { this . log . error ( 'Error tagging peer %s with topic %s' , peerId , topic , err ) } )
3079
+ }
3080
+
3081
+ private readonly untagMeshPeer = ( evt : CustomEvent < MeshPeer > ) : void => {
3082
+ const { peerId, topic } = evt . detail
3083
+ this . components . peerStore . merge ( peerIdFromString ( peerId ) , {
3084
+ tags : {
3085
+ [ topic ] : undefined
3086
+ }
3087
+ } ) . catch ( ( err ) => { this . log . error ( 'Error untagging peer %s with topic %s' , peerId , topic , err ) } )
3088
+ }
3026
3089
}
3027
3090
3028
3091
export function gossipsub (
0 commit comments