@libp2p/circuit-relay-v2 0.0.0-05b52d69c

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.
Files changed (65) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +69 -0
  3. package/dist/index.min.js +45 -0
  4. package/dist/src/constants.d.ts +55 -0
  5. package/dist/src/constants.d.ts.map +1 -0
  6. package/dist/src/constants.js +61 -0
  7. package/dist/src/constants.js.map +1 -0
  8. package/dist/src/index.d.ts +56 -0
  9. package/dist/src/index.d.ts.map +1 -0
  10. package/dist/src/index.js +39 -0
  11. package/dist/src/index.js.map +1 -0
  12. package/dist/src/pb/index.d.ts +93 -0
  13. package/dist/src/pb/index.d.ts.map +1 -0
  14. package/dist/src/pb/index.js +425 -0
  15. package/dist/src/pb/index.js.map +1 -0
  16. package/dist/src/server/advert-service.d.ts +46 -0
  17. package/dist/src/server/advert-service.d.ts.map +1 -0
  18. package/dist/src/server/advert-service.js +72 -0
  19. package/dist/src/server/advert-service.js.map +1 -0
  20. package/dist/src/server/index.d.ts +67 -0
  21. package/dist/src/server/index.d.ts.map +1 -0
  22. package/dist/src/server/index.js +313 -0
  23. package/dist/src/server/index.js.map +1 -0
  24. package/dist/src/server/reservation-store.d.ts +49 -0
  25. package/dist/src/server/reservation-store.d.ts.map +1 -0
  26. package/dist/src/server/reservation-store.js +65 -0
  27. package/dist/src/server/reservation-store.js.map +1 -0
  28. package/dist/src/server/reservation-voucher.d.ts +18 -0
  29. package/dist/src/server/reservation-voucher.d.ts.map +1 -0
  30. package/dist/src/server/reservation-voucher.js +36 -0
  31. package/dist/src/server/reservation-voucher.js.map +1 -0
  32. package/dist/src/transport/discovery.d.ts +48 -0
  33. package/dist/src/transport/discovery.d.ts.map +1 -0
  34. package/dist/src/transport/discovery.js +97 -0
  35. package/dist/src/transport/discovery.js.map +1 -0
  36. package/dist/src/transport/index.d.ts +58 -0
  37. package/dist/src/transport/index.d.ts.map +1 -0
  38. package/dist/src/transport/index.js +279 -0
  39. package/dist/src/transport/index.js.map +1 -0
  40. package/dist/src/transport/listener.d.ts +11 -0
  41. package/dist/src/transport/listener.d.ts.map +1 -0
  42. package/dist/src/transport/listener.js +66 -0
  43. package/dist/src/transport/listener.js.map +1 -0
  44. package/dist/src/transport/reservation-store.d.ts +74 -0
  45. package/dist/src/transport/reservation-store.d.ts.map +1 -0
  46. package/dist/src/transport/reservation-store.js +209 -0
  47. package/dist/src/transport/reservation-store.js.map +1 -0
  48. package/dist/src/utils.d.ts +14 -0
  49. package/dist/src/utils.d.ts.map +1 -0
  50. package/dist/src/utils.js +106 -0
  51. package/dist/src/utils.js.map +1 -0
  52. package/package.json +83 -0
  53. package/src/constants.ts +79 -0
  54. package/src/index.ts +64 -0
  55. package/src/pb/index.proto +67 -0
  56. package/src/pb/index.ts +539 -0
  57. package/src/server/advert-service.ts +109 -0
  58. package/src/server/index.ts +446 -0
  59. package/src/server/reservation-store.ts +116 -0
  60. package/src/server/reservation-voucher.ts +51 -0
  61. package/src/transport/discovery.ts +138 -0
  62. package/src/transport/index.ts +399 -0
  63. package/src/transport/listener.ts +98 -0
  64. package/src/transport/reservation-store.ts +312 -0
  65. package/src/utils.ts +134 -0
