UNPKG

ipfs-bitswap

Version:

JavaScript implementation of the Bitswap data exchange protocol used by IPFS

277 lines (241 loc) 8.25 kB
import { CodeError } from '@libp2p/interface' import { base58btc } from 'multiformats/bases/base58' import { CID } from 'multiformats/cid' import { sha256 } from 'multiformats/hashes/sha2' // @ts-expect-error no types import vd from 'varint-decoder' import { isMapEqual } from '../utils/index.js' import ve from '../utils/varint-encoder.js' import { BitswapMessageEntry as Entry } from './entry.js' import { Message } from './message.js' import type { MultihashHasherLoader } from '../index.js' export class BitswapMessage { static Entry = Entry static WantType = { Block: Message.Wantlist.WantType.Block, Have: Message.Wantlist.WantType.Have } static BlockPresenceType = { Have: Message.BlockPresenceType.Have, DontHave: Message.BlockPresenceType.DontHave } static deserialize = async (raw: Uint8Array, hashLoader?: MultihashHasherLoader): Promise<BitswapMessage> => { const decoded = Message.decode(raw) const isFull = decoded.wantlist?.full === true const msg = new BitswapMessage(isFull) decoded.wantlist?.entries.forEach((entry) => { if (entry.block == null) { return } // note: entry.block is the CID here const cid = CID.decode(entry.block) msg.addEntry(cid, entry.priority ?? 0, entry.wantType, Boolean(entry.cancel), Boolean(entry.sendDontHave)) }) decoded.blockPresences.forEach((blockPresence) => { if (blockPresence.cid == null) { return } const cid = CID.decode(blockPresence.cid) if (blockPresence.type === BitswapMessage.BlockPresenceType.Have) { msg.addHave(cid) } else { msg.addDontHave(cid) } }) // Bitswap 1.0.0 // decoded.blocks are just the byte arrays if (decoded.blocks.length > 0) { await Promise.all(decoded.blocks.map(async (b) => { const hash = await sha256.digest(b) const cid = CID.createV0(hash) msg.addBlock(cid, b) })) return msg } // Bitswap 1.1.0 if (decoded.payload.length > 0) { await Promise.all(decoded.payload.map(async (p) => { if (p.prefix == null || p.data == null) { return } const values = vd(p.prefix) const cidVersion = values[0] const multicodec = values[1] const hashAlg = values[2] const hasher = hashAlg === sha256.code ? sha256 : await hashLoader?.getHasher(hashAlg) if (hasher == null) { throw new CodeError('Unknown hash algorithm', 'ERR_UNKNOWN_HASH_ALG') } // const hashLen = values[3] // We haven't need to use this so far const hash = await hasher.digest(p.data) const cid = CID.create(cidVersion, multicodec, hash) msg.addBlock(cid, p.data) })) msg.setPendingBytes(decoded.pendingBytes) return msg } return msg } static blockPresenceSize = (cid: CID): number => { // It's ok if this is not exactly right: it's used to estimate the size of // the HAVE / DONT_HAVE on the wire, but when doing that calculation we leave // plenty of padding under the maximum message size. // (It's more important for this to be fast). return cid.bytes.length + 1 } public full: boolean public wantlist: Map<string, Entry> public blocks: Map<string, Uint8Array> public blockPresences: Map<string, Message.BlockPresenceType> public pendingBytes: number constructor (full: boolean) { this.full = full this.wantlist = new Map() this.blocks = new Map() this.blockPresences = new Map() this.pendingBytes = 0 } get empty (): boolean { return this.blocks.size === 0 && this.wantlist.size === 0 && this.blockPresences.size === 0 } addEntry (cid: CID, priority: number, wantType?: Message.Wantlist.WantType, cancel?: boolean, sendDontHave?: boolean): void { if (wantType == null) { wantType = BitswapMessage.WantType.Block } const cidStr = cid.toString(base58btc) const entry = this.wantlist.get(cidStr) if (entry != null) { // Only change priority if want is of the same type if (entry.wantType === wantType) { entry.priority = priority } // Only change from "dont cancel" to "do cancel" if (cancel === true) { entry.cancel = Boolean(cancel) } // Only change from "dont send" to "do send" DONT_HAVE if (sendDontHave === true) { entry.sendDontHave = Boolean(sendDontHave) } // want-block overrides existing want-have if (wantType === BitswapMessage.WantType.Block && entry.wantType === BitswapMessage.WantType.Have) { entry.wantType = wantType } } else { this.wantlist.set(cidStr, new Entry(cid, priority, wantType, cancel, sendDontHave)) } } addBlock (cid: CID, block: Uint8Array): void { const cidStr = cid.toString(base58btc) this.blocks.set(cidStr, block) } addHave (cid: CID): void { const cidStr = cid.toString(base58btc) if (!this.blockPresences.has(cidStr)) { this.blockPresences.set(cidStr, BitswapMessage.BlockPresenceType.Have) } } addDontHave (cid: CID): void { const cidStr = cid.toString(base58btc) if (!this.blockPresences.has(cidStr)) { this.blockPresences.set(cidStr, BitswapMessage.BlockPresenceType.DontHave) } } cancel (cid: CID): void { const cidStr = cid.toString(base58btc) this.wantlist.delete(cidStr) this.addEntry(cid, 0, BitswapMessage.WantType.Block, true, false) } setPendingBytes (size: number): void { this.pendingBytes = size } /** * Serializes to Bitswap Message protobuf of * version 1.0.0 */ serializeToBitswap100 (): Uint8Array { return Message.encode({ wantlist: { entries: Array.from(this.wantlist.values()).map((entry) => { return { block: entry.cid.bytes, // cid priority: Number(entry.priority), cancel: Boolean(entry.cancel), wantType: Message.Wantlist.WantType.Block, sendDontHave: false } }), full: Boolean(this.full) }, blocks: Array.from(this.blocks.values()) }) } /** * Serializes to Bitswap Message protobuf of * version 1.1.0 */ serializeToBitswap110 (): Uint8Array { const msg: Message = { wantlist: { entries: Array.from(this.wantlist.values()).map((entry) => { return { block: entry.cid.bytes, // cid priority: Number(entry.priority), wantType: entry.wantType, cancel: Boolean(entry.cancel), sendDontHave: Boolean(entry.sendDontHave) } }), full: Boolean(this.full) }, blockPresences: [], payload: [], pendingBytes: this.pendingBytes, blocks: [] } for (const [cidStr, data] of this.blocks.entries()) { const cid = CID.parse(cidStr) const version = cid.version const codec = cid.code const multihash = cid.multihash.code const digestLength = cid.multihash.digest.length const prefix = ve([ version, codec, multihash, digestLength ]) msg.payload.push({ prefix, data }) } for (const [cidStr, bpType] of this.blockPresences) { msg.blockPresences.push({ cid: CID.parse(cidStr).bytes, type: bpType }) } if (this.pendingBytes > 0) { msg.pendingBytes = this.pendingBytes } return Message.encode(msg) } equals (other: BitswapMessage): boolean { if (this.full !== other.full || this.pendingBytes !== other.pendingBytes || !isMapEqual(this.wantlist, other.wantlist) || !isMapEqual(this.blocks, other.blocks) || // @TODO - Is this a bug ? // @ts-expect-error - isMap equals map values to be objects not numbers !isMapEqual(this.blockPresences, other.blockPresences) ) { return false } return true } get [Symbol.toStringTag] (): string { const list = Array.from(this.wantlist.keys()) const blocks = Array.from(this.blocks.keys()) return `BitswapMessage <full: ${this.full}, list: ${list}, blocks: ${blocks}>` } }