Repository navigation
v2.0.0
Breaking release. A review against go-libp2p-pubsub v0.15.0 found remote denial-of-service holes, a crash, message IDs that did not interoperate with Go, and a peer-scoring model that did not work. 2.0.0 fixes them and aligns the GossipSub router with Go.
Upgrading from 1.x
- Peer scoring moved to the router and is opt-in. Pass
scoreParams:andscoreThresholds:(both, together) toGossipSubRouter.PubSubno longer hasscoreParams,getPeerScore,getPeerScoreObject,peerScoresorrefreshScores; userouter.score. - Message IDs changed to Go's default (
frombytes followed byseqno). Nodes on 1.x and 2.0.0 compute different IDs for the same message, so upgrade a network together. GossipSubParamsuses Go's defaults and validation rules (see Router below).- Peer Exchange is off by default (
GossipSubRouter(doPX: true)). PubSub.stop()no longer disposes the tracer (dispose it yourself), keeps subscriptions, and can be followed bystart().RoutergainedprotocolsandacceptFrom; custom routers must implement them.dart_libp2p>=4.0.1 <5.0.0 is required (was>=1.0.0); this release is tested only against 4.0.1.
Security
- A failed stream open crashed the process. A peer that connected without speaking pubsub made the per-peer stream lock complete with an error nobody listened to, which terminated the isolate. It no longer does.
- Inbound RPC size was unbounded. A peer could announce a huge frame and make the node buffer it. Frames above
maxMessageSize(default 1 MiB,PubSub(maxMessageSize:)) now reset the stream before being read. Outgoing RPCs are split to fit (as Go'sRPC.split), and an oversized message is dropped. - IWANT replies could be amplified. Requested IDs were not de-duplicated and could be requested without limit. Each message is now sent once per IWANT, at most
gossipRetransmission(3) times per peer, and only to peers above the gossip threshold. - A PRUNE with a huge backoff aborted RPC handling. Backoffs are now capped to
maxPruneBackoff(24 h). - Messages were marked seen before their signature was checked, so a forged copy blocked the genuine message and got honest forwarders penalised. As in Go, a message is marked seen once its signature is verified, and messages failing signature checks are not tracked.
- IHAVE handling had no limits.
maxIHaveMessages,maxIHaveLengthand IWANT promises (a peer that does not deliver an advertised message is penalised) now apply, and IHAVEs for topics not joined are ignored. - Graylisted peers (score below
graylistThreshold) have their RPCs ignored (Router.acceptFrom). - The outgoing RPC queue was unbounded and could keep failed RPCs forever. It now holds 32 RPCs per peer with a priority lane, drops RPCs when full (traced as DROP_RPC; dropped GRAFT/PRUNEs are retried with the next RPC), and drops an RPC that fails to send.
- Concurrent writes could interleave frames on a stream; writes are now serialised per stream.
- A message that claims to come from this node but arrives from a peer is rejected.
- The blacklist is enforced, as go-libp2p-pubsub's
WithBlacklistandBlacklistPeer.PubSub(blacklist:)takes aBlacklist(now exported) andPubSub.blacklistPeeradds a peer, removing it from the router if connected. A blacklisted peer is not added or greeted, its RPCs are ignored, and messages it forwards or wrote are dropped before validation (traced asblacklisted peer/blacklisted source) without being marked seen. Before,Blacklistwas not used by anything.
Interop
- Message IDs now match Go. The default ID is the bytes of
fromfollowed byseqno(Go'sDefaultMsgIdFn), andmessageIDsin IHAVE/IWANT/IDONTWANT arebytes(wire-compatible with Go'sstring). IDs are Dart strings with one byte per code unit (messageIdFromBytes,messageIdToBytes).PubSub(messageIdFn:)sets a custom ID function. - Protocols. GossipSub speaks
/meshsub/1.2.0,1.1.0,1.0.0and/floodsub/1.0.0and negotiates one per peer (Router.protocols); features follow the protocol (PX from v1.1, IDONTWANT from v1.2). FloodSub peers get every message of their topics. - IDONTWANT (v1.2) is sent for large messages and honoured.
- Signature policies:
PubSub(signaturePolicy:)takesstrictSign(default),strictNoSign,laxSignorlaxNoSign, andnoAuthoromitsfrom/seqno, as Go'sWithMessageSignaturePolicyandWithNoAuthor. - Heartbeat gossip: the heartbeat now emits IHAVE gossip for recent messages to
max(DLazy, gossipFactor * peers)peers, so lost messages are repaired; IHAVE is no longer sent at publish time. The message cache keepshistoryLengthheartbeats and gossips the lasthistoryGossip, as Go's. - FloodSub and RandomSub work: they register their protocols, keep a seen cache, validate messages, and skip the source and the author. RandomSub sends to
max(RandomSubD, sqrt(networkSize))peers.
Peer scoring (breaking)
- Scoring was rebuilt on go-libp2p-pubsub's
score.go: decaying counters per peer and topic, a score computed on demand from P1–P7, delivery records, IP colocation and the behaviour penalty. Before, the score accumulated on each refresh and one penalty pinned a peer near -1000. - Scoring moved from
PubSubto the router and is opt-in, as in Go:GossipSubRouter(scoreParams:, scoreThresholds:), both required together.PubSubno longer hasscoreParams,getPeerScore,getPeerScoreObject,peerScoresorrefreshScores; userouter.score. PeerScoreParams,TopicScoreParamsandPeerScoreThresholdshave Go's fields andvalidate()rules (topics,topicScoreCap,appSpecificWeight,ipColocationFactorWeight,behaviourPenaltyThreshold,gossipThreshold,publishThreshold,graylistThreshold,acceptPXThreshold,opportunisticGraftThreshold, ...).scoreParameterDecayis Go'sScoreParameterDecay.
Peer Exchange and lifecycle
- The router now connects to the peers offered in a received PRUNE (Peer Exchange), as Go's
pxConnect: when the sender's score is at leastacceptPXThreshold, up toprunePeersof them, throughconnectors(8) concurrent attempts with at mostmaxPendingConnections(128) queued and aconnectionTimeout(30 s). A signed peer record must be about the peer and signed by its key; its addresses are stored in the certified address book. Our PX now carries the signed records the address book has (dart_libp2p 4.0.1's identify does not store remote records, so these are mostly records learned from PX). stop()thenstart()works, and a peer that restarts its pubsub is picked up again:PubSub.stop()removes all peers from the router and closes all pubsub streams; the router forgets its meshes, fanouts, backoffs and queues.start()registers the protocol handlers again, rejoins the subscribed topics and greets the connected peers again. Subscriptions and validators are kept across a restart.stop()no longer disposes the tracer, which it could not reopen (as in Go, the owner of the tracer closes it).- As Go's
handlePeerDead, a peer that ends our stream to it (it stopped its pubsub) is removed from the router; if it is still connected, it is greeted again with Go's dead-peer backoff (at most 4 attempts in 10 minutes), so it is added back when it restarts.
PubSub.stop()could hang forever. Closing a stream waits for its FIN to be sent behind any data queued for a peer, andPubSubProtocol.close()waited for every stream with no limit, so one slow or stalled peer could blockstop()indefinitely (reported on 1.5.0 after a burst of 100 messages). Each stream close is now bounded bystreamCloseTimeout(default 2 s) and the stream is reset when it expires. This applies toclose(),closePeerStreamand the stream cleanup insendRpc, whose wait stalled that peer's send queue.close()now also closes inbound streams, ending their read loops.
Router (breaking)
- The heartbeat follows Go: prunes mesh peers with a negative score, keeps
DOutoutbound peers, keeps theDScorebest peers when pruning, grafts opportunistically only when the mesh median is belowopportunisticGraftThreshold, filters fanout bypublishThreshold, and keeps backoffs two heartbeats past their end. - GRAFTs from peers with a negative score, or from inbound peers when the mesh has
DHighpeers, are refused with a PRUNE. - Flood publish is on by default (
floodPublish), and forwarded messages skip their author. - Peer Exchange is off by default (
GossipSubRouter(doPX: true)). GossipSubParams:DScoreis now a peer count (default 4),DLowdefaults to 5, newDOut,gossipFactor,historyLength,historyGossip,maxIHaveLength,maxIHaveMessages,iwantFollowupTime,opportunisticGraftPeers, IDONTWANT parameters;opportunisticGraftScoreThresholdwas removed;prunePeersdefaults to 16.validate()uses Go's rules.RouterhasprotocolsandacceptFrom;PubSubnow callsRouter.addPeerfor each pubsub peer, sends its hello to every connected peer (including peers connected beforestart()), and announces subscription changes to peers being greeted.- Validation no longer blocks reading a peer's stream.
Also
- The public library exports the routers,
Router,Topic, the protocol IDs, the message ID helpers and the tracers. - README links point to
doc/.
Known limitations
- The PRUNEs we send rarely carry signed peer records: dart_libp2p 4.0.1's identify checks the records of remote peers but does not store them, so we offer mostly records learned through PX. Peer Exchange is tested with mocks, not yet on a real network.
- The tracers do not yet write Go's format (the protobuf tracer frames events with 4 bytes where Go uses a varint; the JSON tracer writes field numbers, no timestamps, and the remote peer as
peerID). - Topic discovery uses the namespace
gossipsub_topic:(Go:floodsub:) and is not wired intoPubSub. - Messages for topics the node neither subscribes to nor relays are still validated.
- GossipSub v1.3 extensions are not supported.