@helia/bitswap
Version:
JavaScript implementation of the Bitswap data exchange protocol used by Helia
502 lines (416 loc) • 14.6 kB
text/typescript
import { TypedEventEmitter, setMaxListeners } from '@libp2p/interface'
import { trackedPeerMap } from '@libp2p/peer-collections'
import { trackedMap } from '@libp2p/utils/tracked-map'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import pDefer from 'p-defer'
import { raceEvent } from 'race-event'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { DEFAULT_MESSAGE_SEND_DELAY } from './constants.js'
import { BlockPresenceType, WantType } from './pb/message.js'
import { QueuedBitswapMessage } from './utils/bitswap-message.js'
import vd from './utils/varint-decoder.js'
import type { BitswapNotifyProgressEvents, MultihashHasherLoader } from './index.js'
import type { BitswapNetworkWantProgressEvents, Network } from './network.js'
import type { BitswapMessage } from './pb/message.js'
import type { ComponentLogger, PeerId, Startable, AbortOptions, Libp2p, TypedEventTarget, Metrics } from '@libp2p/interface'
import type { Logger } from '@libp2p/logger'
import type { PeerMap } from '@libp2p/peer-collections'
import type { DeferredPromise } from 'p-defer'
import type { ProgressOptions } from 'progress-events'
export interface WantListComponents {
network: Network
logger: ComponentLogger
libp2p: Libp2p
metrics?: Metrics
}
export interface WantListInit {
sendMessagesDelay?: number
hashLoader?: MultihashHasherLoader
}
export interface WantListEntry {
/**
* The CID we send to the remote
*/
cid: CID
/**
* The priority with which the remote should return the block
*/
priority: number
/**
* If we want the block or if we want the remote to tell us if they have the
* block - note if the block is small they'll send it to us anyway.
*/
wantType: WantType
/**
* Whether we are cancelling the block want or not
*/
cancel: boolean
/**
* Whether the remote should tell us if they have the block or not
*/
sendDontHave: boolean
}
export interface WantOptions extends AbortOptions, ProgressOptions<BitswapNetworkWantProgressEvents> {
/**
* Allow prioritising blocks
*/
priority?: number
}
export interface WantBlockResult {
sender: PeerId
cid: CID
block: Uint8Array
}
export interface WantDontHaveResult {
sender: PeerId
cid: CID
has: false
}
export interface WantHaveResult {
sender: PeerId
cid: CID
has: true
block?: Uint8Array
}
export type WantPresenceResult = WantDontHaveResult | WantHaveResult
export interface WantListEvents {
block: CustomEvent<WantBlockResult>
presence: CustomEvent<WantPresenceResult>
}
export class WantList extends TypedEventEmitter<WantListEvents> implements Startable, TypedEventTarget<WantListEvents> {
/**
* Tracks what CIDs we've previously sent to which peers
*/
public readonly peers: PeerMap<Set<string>>
public readonly wants: Map<string, WantListEntry>
private readonly network: Network
private readonly log: Logger
private readonly sendMessagesDelay: number
private sendMessagesTimeout?: ReturnType<typeof setTimeout>
private readonly hashLoader?: MultihashHasherLoader
private sendingMessages?: DeferredPromise<void>
constructor (components: WantListComponents, init: WantListInit = {}) {
super()
setMaxListeners(Infinity, this)
this.peers = trackedPeerMap({
name: 'helia_bitswap_peers',
metrics: components.metrics
})
this.wants = trackedMap({
name: 'helia_bitswap_wantlist',
metrics: components.metrics
})
this.network = components.network
this.sendMessagesDelay = init.sendMessagesDelay ?? DEFAULT_MESSAGE_SEND_DELAY
this.log = components.logger.forComponent('helia:bitswap:wantlist')
this.hashLoader = init.hashLoader
this.network.addEventListener('bitswap:message', (evt) => {
this.receiveMessage(evt.detail.peer, evt.detail.message)
.catch(err => {
this.log.error('error receiving bitswap message from %p', evt.detail.peer, err)
})
})
this.network.addEventListener('peer:connected', evt => {
this.peerConnected(evt.detail)
.catch(err => {
this.log.error('error processing newly connected bitswap peer %p', evt.detail, err)
})
})
this.network.addEventListener('peer:disconnected', evt => {
this.peerDisconnected(evt.detail)
})
}
private async addEntry (cid: CID, options: WantOptions & { wantType: WantType.WantBlock }): Promise<WantBlockResult>
private async addEntry (cid: CID, options: WantOptions & { wantType: WantType.WantHave }): Promise<WantPresenceResult>
private async addEntry (cid: CID, options: WantOptions & { wantType: WantType }): Promise<WantBlockResult | WantPresenceResult> {
const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
let entry = this.wants.get(cidStr)
if (entry == null) {
entry = {
cid,
priority: options.priority ?? 1,
wantType: options.wantType ?? WantType.WantBlock,
cancel: false,
sendDontHave: true
}
this.wants.set(cidStr, entry)
}
// upgrade want-have to want-block if the new want is a WantBlock but the
// previous want was a WantHave
if (entry.wantType === WantType.WantHave && options.wantType === WantType.WantBlock) {
entry.wantType = WantType.WantBlock
}
// broadcast changes
await this.sendMessagesDebounced()
try {
if (options.wantType === WantType.WantBlock) {
const event = await raceEvent<CustomEvent<WantBlockResult>>(this, 'block', options?.signal, {
filter: (event) => {
return uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
},
errorMessage: 'Want was aborted'
})
return event.detail
}
const event = await raceEvent<CustomEvent<WantPresenceResult>>(this, 'presence', options?.signal, {
filter: (event) => {
return uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
},
errorMessage: 'Want was aborted'
})
return event.detail
} finally {
if (options.signal?.aborted === true) {
this.log('want for %c was aborted, cancelling want', cid)
entry.cancel = true
// broadcast changes
await this.sendMessagesDebounced()
}
}
}
private async sendMessagesDebounced (): Promise<void> {
await this.sendingMessages?.promise
// broadcast changes
clearTimeout(this.sendMessagesTimeout)
this.sendMessagesTimeout = setTimeout(() => {
void this.sendMessages()
.catch(err => {
this.log('error sending messages to peers', err)
})
}, this.sendMessagesDelay)
}
private async sendMessages (): Promise<void> {
this.sendingMessages = pDefer()
await Promise.all(
[...this.peers.entries()].map(async ([peerId, sentWants]) => {
const sent = new Set<string>()
const message = new QueuedBitswapMessage()
for (const [key, entry] of this.wants.entries()) {
const sentPreviously = sentWants.has(key)
// only send if either we've not sent it before, or we haven't sent it
// but we're also cancelling the want.
if (sentPreviously || entry.cancel) {
continue
}
sent.add(key)
message.addWantlistEntry(entry.cid, {
cid: entry.cid.bytes,
priority: entry.priority,
wantType: entry.wantType,
cancel: entry.cancel,
sendDontHave: entry.sendDontHave
})
}
if (message.wantlist.size === 0) {
return
}
// add message to send queue
try {
await this.network.sendMessage(peerId, message)
// update list of messages sent to remote
for (const key of sent) {
sentWants.add(key)
}
} catch (err: any) {
this.log.error('error sending full wantlist to new peer', err)
}
})
).catch(err => {
this.log.error('error sending messages', err)
})
// queued all message sends, remove cancelled wants from wantlist and sent
// wants
for (const [key, entry] of this.wants) {
if (entry.cancel) {
this.wants.delete(key)
for (const sentWants of this.peers.values()) {
sentWants.delete(key)
}
}
}
this.sendingMessages.resolve()
}
has (cid: CID): boolean {
const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
return this.wants.has(cidStr)
}
/**
* Add a CID to the wantlist
*/
async wantSessionPresence (cid: CID, peerId: PeerId, options: WantOptions = {}): Promise<WantPresenceResult> {
const message = new QueuedBitswapMessage()
message.addWantlistEntry(cid, {
cid: cid.bytes,
sendDontHave: true,
wantType: WantType.WantHave,
priority: 1
})
// sending WantHave directly to peer
await this.network.sendMessage(peerId, message)
// wait for peer response
const event = await raceEvent<CustomEvent<WantHaveResult | WantDontHaveResult>>(this, 'presence', options.signal, {
filter: (event) => {
return peerId.equals(event.detail.sender) && uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
}
})
return event.detail
}
/**
* Add a CID to the wantlist
*/
async wantBlock (cid: CID, options: WantOptions = {}): Promise<WantBlockResult> {
return this.addEntry(cid, {
...options,
wantType: WantType.WantBlock
})
}
/**
* Add a CID to the wantlist
*/
async wantSessionBlock (cid: CID, peerId: PeerId, options: WantOptions = {}): Promise<WantPresenceResult> {
const message = new QueuedBitswapMessage()
message.addWantlistEntry(cid, {
cid: cid.bytes,
sendDontHave: true,
wantType: WantType.WantBlock,
priority: 1
})
// sending WantBlockResult directly to peer
await this.network.sendMessage(peerId, message)
// wait for peer response
const event = await raceEvent<CustomEvent<WantPresenceResult>>(this, 'presence', options.signal, {
filter: (event) => {
return peerId.equals(event.detail.sender) && uint8ArrayEquals(cid.multihash.digest, event.detail.cid.multihash.digest)
}
})
return event.detail
}
/**
* Invoked when a block has been received from an external source
*/
async receivedBlock (cid: CID, options: ProgressOptions<BitswapNotifyProgressEvents> & AbortOptions): Promise<void> {
const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
const entry = this.wants.get(cidStr)
if (entry == null) {
return
}
entry.cancel = true
await this.sendMessagesDebounced()
}
/**
* Invoked when a message is received from a bitswap peer
*/
private async receiveMessage (sender: PeerId, message: BitswapMessage): Promise<void> {
this.log('received message from %p with %d blocks', sender, message.blocks.length)
let blocksCancelled = false
// process blocks
for (const block of message.blocks) {
if (block.prefix == null || block.data == null) {
continue
}
const values = vd(block.prefix)
const cidVersion = values[0]
const multicodec = values[1]
const hashAlg = values[2]
// const hashLen = values[3] // We haven't need to use this so far
const hasher = hashAlg === sha256.code ? sha256 : await this.hashLoader?.getHasher(hashAlg)
if (hasher == null) {
this.log.error('unknown hash algorithm', hashAlg)
continue
}
let hash: any = hasher.digest(block.data)
if (hash.then != null) {
hash = await hash
}
const cid = CID.create(cidVersion === 0 ? 0 : 1, multicodec, hash)
this.log('received block from %p for %c', sender, cid)
this.safeDispatchEvent<WantBlockResult>('block', {
detail: {
sender,
cid,
block: block.data
}
})
this.safeDispatchEvent<WantHaveResult | WantDontHaveResult>('presence', {
detail: {
sender,
cid,
has: true,
block: block.data
}
})
const cidStr = uint8ArrayToString(cid.multihash.bytes, 'base64')
const entry = this.wants.get(cidStr)
if (entry == null) {
continue
}
// since we received the block, flip the cancel flag to send cancels to
// any peers on the next message sending iteration, this will remove it
// from the internal want list
entry.cancel = true
blocksCancelled = true
}
// process block presences
for (const { cid: cidBytes, type } of message.blockPresences) {
const cid = CID.decode(cidBytes)
this.log('received %s from %p for %c', type, sender, cid)
this.safeDispatchEvent<WantHaveResult | WantDontHaveResult>('presence', {
detail: {
sender,
cid,
has: type === BlockPresenceType.HaveBlock
}
})
}
if (blocksCancelled) {
await this.sendMessagesDebounced()
}
}
/**
* Invoked when the network topology notices a new peer that supports Bitswap
*/
async peerConnected (peerId: PeerId): Promise<void> {
const sentWants = new Set<string>()
const message = new QueuedBitswapMessage(true)
// new peer, give them the full wantlist
for (const [key, entry] of this.wants.entries()) {
if (entry.cancel) {
continue
}
sentWants.add(key)
message.addWantlistEntry(entry.cid, {
cid: entry.cid.bytes,
priority: 1,
wantType: WantType.WantBlock,
cancel: false,
sendDontHave: false
})
}
// only send the wantlist if we have something to send
if (message.wantlist.size === 0) {
this.peers.set(peerId, sentWants)
return
}
try {
await this.network.sendMessage(peerId, message)
this.peers.set(peerId, sentWants)
} catch (err) {
this.log.error('error sending full wantlist to new peer %p', peerId, err)
}
}
/**
* Invoked when the network topology notices peer that supports Bitswap has
* disconnected
*/
peerDisconnected (peerId: PeerId): void {
this.peers.delete(peerId)
}
start (): void {
}
stop (): void {
this.peers.clear()
clearTimeout(this.sendMessagesTimeout)
}
}