@@ -0,0 +1,446 @@
1
+ import { TypedEventEmitter, setMaxListeners } from '@libp2p/interface/events'
2
+ import { peerIdFromBytes } from '@libp2p/peer-id'
3
+ import { RecordEnvelope } from '@libp2p/peer-record'
4
+ import { type Multiaddr, multiaddr } from '@multiformats/multiaddr'
5
+ import { pbStream, type ProtobufStream } from 'it-protobuf-stream'
6
+ import pDefer from 'p-defer'
7
+ import {
8
+ CIRCUIT_PROTO_CODE,
9
+ DEFAULT_HOP_TIMEOUT,
10
+ MAX_CONNECTIONS,
11
+ RELAY_SOURCE_TAG,
12
+ RELAY_V2_HOP_CODEC,
13
+ RELAY_V2_STOP_CODEC
14
+ } from '../constants.js'
15
+ import { HopMessage, type Reservation, Status, StopMessage } from '../pb/index.js'
16
+ import { createLimitedRelay } from '../utils.js'
17
+ import { AdvertService, type AdvertServiceComponents, type AdvertServiceInit } from './advert-service.js'
18
+ import { ReservationStore, type ReservationStoreInit } from './reservation-store.js'
19
+ import { ReservationVoucherRecord } from './reservation-voucher.js'
20
+ import type { CircuitRelayService, RelayReservation } from '../index.js'
21
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
22
+ import type { Connection, Stream } from '@libp2p/interface/connection'
23
+ import type { ConnectionGater } from '@libp2p/interface/connection-gater'
24
+ import type { PeerId } from '@libp2p/interface/peer-id'
25
+ import type { PeerStore } from '@libp2p/interface/peer-store'
26
+ import type { Startable } from '@libp2p/interface/startable'
27
+ import type { AddressManager } from '@libp2p/interface-internal/address-manager'
28
+ import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
29
+ import type { IncomingStreamData, Registrar } from '@libp2p/interface-internal/registrar'
30
+ import type { PeerMap } from '@libp2p/peer-collections'
31
+
32
+ const isRelayAddr = (ma: Multiaddr): boolean => ma.protoCodes().includes(CIRCUIT_PROTO_CODE)
33
+
34
+ export interface CircuitRelayServerInit {
35
+ /**
36
+ * Incoming hop requests must complete within this time in ms otherwise
37
+ * the stream will be reset (default: 30s)
38
+ */
39
+ hopTimeout?: number
40
+
41
+ /**
42
+ * If true, advertise this service via libp2p content routing to allow
43
+ * peers to locate us on the network (default: false)
44
+ */
45
+ advertise?: boolean | AdvertServiceInit
46
+
47
+ /**
48
+ * Configuration of reservations
49
+ */
50
+ reservations?: ReservationStoreInit
51
+
52
+ /**
53
+ * The maximum number of simultaneous HOP inbound streams that can be open at once
54
+ */
55
+ maxInboundHopStreams?: number
56
+
57
+ /**
58
+ * The maximum number of simultaneous HOP outbound streams that can be open at once
59
+ */
60
+ maxOutboundHopStreams?: number
61
+
62
+ /**
63
+ * The maximum number of simultaneous STOP outbound streams that can be open at
64
+ * once. (default: 300)
65
+ */
66
+ maxOutboundStopStreams?: number
67
+ }
68
+
69
+ export interface HopProtocolOptions {
70
+ connection: Connection
71
+ request: HopMessage
72
+ stream: ProtobufStream<Stream>
73
+ }
74
+
75
+ export interface StopOptions {
76
+ connection: Connection
77
+ request: StopMessage
78
+ }
79
+
80
+ export interface CircuitRelayServerComponents extends AdvertServiceComponents {
81
+ registrar: Registrar
82
+ peerStore: PeerStore
83
+ addressManager: AddressManager
84
+ peerId: PeerId
85
+ connectionManager: ConnectionManager
86
+ connectionGater: ConnectionGater
87
+ logger: ComponentLogger
88
+ }
89
+
90
+ export interface RelayServerEvents {
91
+ 'relay:reservation': CustomEvent<RelayReservation>
92
+ 'relay:advert:success': CustomEvent<unknown>
93
+ 'relay:advert:error': CustomEvent<Error>
94
+ }
95
+
96
+ const defaults = {
97
+ maxOutboundStopStreams: MAX_CONNECTIONS
98
+ }
99
+
100
+ class CircuitRelayServer extends TypedEventEmitter<RelayServerEvents> implements Startable, CircuitRelayService {
101
+ private readonly registrar: Registrar
102
+ private readonly peerStore: PeerStore
103
+ private readonly addressManager: AddressManager
104
+ private readonly peerId: PeerId
105
+ private readonly connectionManager: ConnectionManager
106
+ private readonly connectionGater: ConnectionGater
107
+ private readonly reservationStore: ReservationStore
108
+ private readonly advertService: AdvertService | undefined
109
+ private started: boolean
110
+ private readonly hopTimeout: number
111
+ private readonly shutdownController: AbortController
112
+ private readonly maxInboundHopStreams?: number
113
+ private readonly maxOutboundHopStreams?: number
114
+ private readonly maxOutboundStopStreams: number
115
+ private readonly log: Logger
116
+
117
+ /**
118
+ * Creates an instance of Relay
119
+ */
120
+ constructor (components: CircuitRelayServerComponents, init: CircuitRelayServerInit = {}) {
121
+ super()
122
+
123
+ this.log = components.logger.forComponent('libp2p:circuit-relay:server')
124
+ this.registrar = components.registrar
125
+ this.peerStore = components.peerStore
126
+ this.addressManager = components.addressManager
127
+ this.peerId = components.peerId
128
+ this.connectionManager = components.connectionManager
129
+ this.connectionGater = components.connectionGater
130
+ this.started = false
131
+ this.hopTimeout = init?.hopTimeout ?? DEFAULT_HOP_TIMEOUT
132
+ this.shutdownController = new AbortController()
133
+ this.maxInboundHopStreams = init.maxInboundHopStreams
134
+ this.maxOutboundHopStreams = init.maxOutboundHopStreams
135
+ this.maxOutboundStopStreams = init.maxOutboundStopStreams ?? defaults.maxOutboundStopStreams
136
+
137
+ setMaxListeners(Infinity, this.shutdownController.signal)
138
+
139
+ if (init.advertise != null && init.advertise !== false) {
140
+ this.advertService = new AdvertService(components, init.advertise === true ? undefined : init.advertise)
141
+ this.advertService.addEventListener('advert:success', () => {
142
+ this.safeDispatchEvent('relay:advert:success', {})
143
+ })
144
+ this.advertService.addEventListener('advert:error', (evt) => {
145
+ this.safeDispatchEvent('relay:advert:error', { detail: evt.detail })
146
+ })
147
+ }
148
+
149
+ this.reservationStore = new ReservationStore(init.reservations)
150
+ }
151
+
152
+ isStarted (): boolean {
153
+ return this.started
154
+ }
155
+
156
+ /**
157
+ * Start Relay service
158
+ */
159
+ async start (): Promise<void> {
160
+ if (this.started) {
161
+ return
162
+ }
163
+
164
+ // Advertise service if HOP enabled and advertising enabled
165
+ this.advertService?.start()
166
+
167
+ await this.registrar.handle(RELAY_V2_HOP_CODEC, (data) => {
168
+ void this.onHop(data).catch(err => {
169
+ this.log.error(err)
170
+ })
171
+ }, {
172
+ maxInboundStreams: this.maxInboundHopStreams,
173
+ maxOutboundStreams: this.maxOutboundHopStreams,
174
+ runOnTransientConnection: true
175
+ })
176
+
177
+ this.reservationStore.start()
178
+
179
+ this.started = true
180
+ }
181
+
182
+ /**
183
+ * Stop Relay service
184
+ */
185
+ async stop (): Promise<void> {
186
+ this.advertService?.stop()
187
+ this.reservationStore.stop()
188
+ this.shutdownController.abort()
189
+ await this.registrar.unhandle(RELAY_V2_HOP_CODEC)
190
+
191
+ this.started = false
192
+ }
193
+
194
+ async onHop ({ connection, stream }: IncomingStreamData): Promise<void> {
195
+ this.log('received circuit v2 hop protocol stream from %p', connection.remotePeer)
196
+
197
+ const hopTimeoutPromise = pDefer<HopMessage>()
198
+ const timeout = setTimeout(() => {
199
+ hopTimeoutPromise.reject('timed out')
200
+ }, this.hopTimeout)
201
+ const pbstr = pbStream(stream)
202
+
203
+ try {
204
+ const request: HopMessage = await Promise.race([
205
+ pbstr.pb(HopMessage).read(),
206
+ hopTimeoutPromise.promise
207
+ ])
208
+
209
+ if (request?.type == null) {
210
+ throw new Error('request was invalid, could not read from stream')
211
+ }
212
+
213
+ this.log('received', request.type)
214
+
215
+ await Promise.race([
216
+ this.handleHopProtocol({
217
+ connection,
218
+ stream: pbstr,
219
+ request
220
+ }),
221
+ hopTimeoutPromise.promise
222
+ ])
223
+ } catch (err: any) {
224
+ this.log.error('error while handling hop', err)
225
+ await pbstr.pb(HopMessage).write({
226
+ type: HopMessage.Type.STATUS,
227
+ status: Status.MALFORMED_MESSAGE
228
+ })
229
+ stream.abort(err)
230
+ } finally {
231
+ clearTimeout(timeout)
232
+ }
233
+ }
234
+
235
+ async handleHopProtocol ({ stream, request, connection }: HopProtocolOptions): Promise<void> {
236
+ this.log('received hop message')
237
+ switch (request.type) {
238
+ case HopMessage.Type.RESERVE: await this.handleReserve({ stream, request, connection }); break
239
+ case HopMessage.Type.CONNECT: await this.handleConnect({ stream, request, connection }); break
240
+ default: {
241
+ this.log.error('invalid hop request type %s via peer %p', request.type, connection.remotePeer)
242
+ await stream.pb(HopMessage).write({ type: HopMessage.Type.STATUS, status: Status.UNEXPECTED_MESSAGE })
243
+ }
244
+ }
245
+ }
246
+
247
+ async handleReserve ({ stream, request, connection }: HopProtocolOptions): Promise<void> {
248
+ const hopstr = stream.pb(HopMessage)
249
+ this.log('hop reserve request from %p', connection.remotePeer)
250
+
251
+ if (isRelayAddr(connection.remoteAddr)) {
252
+ this.log.error('relay reservation over circuit connection denied for peer: %p', connection.remotePeer)
253
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.PERMISSION_DENIED })
254
+ return
255
+ }
256
+
257
+ if ((await this.connectionGater.denyInboundRelayReservation?.(connection.remotePeer)) === true) {
258
+ this.log.error('reservation for %p denied by connection gater', connection.remotePeer)
259
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.PERMISSION_DENIED })
260
+ return
261
+ }
262
+
263
+ const result = this.reservationStore.reserve(connection.remotePeer, connection.remoteAddr)
264
+
265
+ if (result.status !== Status.OK) {
266
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: result.status })
267
+ return
268
+ }
269
+
270
+ try {
271
+ // tag relay target peer
272
+ // result.expire is non-null if `ReservationStore.reserve` returns with status == OK
273
+ if (result.expire != null) {
274
+ const ttl = (result.expire * 1000) - Date.now()
275
+ await this.peerStore.merge(connection.remotePeer, {
276
+ tags: {
277
+ [RELAY_SOURCE_TAG]: { value: 1, ttl }
278
+ }
279
+ })
280
+ }
281
+
282
+ await hopstr.write({
283
+ type: HopMessage.Type.STATUS,
284
+ status: Status.OK,
285
+ reservation: await this.makeReservation(connection.remotePeer, BigInt(result.expire ?? 0)),
286
+ limit: this.reservationStore.get(connection.remotePeer)?.limit
287
+ })
288
+ this.log('sent confirmation response to %s', connection.remotePeer)
289
+ } catch (err) {
290
+ this.log.error('failed to send confirmation response to %p', connection.remotePeer, err)
291
+ this.reservationStore.removeReservation(connection.remotePeer)
292
+ }
293
+ }
294
+
295
+ async makeReservation (
296
+ remotePeer: PeerId,
297
+ expire: bigint
298
+ ): Promise<Reservation> {
299
+ const addrs = []
300
+
301
+ for (const relayAddr of this.addressManager.getAddresses()) {
302
+ if (relayAddr.toString().includes('/p2p-circuit')) {
303
+ continue
304
+ }
305
+
306
+ addrs.push(relayAddr.bytes)
307
+ }
308
+
309
+ const voucher = await RecordEnvelope.seal(new ReservationVoucherRecord({
310
+ peer: remotePeer,
311
+ relay: this.peerId,
312
+ expiration: Number(expire)
313
+ }), this.peerId)
314
+
315
+ return {
316
+ addrs,
317
+ expire,
318
+ voucher: voucher.marshal()
319
+ }
320
+ }
321
+
322
+ async handleConnect ({ stream, request, connection }: HopProtocolOptions): Promise<void> {
323
+ const hopstr = stream.pb(HopMessage)
324
+
325
+ if (isRelayAddr(connection.remoteAddr)) {
326
+ this.log.error('relay reservation over circuit connection denied for peer: %p', connection.remotePeer)
327
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.PERMISSION_DENIED })
328
+ return
329
+ }
330
+
331
+ this.log('hop connect request from %p', connection.remotePeer)
332
+
333
+ let dstPeer: PeerId
334
+
335
+ try {
336
+ if (request.peer == null) {
337
+ this.log.error('no peer info in hop connect request')
338
+ throw new Error('no peer info in request')
339
+ }
340
+
341
+ request.peer.addrs.forEach(multiaddr)
342
+ dstPeer = peerIdFromBytes(request.peer.id)
343
+ } catch (err) {
344
+ this.log.error('invalid hop connect request via peer %p %s', connection.remotePeer, err)
345
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.MALFORMED_MESSAGE })
346
+ return
347
+ }
348
+
349
+ if (!this.reservationStore.hasReservation(dstPeer)) {
350
+ this.log.error('hop connect denied for destination peer %p not having a reservation for %p with status %s', dstPeer, connection.remotePeer, Status.NO_RESERVATION)
351
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.NO_RESERVATION })
352
+ return
353
+ }
354
+
355
+ if ((await this.connectionGater.denyOutboundRelayedConnection?.(connection.remotePeer, dstPeer)) === true) {
356
+ this.log.error('hop connect for %p to %p denied by connection gater', connection.remotePeer, dstPeer)
357
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.PERMISSION_DENIED })
358
+ return
359
+ }
360
+
361
+ const connections = this.connectionManager.getConnections(dstPeer)
362
+
363
+ if (connections.length === 0) {
364
+ this.log('hop connect denied for destination peer %p not having a connection for %p as there is no destination connection', dstPeer, connection.remotePeer)
365
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.NO_RESERVATION })
366
+ return
367
+ }
368
+
369
+ const destinationConnection = connections[0]
370
+
371
+ const destinationStream = await this.stopHop({
372
+ connection: destinationConnection,
373
+ request: {
374
+ type: StopMessage.Type.CONNECT,
375
+ peer: {
376
+ id: connection.remotePeer.toBytes(),
377
+ addrs: []
378
+ }
379
+ }
380
+ })
381
+
382
+ if (destinationStream == null) {
383
+ this.log.error('failed to open stream to destination peer %p', destinationConnection?.remotePeer)
384
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.CONNECTION_FAILED })
385
+ return
386
+ }
387
+
388
+ await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.OK })
389
+ const sourceStream = stream.unwrap()
390
+
391
+ this.log('connection from %p to %p established - merging streams', connection.remotePeer, dstPeer)
392
+ const limit = this.reservationStore.get(dstPeer)?.limit
393
+ // Short circuit the two streams to create the relayed connection
394
+ createLimitedRelay(sourceStream, destinationStream, this.shutdownController.signal, limit, {
395
+ log: this.log
396
+ })
397
+ }
398
+
399
+ /**
400
+ * Send a STOP request to the target peer that the dialing peer wants to contact
401
+ */
402
+ async stopHop ({
403
+ connection,
404
+ request
405
+ }: StopOptions): Promise<Stream | undefined> {
406
+ this.log('starting circuit relay v2 stop request to %s', connection.remotePeer)
407
+ const stream = await connection.newStream([RELAY_V2_STOP_CODEC], {
408
+ maxOutboundStreams: this.maxOutboundStopStreams,
409
+ runOnTransientConnection: true
410
+ })
411
+ const pbstr = pbStream(stream)
412
+ const stopstr = pbstr.pb(StopMessage)
413
+ await stopstr.write(request)
414
+ let response
415
+
416
+ try {
417
+ response = await stopstr.read()
418
+ } catch (err) {
419
+ this.log.error('error parsing stop message response from %p', connection.remotePeer)
420
+ }
421
+
422
+ if (response == null) {
423
+ this.log.error('could not read response from %p', connection.remotePeer)
424
+ await stream.close()
425
+ return
426
+ }
427
+
428
+ if (response.status === Status.OK) {
429
+ this.log('stop request to %p was successful', connection.remotePeer)
430
+ return pbstr.unwrap()
431
+ }
432
+
433
+ this.log('stop request failed with code %d', response.status)
434
+ await stream.close()
435
+ }
436
+
437
+ get reservations (): PeerMap<RelayReservation> {
438
+ return this.reservationStore.reservations
439
+ }
440
+ }
441
+
442
+ export function circuitRelayServer (init: CircuitRelayServerInit = {}): (components: CircuitRelayServerComponents) => CircuitRelayService {
443
+ return (components) => {
444
+ return new CircuitRelayServer(components, init)
445
+ }
446
+ }
@@ -0,0 +1,116 @@
1
+ import { PeerMap } from '@libp2p/peer-collections'
2
+ import { DEFAULT_DATA_LIMIT, DEFAULT_DURATION_LIMIT, DEFAULT_MAX_RESERVATION_CLEAR_INTERVAL, DEFAULT_MAX_RESERVATION_STORE_SIZE, DEFAULT_MAX_RESERVATION_TTL } from '../constants.js'
3
+ import { type Limit, Status } from '../pb/index.js'
4
+ import type { RelayReservation } from '../index.js'
5
+ import type { RecursivePartial } from '@libp2p/interface'
6
+ import type { PeerId } from '@libp2p/interface/peer-id'
7
+ import type { Startable } from '@libp2p/interface/startable'
8
+ import type { Multiaddr } from '@multiformats/multiaddr'
9
+
10
+ export type ReservationStatus = Status.OK | Status.PERMISSION_DENIED | Status.RESERVATION_REFUSED
11
+
12
+ export interface ReservationStoreInit {
13
+ /*
14
+ * maximum number of reservations allowed, default: 15
15
+ */
16
+ maxReservations?: number
17
+ /*
18
+ * interval after which stale reservations are cleared, default: 300s
19
+ */
20
+ reservationClearInterval?: number
21
+ /*
22
+ * apply default relay limits to a new reservation, default: true
23
+ */
24
+ applyDefaultLimit?: boolean
25
+ /**
26
+ * reservation ttl, default: 2 hours
27
+ */
28
+ reservationTtl?: number
29
+ /**
30
+ * The maximum time a relayed connection can be open for
31
+ */
32
+ defaultDurationLimit?: number
33
+ /**
34
+ * The maximum amount of data allowed to be transferred over a relayed connection
35
+ */
36
+ defaultDataLimit?: bigint
37
+ }
38
+
39
+ export type ReservationStoreOptions = RecursivePartial<ReservationStoreInit>
40
+
41
+ export class ReservationStore implements Startable {
42
+ public readonly reservations = new PeerMap<RelayReservation>()
43
+ private _started = false
44
+ private interval: any
45
+ private readonly maxReservations: number
46
+ private readonly reservationClearInterval: number
47
+ private readonly applyDefaultLimit: boolean
48
+ private readonly reservationTtl: number
49
+ private readonly defaultDurationLimit: number
50
+ private readonly defaultDataLimit: bigint
51
+
52
+ constructor (options: ReservationStoreOptions = {}) {
53
+ this.maxReservations = options.maxReservations ?? DEFAULT_MAX_RESERVATION_STORE_SIZE
54
+ this.reservationClearInterval = options.reservationClearInterval ?? DEFAULT_MAX_RESERVATION_CLEAR_INTERVAL
55
+ this.applyDefaultLimit = options.applyDefaultLimit !== false
56
+ this.reservationTtl = options.reservationTtl ?? DEFAULT_MAX_RESERVATION_TTL
57
+ this.defaultDurationLimit = options.defaultDurationLimit ?? DEFAULT_DURATION_LIMIT
58
+ this.defaultDataLimit = options.defaultDataLimit ?? DEFAULT_DATA_LIMIT
59
+ }
60
+
61
+ isStarted (): boolean {
62
+ return this._started
63
+ }
64
+
65
+ start (): void {
66
+ if (this._started) {
67
+ return
68
+ }
69
+ this._started = true
70
+ this.interval = setInterval(
71
+ () => {
72
+ const now = (new Date()).getTime()
73
+ this.reservations.forEach((r, k) => {
74
+ if (r.expire.getTime() < now) {
75
+ this.reservations.delete(k)
76
+ }
77
+ })
78
+ },
79
+ this.reservationClearInterval
80
+ )
81
+ }
82
+
83
+ stop (): void {
84
+ clearInterval(this.interval)
85
+ }
86
+
87
+ reserve (peer: PeerId, addr: Multiaddr, limit?: Limit): { status: ReservationStatus, expire?: number } {
88
+ if (this.reservations.size >= this.maxReservations && !this.reservations.has(peer)) {
89
+ return { status: Status.RESERVATION_REFUSED }
90
+ }
91
+
92
+ const expire = new Date(Date.now() + this.reservationTtl)
93
+ let checkedLimit: Limit | undefined
94
+
95
+ if (this.applyDefaultLimit) {
96
+ checkedLimit = limit ?? { data: this.defaultDataLimit, duration: this.defaultDurationLimit }
97
+ }
98
+
99
+ this.reservations.set(peer, { addr, expire, limit: checkedLimit })
100
+
101
+ // return expiry time in seconds
102
+ return { status: Status.OK, expire: Math.round(expire.getTime() / 1000) }
103
+ }
104
+
105
+ removeReservation (peer: PeerId): void {
106
+ this.reservations.delete(peer)
107
+ }
108
+
109
+ hasReservation (dst: PeerId): boolean {
110
+ return this.reservations.has(dst)
111
+ }
112
+
113
+ get (peer: PeerId): RelayReservation | undefined {
114
+ return this.reservations.get(peer)
115
+ }
116
+ }
@@ -0,0 +1,51 @@
1
+ import { ReservationVoucher } from '../pb/index.js'
2
+ import type { PeerId } from '@libp2p/interface/peer-id'
3
+ import type { Record } from '@libp2p/interface/record'
4
+
5
+ export interface ReservationVoucherOptions {
6
+ relay: PeerId
7
+ peer: PeerId
8
+ expiration: number
9
+ }
10
+
11
+ export class ReservationVoucherRecord implements Record {
12
+ public readonly domain = 'libp2p-relay-rsvp'
13
+ public readonly codec = new Uint8Array([0x03, 0x02])
14
+
15
+ private readonly relay: PeerId
16
+ private readonly peer: PeerId
17
+ private readonly expiration: number
18
+
19
+ constructor ({ relay, peer, expiration }: ReservationVoucherOptions) {
20
+ this.relay = relay
21
+ this.peer = peer
22
+ this.expiration = expiration
23
+ }
24
+
25
+ marshal (): Uint8Array {
26
+ return ReservationVoucher.encode({
27
+ relay: this.relay.toBytes(),
28
+ peer: this.peer.toBytes(),
29
+ expiration: BigInt(this.expiration)
30
+ })
31
+ }
32
+
33
+ equals (other: Record): boolean {
34
+ if (!(other instanceof ReservationVoucherRecord)) {
35
+ return false
36
+ }
37
+ if (!this.peer.equals(other.peer)) {
38
+ return false
39
+ }
40
+
41
+ if (!this.relay.equals(other.relay)) {
42
+ return false
43
+ }
44
+
45
+ if (this.expiration !== other.expiration) {
46
+ return false
47
+ }
48
+
49
+ return true
50
+ }
51
+ }