From 92d61e429fbc6f43d8377ac018dd65167642a939 Mon Sep 17 00:00:00 2001 From: dozyio Date: Wed, 26 Feb 2025 13:24:47 +0000 Subject: [PATCH 1/2] feat: downloadOnly flag for bitswap --- packages/bitswap/src/bitswap.ts | 39 +++-- packages/bitswap/src/index.ts | 8 + packages/bitswap/test/bitswap.spec.ts | 207 +++++++++++++++++++++++++- 3 files changed, 243 insertions(+), 11 deletions(-) diff --git a/packages/bitswap/src/bitswap.ts b/packages/bitswap/src/bitswap.ts index a960eaa7f..430629270 100644 --- a/packages/bitswap/src/bitswap.ts +++ b/packages/bitswap/src/bitswap.ts @@ -35,8 +35,9 @@ export class Bitswap implements BitswapInterface { public readonly stats: Stats public network: Network public blockstore: Blockstore - public peerWantLists: PeerWantLists + public peerWantLists?: PeerWantLists public wantList: WantList + public downloadOnly: boolean constructor (components: BitswapComponents, init: BitswapOptions = {}) { this.logger = components.logger @@ -49,11 +50,18 @@ export class Bitswap implements BitswapInterface { // the network delivers messages this.network = new Network(components, init) - // handle which blocks we send to peers - this.peerWantLists = new PeerWantLists({ - ...components, - network: this.network - }, init) + // only download blocks, don't provide them + this.downloadOnly = init.downloadOnly ?? false + + if (this.downloadOnly) { + this.peerWantLists = undefined + } else { + // handle which blocks we send to peers + this.peerWantLists = new PeerWantLists({ + ...components, + network: this.network + }, init) + } // handle which blocks we ask peers for this.wantList = new WantList({ @@ -107,10 +115,17 @@ export class Bitswap implements BitswapInterface { * Sends notifications about the arrival of a block */ async notify (cid: CID, block: Uint8Array, options: ProgressOptions & AbortOptions = {}): Promise { - await Promise.all([ - this.peerWantLists.receivedBlock(cid, options), - this.wantList.receivedBlock(cid, options) - ]) + if (this.peerWantLists === undefined) { + // download only + await Promise.all([ + this.wantList.receivedBlock(cid, options) + ]) + } else { + await Promise.all([ + this.peerWantLists.receivedBlock(cid, options), + this.wantList.receivedBlock(cid, options) + ]) + } } getWantlist (): WantListEntry[] { @@ -124,6 +139,10 @@ export class Bitswap implements BitswapInterface { } getPeerWantlist (peer: PeerId): WantListEntry[] | undefined { + if (this.peerWantLists === undefined) { + return undefined + } + return this.peerWantLists.wantListForPeer(peer) } diff --git a/packages/bitswap/src/index.ts b/packages/bitswap/src/index.ts index 65a06cb39..d37fdccf3 100644 --- a/packages/bitswap/src/index.ts +++ b/packages/bitswap/src/index.ts @@ -186,6 +186,14 @@ export interface BitswapOptions { * @default 2097152 */ maxIncomingMessageSize?: number + + /** + * Operate in download-only mode. In this mode, the node will only retrieve + * blocks from peers and will not serve or advertise any blocks it holds. + * + * @default to `false` + */ + downloadOnly?: boolean } export const createBitswap = (components: BitswapComponents, options: BitswapOptions = {}): Bitswap => { diff --git a/packages/bitswap/test/bitswap.spec.ts b/packages/bitswap/test/bitswap.spec.ts index f2658f071..3a63d14b7 100644 --- a/packages/bitswap/test/bitswap.spec.ts +++ b/packages/bitswap/test/bitswap.spec.ts @@ -117,7 +117,8 @@ describe('bitswap', () => { }) it('should notify peers we have a block', async () => { - const receivedBlockSpy = Sinon.spy(bitswap.peerWantLists, 'receivedBlock') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const receivedBlockSpy = Sinon.spy(bitswap.peerWantLists!, 'receivedBlock') await bitswap.notify(cid, block) @@ -219,3 +220,207 @@ describe('bitswap', () => { }) }) }) + +describe('bitswap download only', () => { + let components: StubbedBitswapComponents + let bitswap: Bitswap + let cid: CID + let block: Uint8Array + + beforeEach(async () => { + block = Uint8Array.from([0, 1, 2, 3, 4]) + const mh = await sha256.digest(block) + cid = CID.createV0(mh).toV1() + + components = { + peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + routing: stubInterface(), + blockstore: new MemoryBlockstore(), + libp2p: stubInterface({ + metrics: undefined + }) + } + + bitswap = new Bitswap( + { + ...components, + logger: defaultLogger() + }, + { + downloadOnly: true + } + ) + + components.libp2p.getConnections.returns([]) + + await start(bitswap) + }) + + afterEach(async () => { + if (bitswap != null) { + await stop(bitswap) + } + }) + + describe('want', () => { + it('should want a block that is available on the network', async () => { + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const findProvsSpy = bitswap.network.findAndConnect = Sinon.stub() + findProvsSpy.resolves() + + // add peer + bitswap.wantList.peers.set(remotePeer, new Set()) + + // wait for message send to peer + const sentMessages = pDefer() + + bitswap.network.sendMessage = async (peerId) => { + if (remotePeer.equals(peerId)) { + sentMessages.resolve() + } + } + + const p = bitswap.want(cid) + + // wait for message send to peer + await sentMessages.promise + + // provider sends message + bitswap.network.safeDispatchEvent('bitswap:message', { + detail: { + peer: remotePeer, + message: { + blocks: [{ + prefix: cidToPrefix(cid), + data: block + }], + blockPresences: [], + pendingBytes: 0 + } + } + }) + + const b = await p + + // should have added cid to wantlist and searched for providers + expect(findProvsSpy.called).to.be.true() + + // should have cancelled the notification request + expect(b).to.equalBytes(block) + }) + + it('should abort wanting a block that is not available on the network', async () => { + const p = bitswap.want(cid, { + signal: AbortSignal.timeout(100) + }) + + await expect(p).to.eventually.be.rejected + .with.property('name', 'AbortError') + }) + + it('should not notify peers we have a block', async () => { + // Ensure peerWantLists is undefined. + bitswap.peerWantLists = undefined + + // Call the notify function and assert it doesn't throw. + await expect(bitswap.notify(cid, block)).to.eventually.be.fulfilled + + // Optionally, check that peerWantLists remains undefined. + expect(bitswap.peerWantLists).to.equal(undefined) + }) + }) + + describe('wantlist', () => { + it('should remove CIDs from the wantlist when the block arrives', async () => { + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + expect(bitswap.getWantlist()).to.be.empty() + + const findProvsSpy = bitswap.network.findAndConnect = Sinon.stub() + findProvsSpy.resolves() + + // add peer + bitswap.wantList.peers.set(remotePeer, new Set()) + + // wait for message send to peer + const sentMessages = pDefer() + + bitswap.network.sendMessage = async (peerId) => { + if (remotePeer.equals(peerId)) { + sentMessages.resolve() + } + } + + const p = bitswap.want(cid) + + // wait for message send to peer + await sentMessages.promise + + expect(bitswap.getWantlist().map(w => w.cid)).to.include(cid) + + // provider sends message + bitswap.network.safeDispatchEvent('bitswap:message', { + detail: { + peer: remotePeer, + message: { + blocks: [{ + prefix: cidToPrefix(cid), + data: block + }], + blockPresences: [], + pendingBytes: 0 + } + } + }) + + const b = await p + + expect(bitswap.getWantlist()).to.be.empty() + expect(b).to.equalBytes(block) + }) + + it('should remove CIDs from the wantlist when the want is aborted', async () => { + expect(bitswap.getWantlist()).to.be.empty() + + const p = bitswap.want(cid, { + signal: AbortSignal.timeout(100) + }) + + expect(bitswap.getWantlist().map(w => w.cid)).to.include(cid) + + await expect(p).to.eventually.be.rejected + .with.property('name', 'AbortError') + + expect(bitswap.getWantlist()).to.be.empty() + }) + }) + + describe('peer wantlist', () => { + it('should not return a peer wantlist', async () => { + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + + // don't have this peer yet + expect(bitswap.getPeerWantlist(remotePeer)).to.be.undefined() + + // peers sends message with wantlist + bitswap.network.safeDispatchEvent('bitswap:message', { + detail: { + peer: remotePeer, + message: { + wantlist: { + full: false, + entries: [{ + cid: cid.bytes, + priority: 100 + }] + }, + blockPresences: [], + blocks: [], + pendingBytes: 0 + } + } + }) + + expect(bitswap.getPeerWantlist(remotePeer)).to.be.undefined() + }) + }) +}) From c87a569c3be08340dd728227d5579b31bea1f777 Mon Sep 17 00:00:00 2001 From: dozyio Date: Wed, 26 Feb 2025 16:21:42 +0000 Subject: [PATCH 2/2] chore: await instead of Promise.all --- packages/bitswap/src/bitswap.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/bitswap/src/bitswap.ts b/packages/bitswap/src/bitswap.ts index 430629270..9918bcbbc 100644 --- a/packages/bitswap/src/bitswap.ts +++ b/packages/bitswap/src/bitswap.ts @@ -117,9 +117,7 @@ export class Bitswap implements BitswapInterface { async notify (cid: CID, block: Uint8Array, options: ProgressOptions & AbortOptions = {}): Promise { if (this.peerWantLists === undefined) { // download only - await Promise.all([ - this.wantList.receivedBlock(cid, options) - ]) + await this.wantList.receivedBlock(cid, options) } else { await Promise.all([ this.peerWantLists.receivedBlock(cid, options),