Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: downloadOnly option for bitswap #748

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions packages/bitswap/src/bitswap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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({
Expand Down Expand Up @@ -107,10 +115,15 @@ export class Bitswap implements BitswapInterface {
* Sends notifications about the arrival of a block
*/
async notify (cid: CID, block: Uint8Array, options: ProgressOptions<BitswapNotifyProgressEvents> & AbortOptions = {}): Promise<void> {
await Promise.all([
this.peerWantLists.receivedBlock(cid, options),
this.wantList.receivedBlock(cid, options)
])
if (this.peerWantLists === undefined) {
// download only
await this.wantList.receivedBlock(cid, options)
} else {
await Promise.all([
this.peerWantLists.receivedBlock(cid, options),
this.wantList.receivedBlock(cid, options)
])
}
}

getWantlist (): WantListEntry[] {
Expand All @@ -124,6 +137,10 @@ export class Bitswap implements BitswapInterface {
}

getPeerWantlist (peer: PeerId): WantListEntry[] | undefined {
if (this.peerWantLists === undefined) {
return undefined
}

return this.peerWantLists.wantListForPeer(peer)
}

Expand Down
8 changes: 8 additions & 0 deletions packages/bitswap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
207 changes: 206 additions & 1 deletion packages/bitswap/test/bitswap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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<Routing>(),
blockstore: new MemoryBlockstore(),
libp2p: stubInterface<Libp2p>({
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<BitswapMessageEventDetail>('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<BitswapMessageEventDetail>('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<BitswapMessageEventDetail>('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()
})
})
})