@helia/bitswap 3.1.2 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bitswap.ts CHANGED
@@ -6,7 +6,7 @@ import { PeerWantLists } from './peer-want-lists/index.js'
6
6
  import { createBitswapSession } from './session.js'
7
7
  import { Stats } from './stats.js'
8
8
  import { WantList } from './want-list.js'
9
- import type { BitswapOptions, Bitswap as BitswapInterface, BitswapWantProgressEvents, BitswapNotifyProgressEvents, WantListEntry, BitswapComponents } from './index.js'
9
+ import type { BitswapOptions, Bitswap as BitswapInterface, BitswapWantProgressEvents, BitswapNotifyProgressEvents, WantListEntry, BitswapComponents, PeerWantListEntry } from './index.js'
10
10
  import type { CreateSessionOptions, ProviderOptions, SessionBlockBroker } from '@helia/interface'
11
11
  import type { ComponentLogger, Libp2p, PeerId, AbortOptions } from '@libp2p/interface'
12
12
  import type { Logger } from '@libp2p/logger'
@@ -127,7 +127,7 @@ export class Bitswap implements BitswapInterface {
127
127
  }))
128
128
  }
129
129
 
130
- getPeerWantlist (peer: PeerId): WantListEntry[] | undefined {
130
+ getPeerWantlist (peer: PeerId): PeerWantListEntry[] | undefined {
131
131
  return this.peerWantLists.wantListForPeer(peer)
132
132
  }
133
133
 
package/src/constants.ts CHANGED
@@ -11,3 +11,5 @@ export const DEFAULT_SESSION_ROOT_PRIORITY = 1
11
11
  export const DEFAULT_MAX_PROVIDERS_PER_REQUEST = 3
12
12
  export const DEFAULT_MAX_OUTGOING_MESSAGE_SIZE = 1024 * 1024 * 4
13
13
  export const DEFAULT_MAX_INCOMING_MESSAGE_SIZE = DEFAULT_MAX_OUTGOING_MESSAGE_SIZE
14
+ export const DEFAULT_DO_NOT_RESEND_BLOCK_WINDOW = 1_000 * 5
15
+ export const DEFAULT_MAX_WANTLIST_SIZE = 1024
package/src/index.ts CHANGED
@@ -35,12 +35,18 @@ export type { BitswapNetworkProgressEvents }
35
35
  export type { WantType }
36
36
  export type { BitswapProvider } from './network.ts'
37
37
 
38
+ export type WantStatus = 'want' | 'sending' | 'sent'
39
+
38
40
  export interface WantListEntry {
39
41
  cid: CID
40
42
  priority: number
41
43
  wantType: WantType
42
44
  }
43
45
 
46
+ export interface PeerWantListEntry extends WantListEntry {
47
+ status: WantStatus
48
+ }
49
+
44
50
  export interface Bitswap extends Startable {
45
51
  /**
46
52
  * Returns the current state of the wantlist
@@ -51,7 +57,7 @@ export interface Bitswap extends Startable {
51
57
  * Returns the current state of the wantlist for a peer, if it is being
52
58
  * tracked
53
59
  */
54
- getPeerWantlist(peerId: PeerId): WantListEntry[] | undefined
60
+ getPeerWantlist(peerId: PeerId): PeerWantListEntry[] | undefined
55
61
 
56
62
  /**
57
63
  * Notify bitswap that a new block is available
@@ -188,6 +194,21 @@ export interface BitswapOptions {
188
194
  * @default 2097152
189
195
  */
190
196
  maxIncomingMessageSize?: number
197
+
198
+ /**
199
+ * If a block has been sent to a peer and it is requested again by the same
200
+ * peer, do not send it again until this many ms have elapsed
201
+ *
202
+ * @default 5000
203
+ */
204
+ doNotResendBlockWindow?: number
205
+
206
+ /**
207
+ * Restrict the local copy of each peer wantlist to this many entries
208
+ *
209
+ * @default 1024
210
+ */
211
+ maxWantlistSize?: number
191
212
  }
192
213
 
193
214
  export const createBitswap = (components: BitswapComponents, options: BitswapOptions = {}): Bitswap => {
@@ -1,9 +1,7 @@
1
1
  import { trackedPeerMap } from '@libp2p/peer-collections'
2
2
  import { CID } from 'multiformats/cid'
3
- import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
- import { WantType } from '../pb/message.js'
5
3
  import { Ledger } from './ledger.js'
6
- import type { BitswapNotifyProgressEvents, WantListEntry } from '../index.js'
4
+ import type { BitswapNotifyProgressEvents, PeerWantListEntry } from '../index.js'
7
5
  import type { Network } from '../network.js'
8
6
  import type { BitswapMessage } from '../pb/message.js'
9
7
  import type { AbortOptions, ComponentLogger, Libp2p, Logger, Metrics, PeerId } from '@libp2p/interface'
@@ -13,6 +11,8 @@ import type { ProgressOptions } from 'progress-events'
13
11
 
14
12
  export interface PeerWantListsInit {
15
13
  maxSizeReplaceHasWithBlock?: number
14
+ doNotResendBlockWindow?: number
15
+ maxWantListSize?: number
16
16
  }
17
17
 
18
18
  export interface PeerWantListsComponents {
@@ -36,6 +36,8 @@ export class PeerWantLists {
36
36
  public network: Network
37
37
  public readonly ledgerMap: PeerMap<Ledger>
38
38
  private readonly maxSizeReplaceHasWithBlock?: number
39
+ private readonly doNotResendBlockWindow?: number
40
+ private readonly maxWantListSize?: number
39
41
  private readonly log: Logger
40
42
  private readonly logger: ComponentLogger
41
43
 
@@ -43,6 +45,8 @@ export class PeerWantLists {
43
45
  this.blockstore = components.blockstore
44
46
  this.network = components.network
45
47
  this.maxSizeReplaceHasWithBlock = init.maxSizeReplaceHasWithBlock
48
+ this.doNotResendBlockWindow = init.doNotResendBlockWindow
49
+ this.maxWantListSize = init.maxWantListSize
46
50
  this.log = components.logger.forComponent('helia:bitswap:peer-want-lists')
47
51
  this.logger = components.logger
48
52
 
@@ -78,14 +82,17 @@ export class PeerWantLists {
78
82
  }
79
83
  }
80
84
 
81
- wantListForPeer (peerId: PeerId): WantListEntry[] | undefined {
85
+ wantListForPeer (peerId: PeerId): PeerWantListEntry[] | undefined {
82
86
  const ledger = this.ledgerMap.get(peerId)
83
87
 
84
88
  if (ledger == null) {
85
89
  return undefined
86
90
  }
87
91
 
88
- return [...ledger.wants.values()]
92
+ // remove any expired wants
93
+ ledger.removeExpiredWants()
94
+
95
+ return ledger.getWants()
89
96
  }
90
97
 
91
98
  peers (): PeerId[] {
@@ -105,7 +112,9 @@ export class PeerWantLists {
105
112
  network: this.network,
106
113
  logger: this.logger
107
114
  }, {
108
- maxSizeReplaceHasWithBlock: this.maxSizeReplaceHasWithBlock
115
+ maxSizeReplaceHasWithBlock: this.maxSizeReplaceHasWithBlock,
116
+ doNotResendBlockWindow: this.doNotResendBlockWindow,
117
+ maxWantListSize: this.maxWantListSize
109
118
  })
110
119
  this.ledgerMap.set(peerId, ledger)
111
120
  }
@@ -113,47 +122,21 @@ export class PeerWantLists {
113
122
  // record the amount of block data received
114
123
  ledger.receivedBytes(message.blocks?.reduce((acc, curr) => acc + curr.data.byteLength, 0) ?? 0)
115
124
 
116
- if (message.wantlist != null) {
117
- // if the message has a full wantlist, clear the current wantlist
118
- if (message.wantlist.full === true) {
119
- ledger.wants.clear()
120
- }
125
+ // remove any expired wants
126
+ ledger.removeExpiredWants()
121
127
 
122
- // clear cancelled wants and add new wants to the ledger
123
- for (const entry of message.wantlist.entries) {
124
- const cid = CID.decode(entry.cid)
125
- const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
126
-
127
- if (entry.cancel === true) {
128
- this.log('peer %p cancelled want of block for %c', peerId, cid)
129
- ledger.wants.delete(cidStr)
130
- } else {
131
- if (entry.wantType === WantType.WantHave) {
132
- this.log('peer %p wanted block presence for %c', peerId, cid)
133
- } else {
134
- this.log('peer %p wanted block for %c', peerId, cid)
135
- }
136
-
137
- ledger.wants.set(cidStr, {
138
- cid,
139
- priority: entry.priority,
140
- wantType: entry.wantType ?? WantType.WantBlock,
141
- sendDontHave: entry.sendDontHave ?? false
142
- })
143
- }
144
- }
145
- }
128
+ // add new wants
129
+ ledger.addWants(message.wantlist)
146
130
 
147
131
  this.log('send blocks to peer')
148
132
  await ledger.sendBlocksToPeer()
149
133
  }
150
134
 
151
135
  async receivedBlock (cid: CID, options: ProgressOptions<BitswapNotifyProgressEvents> & AbortOptions): Promise<void> {
152
- const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
153
136
  const ledgers: Ledger[] = []
154
137
 
155
138
  for (const ledger of this.ledgerMap.values()) {
156
- if (ledger.wants.has(cidStr)) {
139
+ if (ledger.hasWant(cid)) {
157
140
  ledgers.push(ledger)
158
141
  }
159
142
  }
@@ -1,12 +1,14 @@
1
1
  import toBuffer from 'it-to-buffer'
2
- import { DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK } from '../constants.js'
2
+ import { CID } from 'multiformats/cid'
3
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
+ import { DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK, DEFAULT_DO_NOT_RESEND_BLOCK_WINDOW, DEFAULT_MAX_WANTLIST_SIZE } from '../constants.js'
3
5
  import { BlockPresenceType, WantType } from '../pb/message.js'
4
6
  import { QueuedBitswapMessage } from '../utils/bitswap-message.js'
5
7
  import { cidToPrefix } from '../utils/cid-prefix.js'
6
8
  import type { Network } from '../network.js'
9
+ import type { Wantlist } from '../pb/message.js'
7
10
  import type { AbortOptions, ComponentLogger, Logger, PeerId } from '@libp2p/interface'
8
11
  import type { Blockstore } from 'interface-blockstore'
9
- import type { CID } from 'multiformats/cid'
10
12
 
11
13
  export interface LedgerComponents {
12
14
  peerId: PeerId
@@ -17,6 +19,8 @@ export interface LedgerComponents {
17
19
 
18
20
  export interface LedgerInit {
19
21
  maxSizeReplaceHasWithBlock?: number
22
+ doNotResendBlockWindow?: number
23
+ maxWantListSize?: number
20
24
  }
21
25
 
22
26
  export interface PeerWantListEntry {
@@ -45,19 +49,50 @@ export interface PeerWantListEntry {
45
49
  * If we don't have the block and we've told them we don't have the block
46
50
  */
47
51
  sentDoNotHave?: boolean
52
+
53
+ /**
54
+ * If the status is `sending` or `sent`, the block for this CID is or has been
55
+ * sent to the peer so we should not attempt to send it again
56
+ */
57
+ status: 'want' | 'sending' | 'sent'
58
+
59
+ /**
60
+ * A timestamp for when this want should be removed from the list, typically
61
+ * this is set with the `sent` status to prevent sending duplicate blocks to a
62
+ * peer. Once it has expired the peer can request the block a subsequent time.
63
+ */
64
+ expires?: number
65
+
66
+ /**
67
+ * A timestamp of when this entry was created
68
+ */
69
+ created: number
70
+
71
+ /**
72
+ * If this field is false, we have attempted to send this WantList entry but
73
+ * found there is no block for the CID in the blockstore and we are
74
+ * optimistically waiting to see if we come across it later.
75
+ *
76
+ * We only perform the check when we are about to send the block, by which
77
+ * point the entry status is 'sending' so this value with either be false or
78
+ * not set
79
+ */
80
+ haveBlock?: false
48
81
  }
49
82
 
50
83
  export class Ledger {
51
84
  public peerId: PeerId
52
85
  private readonly blockstore: Blockstore
53
86
  private readonly network: Network
54
- public wants: Map<string, PeerWantListEntry>
87
+ private wants: Map<string, PeerWantListEntry>
55
88
  public exchangeCount: number
56
89
  public bytesSent: number
57
90
  public bytesReceived: number
58
91
  public lastExchange?: number
59
92
  private readonly maxSizeReplaceHasWithBlock: number
60
93
  private readonly log: Logger
94
+ private readonly doNotResendBlockWindow: number
95
+ private readonly maxWantListSize: number
61
96
 
62
97
  constructor (components: LedgerComponents, init: LedgerInit) {
63
98
  this.peerId = components.peerId
@@ -70,6 +105,8 @@ export class Ledger {
70
105
  this.bytesSent = 0
71
106
  this.bytesReceived = 0
72
107
  this.maxSizeReplaceHasWithBlock = init.maxSizeReplaceHasWithBlock ?? DEFAULT_MAX_SIZE_REPLACE_HAS_WITH_BLOCK
108
+ this.doNotResendBlockWindow = init.doNotResendBlockWindow ?? DEFAULT_DO_NOT_RESEND_BLOCK_WINDOW
109
+ this.maxWantListSize = init.maxWantListSize ?? DEFAULT_MAX_WANTLIST_SIZE
73
110
  }
74
111
 
75
112
  sentBytes (n: number): void {
@@ -88,18 +125,174 @@ export class Ledger {
88
125
  return (this.bytesSent / (this.bytesReceived + 1)) // +1 is to prevent division by zero
89
126
  }
90
127
 
128
+ removeExpiredWants (): void {
129
+ // remove any expired wants
130
+ this.wants.forEach((value, key) => {
131
+ if (value.expires != null && value.expires < Date.now()) {
132
+ this.wants.delete(key)
133
+ }
134
+ })
135
+ }
136
+
137
+ public addWants (wantlist?: Wantlist): void {
138
+ if (wantlist == null) {
139
+ return
140
+ }
141
+
142
+ // if the message has a full wantlist, remove all entries not currently
143
+ // being sent to the peer
144
+ if (wantlist.full === true) {
145
+ this.wants.forEach((value, key) => {
146
+ if (value.status === 'want') {
147
+ this.wants.delete(key)
148
+ }
149
+ })
150
+ }
151
+
152
+ // clear cancelled wants and add new wants to the ledger
153
+ for (const entry of wantlist.entries) {
154
+ const cid = CID.decode(entry.cid)
155
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
156
+
157
+ if (entry.cancel === true) {
158
+ this.log('peer %p cancelled want of block for %c', this.peerId, cid)
159
+ this.wants.delete(cidStr)
160
+ } else {
161
+ if (entry.wantType === WantType.WantHave) {
162
+ this.log('peer %p wanted block presence for %c', this.peerId, cid)
163
+ } else {
164
+ this.log('peer %p wanted block for %c', this.peerId, cid)
165
+ }
166
+
167
+ const existingWant = this.wants.get(cidStr)
168
+
169
+ // we are already tracking a want for this CID, just update the fields
170
+ if (existingWant != null) {
171
+ const sentOrSending = existingWant.status === 'sent' || existingWant.status === 'sending'
172
+ const wantTypeUpgrade = existingWant.wantType === WantType.WantHave && (entry.wantType == null || entry.wantType === WantType.WantBlock)
173
+
174
+ // allow upgrade from WantHave to WantBlock if we've previously
175
+ // sent or are sending a WantHave
176
+ if (sentOrSending && wantTypeUpgrade) {
177
+ existingWant.status = 'want'
178
+ }
179
+
180
+ existingWant.priority = entry.priority
181
+ existingWant.wantType = entry.wantType ?? WantType.WantBlock
182
+ existingWant.sendDontHave = entry.sendDontHave ?? false
183
+ continue
184
+ }
185
+
186
+ // add a new want
187
+ this.wants.set(cidStr, {
188
+ cid,
189
+ priority: entry.priority,
190
+ wantType: entry.wantType ?? WantType.WantBlock,
191
+ sendDontHave: entry.sendDontHave ?? false,
192
+ status: 'want',
193
+ created: Date.now()
194
+ })
195
+ }
196
+ }
197
+
198
+ // if we have exceeded maxWantListSize, truncate the list - first select
199
+ // wants that are not currently being sent to the user
200
+ const wants = [...this.wants.entries()]
201
+ .filter(([key, entry]) => entry.status === 'want')
202
+
203
+ if (wants.length > this.maxWantListSize) {
204
+ this.truncateWants(wants)
205
+ }
206
+ }
207
+
208
+ private truncateWants (wants: Array<[string, PeerWantListEntry]>): void {
209
+ // sort wants by priority, lack of block presence, then age so the wants
210
+ // to be evicted are a older, low priority wants that we don't have the
211
+ // block for
212
+ wants = wants
213
+ .sort((a, b) => {
214
+ if (a[1].created < b[1].created) {
215
+ return -1
216
+ }
217
+
218
+ if (b[1].created < a[1].created) {
219
+ return 1
220
+ }
221
+
222
+ return 0
223
+ })
224
+ .sort((a, b) => {
225
+ if (a[1].haveBlock === false) {
226
+ return -1
227
+ }
228
+
229
+ if (b[1].haveBlock === false) {
230
+ return 1
231
+ }
232
+
233
+ return 0
234
+ })
235
+ .sort((a, b) => {
236
+ if (a[1].priority < b[1].priority) {
237
+ return -1
238
+ }
239
+
240
+ if (b[1].priority < a[1].priority) {
241
+ return 1
242
+ }
243
+
244
+ return 0
245
+ })
246
+
247
+ const toRemove = wants.length - this.maxWantListSize
248
+
249
+ for (let i = 0; i < toRemove; i++) {
250
+ this.wants.delete(wants[i][0])
251
+ }
252
+ }
253
+
254
+ public getWants (): PeerWantListEntry[] {
255
+ return [...this.wants.values()]
256
+ }
257
+
258
+ public hasWant (cid: CID): boolean {
259
+ const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
260
+
261
+ return this.wants.has(cidStr)
262
+ }
263
+
91
264
  public async sendBlocksToPeer (options?: AbortOptions): Promise<void> {
92
265
  const message = new QueuedBitswapMessage()
93
266
  const sentBlocks = new Set<string>()
94
267
 
95
- for (const [key, entry] of this.wants.entries()) {
268
+ // remove any expired wants
269
+ this.removeExpiredWants()
270
+
271
+ // pick unsent wants
272
+ const unsent = [...this.wants.entries()]
273
+ .filter(([key, value]) => value.status === 'want')
274
+
275
+ // update status, ensure we don't send the same blocks repeatedly
276
+ unsent.forEach(([key, value]) => {
277
+ value.status = 'sending'
278
+ })
279
+
280
+ for (const [key, entry] of unsent) {
96
281
  try {
97
282
  const block = await toBuffer(this.blockstore.get(entry.cid, options))
98
283
 
284
+ // ensure we still need to send the block/status, status may have
285
+ // changed due to incoming message while we were waiting for async block
286
+ // load
287
+ if (entry.status !== 'sending') {
288
+ continue
289
+ }
290
+
99
291
  // do they want the block or just us to tell them we have the block
100
292
  if (entry.wantType === WantType.WantHave) {
101
293
  if (block.byteLength < this.maxSizeReplaceHasWithBlock) {
102
294
  this.log('sending have and block for %c', entry.cid)
295
+
103
296
  // if the block is small we just send it to them
104
297
  sentBlocks.add(key)
105
298
  message.addBlock(entry.cid, {
@@ -123,11 +316,20 @@ export class Ledger {
123
316
  prefix: cidToPrefix(entry.cid)
124
317
  })
125
318
  }
319
+
320
+ entry.status = 'sent'
321
+ entry.expires = Date.now() + this.doNotResendBlockWindow
126
322
  } catch (err: any) {
127
323
  if (err.name !== 'NotFoundError') {
128
324
  throw err
129
325
  }
130
326
 
327
+ // reset status to try again later
328
+ entry.status = 'want'
329
+
330
+ // used to maybe delete this want later if the want list grows too large
331
+ entry.haveBlock = false
332
+
131
333
  this.log('do not have block for %c', entry.cid)
132
334
 
133
335
  // we don't have the requested block and the remote is not interested
@@ -157,12 +359,6 @@ export class Ledger {
157
359
 
158
360
  // update accounting
159
361
  this.sentBytes([...message.blocks.values()].reduce((acc, curr) => acc + curr.data.byteLength, 0))
160
-
161
- // remove sent blocks from local copy of their want list - they can still
162
- // re-request if required
163
- for (const key of sentBlocks) {
164
- this.wants.delete(key)
165
- }
166
362
  }
167
363
  }
168
364
  }