-
Notifications
You must be signed in to change notification settings - Fork 109
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: configurable block brokers #280
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { createBitswap } from 'ipfs-bitswap' | ||
import type { BlockProvider } from '@helia/interface/blocks' | ||
import type { Libp2p } from '@libp2p/interface' | ||
import type { Startable } from '@libp2p/interface/startable' | ||
import type { Blockstore } from 'interface-blockstore' | ||
import type { AbortOptions } from 'interface-store' | ||
import type { Bitswap, BitswapNotifyProgressEvents, BitswapWantBlockProgressEvents } from 'ipfs-bitswap' | ||
import type { CID } from 'multiformats/cid' | ||
import type { MultihashHasher } from 'multiformats/hashes/interface' | ||
import type { ProgressOptions } from 'progress-events' | ||
|
||
export class BitswapBlockProvider implements BlockProvider< | ||
ProgressOptions<BitswapNotifyProgressEvents>, | ||
ProgressOptions<BitswapWantBlockProgressEvents> | ||
>, Startable { | ||
private readonly bitswap: Bitswap | ||
private started: boolean | ||
|
||
constructor (libp2p: Libp2p, blockstore: Blockstore, hashers: MultihashHasher[]) { | ||
this.bitswap = createBitswap(libp2p, blockstore, { | ||
hashLoader: { | ||
getHasher: async (codecOrName: string | number): Promise<MultihashHasher<number>> => { | ||
const hasher = hashers.find(hasher => { | ||
return hasher.code === codecOrName || hasher.name === codecOrName | ||
}) | ||
|
||
if (hasher != null) { | ||
return hasher | ||
} | ||
|
||
throw new Error(`Could not load hasher for code/name "${codecOrName}"`) | ||
} | ||
} | ||
}) | ||
this.started = false | ||
} | ||
|
||
isStarted (): boolean { | ||
return this.started | ||
} | ||
|
||
async start (): Promise<void> { | ||
await this.bitswap.start() | ||
this.started = true | ||
} | ||
|
||
async stop (): Promise<void> { | ||
await this.bitswap.stop() | ||
this.started = false | ||
} | ||
|
||
notify (cid: CID, block: Uint8Array, options?: ProgressOptions<BitswapNotifyProgressEvents>): void { | ||
this.bitswap.notify(cid, block, options) | ||
} | ||
|
||
async get (cid: CID, options?: AbortOptions & ProgressOptions<BitswapWantBlockProgressEvents>): Promise<Uint8Array> { | ||
return this.bitswap.want(cid, options) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { BitswapBlockProvider } from './bitswap-block-provider.js' | ||
export { TrustedGatewayBlockProvider } from './trustless-gateway-block-provider.js' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { logger } from '@libp2p/logger' | ||
import type { BlockProvider } from '@helia/interface/blocks' | ||
import type { AbortOptions } from 'interface-store' | ||
import type { CID } from 'multiformats/cid' | ||
import type { ProgressEvent, ProgressOptions } from 'progress-events' | ||
|
||
const log = logger('helia:trustless-gateway-block-provider') | ||
|
||
export type TrustlessGatewayGetBlockProgressEvents = | ||
ProgressEvent<'trustless-gateway:get-block:fetch', URL> | ||
|
||
/** | ||
* A BlockProvider that accepts a list of trustless gateways that are queried | ||
* for blocks. Individual gateways are randomly chosen. | ||
*/ | ||
export class TrustedGatewayBlockProvider implements BlockProvider< | ||
ProgressOptions, | ||
ProgressOptions<TrustlessGatewayGetBlockProgressEvents> | ||
> { | ||
private readonly gateways: URL[] | ||
achingbrain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
constructor (urls: string[]) { | ||
this.gateways = urls.map(url => new URL(url.toString())) | ||
} | ||
achingbrain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
notify (cid: CID, block: Uint8Array, options?: ProgressOptions): void { | ||
// no-op | ||
} | ||
achingbrain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
async get (cid: CID, options: AbortOptions & ProgressOptions<TrustlessGatewayGetBlockProgressEvents> = {}): Promise<Uint8Array> { | ||
// choose a gateway | ||
const url = this.gateways[Math.floor(Math.random() * this.gateways.length)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Randomly chosen gateway. If a gateway returns a 5xx we may wish to temporarily remove it from rotation and try another? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we definitely should enable some ability to retry other given gateways. IIRC, we discussed in https://pl-strflt.notion.site/Helia-reliable-retrieval-technical-design-golden-path-255-15c4d3c25a404a85b6db8bf3f8d1f310?pvs=4 that just spamming all gateways would be fine for now because we're trying to optimize for successful retrieval. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, but I had a moment of doubt when I realised we'd be making 50 HTTP requests for a 10 block DAG. |
||
|
||
log('getting block for %c from %s', cid, url) | ||
|
||
try { | ||
const block = await getRawBlockFromGateway(url, cid, options.signal) | ||
log('got block for %c from %s', cid, url) | ||
|
||
return block | ||
} catch (err: any) { | ||
log.error('failed to get block for %c from %s', cid, url, err) | ||
|
||
throw err | ||
} | ||
} | ||
} | ||
|
||
async function getRawBlockFromGateway (url: URL, cid: CID, signal?: AbortSignal): Promise<Uint8Array> { | ||
const gwUrl = new URL(url) | ||
gwUrl.pathname = `/ipfs/${cid.toString()}` | ||
|
||
// necessary as not every gateway supports dag-cbor, but every should support | ||
// sending raw block as-is | ||
gwUrl.search = '?format=raw' | ||
|
||
if (signal?.aborted === true) { | ||
throw new Error(`Signal to fetch raw block for CID ${cid} from gateway ${gwUrl.toString()} was aborted prior to fetch`) | ||
} | ||
|
||
try { | ||
const res = await fetch(gwUrl.toString(), { | ||
signal, | ||
headers: { | ||
// also set header, just in case ?format= is filtered out by some | ||
// reverse proxy | ||
Accept: 'application/vnd.ipld.raw' | ||
}, | ||
cache: 'force-cache' | ||
}) | ||
if (!res.ok) { | ||
throw new Error(`unable to fetch raw block for CID ${cid} from gateway ${gwUrl.toString()}`) | ||
} | ||
return new Uint8Array(await res.arrayBuffer()) | ||
} catch (cause) { | ||
// @ts-expect-error - TS thinks signal?.aborted can only be false now | ||
// because it was checked for true above. | ||
if (signal?.aborted === true) { | ||
throw new Error(`fetching raw block for CID ${cid} from gateway ${gwUrl.toString()} was aborted`) | ||
} | ||
throw new Error(`unable to fetch raw block for CID ${cid}`) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bitswap already verifies blocks pulled from the network. If we like the approach in this PR we should PR bitswap to skip verification and let it be handled here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we did remove verification from bitswap, we should only do it via configuration flag (
verifyBlocks (default true)
) so current users don't have a hard migration to do validation themselves.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd release a major. Despite it being published for consumption outside IPFS projects realistically I don't anyone else is using it bar a few weekend hackers.