@helia/block-brokers 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +45 -0
- package/dist/index.min.js +3 -0
- package/dist/src/bitswap.d.ts +19 -0
- package/dist/src/bitswap.d.ts.map +1 -0
- package/dist/src/bitswap.js +48 -0
- package/dist/src/bitswap.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/trustless-gateway/broker.d.ts +15 -0
- package/dist/src/trustless-gateway/broker.d.ts.map +1 -0
- package/dist/src/trustless-gateway/broker.js +55 -0
- package/dist/src/trustless-gateway/broker.js.map +1 -0
- package/dist/src/trustless-gateway/index.d.ts +13 -0
- package/dist/src/trustless-gateway/index.d.ts.map +1 -0
- package/dist/src/trustless-gateway/index.js +15 -0
- package/dist/src/trustless-gateway/index.js.map +1 -0
- package/dist/src/trustless-gateway/trustless-gateway.d.ts +31 -0
- package/dist/src/trustless-gateway/trustless-gateway.d.ts.map +1 -0
- package/dist/src/trustless-gateway/trustless-gateway.js +114 -0
- package/dist/src/trustless-gateway/trustless-gateway.js.map +1 -0
- package/dist/src/utils/default-hashers.d.ts +3 -0
- package/dist/src/utils/default-hashers.d.ts.map +1 -0
- package/dist/src/utils/default-hashers.js +11 -0
- package/dist/src/utils/default-hashers.js.map +1 -0
- package/dist/src/utils/networked-storage.d.ts +69 -0
- package/dist/src/utils/networked-storage.d.ts.map +1 -0
- package/dist/src/utils/networked-storage.js +207 -0
- package/dist/src/utils/networked-storage.js.map +1 -0
- package/package.json +91 -0
- package/src/bitswap.ts +77 -0
- package/src/index.ts +3 -0
- package/src/trustless-gateway/broker.ts +65 -0
- package/src/trustless-gateway/index.ts +33 -0
- package/src/trustless-gateway/trustless-gateway.ts +126 -0
- package/src/utils/default-hashers.ts +12 -0
- package/src/utils/networked-storage.ts +266 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { TrustlessGateway } from './trustless-gateway.js'
|
|
2
|
+
import { DEFAULT_TRUSTLESS_GATEWAYS } from './index.js'
|
|
3
|
+
import type { TrustlessGatewayBlockBrokerInit, TrustlessGatewayComponents, TrustlessGatewayGetBlockProgressEvents } from './index.js'
|
|
4
|
+
import type { BlockRetrievalOptions, BlockRetriever } from '@helia/interface/blocks'
|
|
5
|
+
import type { Logger } from '@libp2p/interface'
|
|
6
|
+
import type { CID } from 'multiformats/cid'
|
|
7
|
+
import type { ProgressOptions } from 'progress-events'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A class that accepts a list of trustless gateways that are queried
|
|
11
|
+
* for blocks.
|
|
12
|
+
*/
|
|
13
|
+
export class TrustlessGatewayBlockBroker implements BlockRetriever<
|
|
14
|
+
ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
|
|
15
|
+
> {
|
|
16
|
+
private readonly gateways: TrustlessGateway[]
|
|
17
|
+
private readonly log: Logger
|
|
18
|
+
|
|
19
|
+
constructor (components: TrustlessGatewayComponents, init: TrustlessGatewayBlockBrokerInit = {}) {
|
|
20
|
+
this.log = components.logger.forComponent('helia:trustless-gateway-block-broker')
|
|
21
|
+
this.gateways = (init.gateways ?? DEFAULT_TRUSTLESS_GATEWAYS)
|
|
22
|
+
.map((gatewayOrUrl) => {
|
|
23
|
+
return new TrustlessGateway(gatewayOrUrl)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async retrieve (cid: CID, options: BlockRetrievalOptions<ProgressOptions<TrustlessGatewayGetBlockProgressEvents>> = {}): Promise<Uint8Array> {
|
|
28
|
+
// Loop through the gateways until we get a block or run out of gateways
|
|
29
|
+
// TODO: switch to toSorted when support is better
|
|
30
|
+
const sortedGateways = this.gateways.sort((a, b) => b.reliability() - a.reliability())
|
|
31
|
+
const aggregateErrors: Error[] = []
|
|
32
|
+
|
|
33
|
+
for (const gateway of sortedGateways) {
|
|
34
|
+
this.log('getting block for %c from %s', cid, gateway.url)
|
|
35
|
+
try {
|
|
36
|
+
const block = await gateway.getRawBlock(cid, options.signal)
|
|
37
|
+
this.log.trace('got block for %c from %s', cid, gateway.url)
|
|
38
|
+
try {
|
|
39
|
+
await options.validateFn?.(block)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
this.log.error('failed to validate block for %c from %s', cid, gateway.url, err)
|
|
42
|
+
gateway.incrementInvalidBlocks()
|
|
43
|
+
|
|
44
|
+
throw new Error(`unable to validate block for CID ${cid} from gateway ${gateway.url}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return block
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
this.log.error('failed to get block for %c from %s', cid, gateway.url, err)
|
|
50
|
+
if (err instanceof Error) {
|
|
51
|
+
aggregateErrors.push(err)
|
|
52
|
+
} else {
|
|
53
|
+
aggregateErrors.push(new Error(`unable to fetch raw block for CID ${cid} from gateway ${gateway.url}`))
|
|
54
|
+
}
|
|
55
|
+
// if signal was aborted, exit the loop
|
|
56
|
+
if (options.signal?.aborted === true) {
|
|
57
|
+
this.log.trace('request aborted while fetching raw block for CID %c from gateway %s', cid, gateway.url)
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new AggregateError(aggregateErrors, `unable to fetch raw block for CID ${cid} from any gateway`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TrustlessGatewayBlockBroker } from './broker.js'
|
|
2
|
+
import type { BlockRetriever } from '@helia/interface/src/blocks.js'
|
|
3
|
+
import type { ComponentLogger } from '@libp2p/interface'
|
|
4
|
+
import type { ProgressEvent } from 'progress-events'
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_TRUSTLESS_GATEWAYS = [
|
|
7
|
+
// 2023-10-03: IPNS, Origin, and Block/CAR support from https://ipfs-public-gateway-checker.on.fleek.co/
|
|
8
|
+
'https://dweb.link',
|
|
9
|
+
|
|
10
|
+
// 2023-10-03: IPNS, Origin, and Block/CAR support from https://ipfs-public-gateway-checker.on.fleek.co/
|
|
11
|
+
'https://cf-ipfs.com',
|
|
12
|
+
|
|
13
|
+
// 2023-10-03: IPNS, Origin, and Block/CAR support from https://ipfs-public-gateway-checker.on.fleek.co/
|
|
14
|
+
'https://4everland.io',
|
|
15
|
+
|
|
16
|
+
// 2023-10-03: IPNS, Origin, and Block/CAR support from https://ipfs-public-gateway-checker.on.fleek.co/
|
|
17
|
+
'https://w3s.link'
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export type TrustlessGatewayGetBlockProgressEvents =
|
|
21
|
+
ProgressEvent<'trustless-gateway:get-block:fetch', URL>
|
|
22
|
+
|
|
23
|
+
export interface TrustlessGatewayBlockBrokerInit {
|
|
24
|
+
gateways?: Array<string | URL>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TrustlessGatewayComponents {
|
|
28
|
+
logger: ComponentLogger
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function trustlessGateway (init: TrustlessGatewayBlockBrokerInit = {}): (components: TrustlessGatewayComponents) => BlockRetriever {
|
|
32
|
+
return (components) => new TrustlessGatewayBlockBroker(components, init)
|
|
33
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { CID } from 'multiformats/cid'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A `TrustlessGateway` keeps track of the number of attempts, errors, and
|
|
5
|
+
* successes for a given gateway url so that we can prioritize gateways that
|
|
6
|
+
* have been more reliable in the past, and ensure that requests are distributed
|
|
7
|
+
* across all gateways within a given `TrustlessGatewayBlockBroker` instance.
|
|
8
|
+
*/
|
|
9
|
+
export class TrustlessGateway {
|
|
10
|
+
public readonly url: URL
|
|
11
|
+
/**
|
|
12
|
+
* The number of times this gateway has been attempted to be used to fetch a
|
|
13
|
+
* block. This includes successful, errored, and aborted attempts. By counting
|
|
14
|
+
* even aborted attempts, slow gateways that are out-raced by others will be
|
|
15
|
+
* considered less reliable.
|
|
16
|
+
*/
|
|
17
|
+
#attempts = 0
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The number of times this gateway has errored while attempting to fetch a
|
|
21
|
+
* block. This includes `response.ok === false` and any other errors that
|
|
22
|
+
* throw while attempting to fetch a block. This does not include aborted
|
|
23
|
+
* attempts.
|
|
24
|
+
*/
|
|
25
|
+
#errors = 0
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The number of times this gateway has returned an invalid block. A gateway
|
|
29
|
+
* that returns the wrong blocks for a CID should be considered for removal
|
|
30
|
+
* from the list of gateways to fetch blocks from.
|
|
31
|
+
*/
|
|
32
|
+
#invalidBlocks = 0
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The number of times this gateway has successfully fetched a block.
|
|
36
|
+
*/
|
|
37
|
+
#successes = 0
|
|
38
|
+
|
|
39
|
+
constructor (url: URL | string) {
|
|
40
|
+
this.url = url instanceof URL ? url : new URL(url)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetch a raw block from `this.url` following the specification defined at
|
|
45
|
+
* https://specs.ipfs.tech/http-gateways/trustless-gateway/
|
|
46
|
+
*/
|
|
47
|
+
async getRawBlock (cid: CID, signal?: AbortSignal): Promise<Uint8Array> {
|
|
48
|
+
const gwUrl = this.url
|
|
49
|
+
gwUrl.pathname = `/ipfs/${cid.toString()}`
|
|
50
|
+
|
|
51
|
+
// necessary as not every gateway supports dag-cbor, but every should support
|
|
52
|
+
// sending raw block as-is
|
|
53
|
+
gwUrl.search = '?format=raw'
|
|
54
|
+
|
|
55
|
+
if (signal?.aborted === true) {
|
|
56
|
+
throw new Error(`Signal to fetch raw block for CID ${cid} from gateway ${this.url} was aborted prior to fetch`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
this.#attempts++
|
|
61
|
+
const res = await fetch(gwUrl.toString(), {
|
|
62
|
+
signal,
|
|
63
|
+
headers: {
|
|
64
|
+
// also set header, just in case ?format= is filtered out by some
|
|
65
|
+
// reverse proxy
|
|
66
|
+
Accept: 'application/vnd.ipld.raw'
|
|
67
|
+
},
|
|
68
|
+
cache: 'force-cache'
|
|
69
|
+
})
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
this.#errors++
|
|
72
|
+
throw new Error(`unable to fetch raw block for CID ${cid} from gateway ${this.url}`)
|
|
73
|
+
}
|
|
74
|
+
this.#successes++
|
|
75
|
+
return new Uint8Array(await res.arrayBuffer())
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
// @ts-expect-error - TS thinks signal?.aborted can only be false now
|
|
78
|
+
// because it was checked for true above.
|
|
79
|
+
if (signal?.aborted === true) {
|
|
80
|
+
throw new Error(`fetching raw block for CID ${cid} from gateway ${this.url} was aborted`)
|
|
81
|
+
}
|
|
82
|
+
this.#errors++
|
|
83
|
+
throw new Error(`unable to fetch raw block for CID ${cid}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Encapsulate the logic for determining whether a gateway is considered
|
|
89
|
+
* reliable, for prioritization. This is based on the number of successful attempts made
|
|
90
|
+
* and the number of errors encountered.
|
|
91
|
+
*
|
|
92
|
+
* Unused gateways have 100% reliability; They will be prioritized over
|
|
93
|
+
* gateways with a 100% success rate to ensure that we attempt all gateways.
|
|
94
|
+
*/
|
|
95
|
+
reliability (): number {
|
|
96
|
+
/**
|
|
97
|
+
* if we have never tried to use this gateway, it is considered the most
|
|
98
|
+
* reliable until we determine otherwise (prioritize unused gateways)
|
|
99
|
+
*/
|
|
100
|
+
if (this.#attempts === 0) {
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.#invalidBlocks > 0) {
|
|
105
|
+
// this gateway may not be trustworthy..
|
|
106
|
+
return -Infinity
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* We have attempted the gateway, so we need to calculate the reliability
|
|
111
|
+
* based on the number of attempts, errors, and successes. Gateways that
|
|
112
|
+
* return a single error should drop their reliability score more than a
|
|
113
|
+
* single success increases it.
|
|
114
|
+
*
|
|
115
|
+
* Play around with the below reliability function at https://www.desmos.com/calculator/d6hfhf5ukm
|
|
116
|
+
*/
|
|
117
|
+
return this.#successes / (this.#attempts + (this.#errors * 3))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Increment the number of invalid blocks returned by this gateway.
|
|
122
|
+
*/
|
|
123
|
+
incrementInvalidBlocks (): void {
|
|
124
|
+
this.#invalidBlocks++
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { identity } from 'multiformats/hashes/identity'
|
|
2
|
+
import { sha256, sha512 } from 'multiformats/hashes/sha2'
|
|
3
|
+
import type { MultihashHasher } from 'multiformats/hashes/interface'
|
|
4
|
+
|
|
5
|
+
export function defaultHashers (hashers: MultihashHasher[] = []): MultihashHasher[] {
|
|
6
|
+
return [
|
|
7
|
+
sha256,
|
|
8
|
+
sha512,
|
|
9
|
+
identity,
|
|
10
|
+
...hashers
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { CodeError, start, stop } from '@libp2p/interface'
|
|
2
|
+
import { anySignal } from 'any-signal'
|
|
3
|
+
import filter from 'it-filter'
|
|
4
|
+
import forEach from 'it-foreach'
|
|
5
|
+
import { CustomProgressEvent, type ProgressOptions } from 'progress-events'
|
|
6
|
+
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
|
|
7
|
+
import type { BlockBroker, Blocks, Pair, DeleteManyBlocksProgressEvents, DeleteBlockProgressEvents, GetBlockProgressEvents, GetManyBlocksProgressEvents, PutManyBlocksProgressEvents, PutBlockProgressEvents, GetAllBlocksProgressEvents, GetOfflineOptions, BlockRetriever, BlockAnnouncer, BlockRetrievalOptions } from '@helia/interface/blocks'
|
|
8
|
+
import type { AbortOptions, ComponentLogger, Logger, LoggerOptions, Startable } from '@libp2p/interface'
|
|
9
|
+
import type { Blockstore } from 'interface-blockstore'
|
|
10
|
+
import type { AwaitIterable } from 'interface-store'
|
|
11
|
+
import type { CID } from 'multiformats/cid'
|
|
12
|
+
import type { MultihashHasher } from 'multiformats/hashes/interface'
|
|
13
|
+
|
|
14
|
+
export interface NetworkedStorageStorageInit {
|
|
15
|
+
blockBrokers?: BlockBroker[]
|
|
16
|
+
hashers?: MultihashHasher[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GetOptions extends AbortOptions {
|
|
20
|
+
progress?(evt: Event): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isBlockRetriever (b: any): b is BlockRetriever {
|
|
24
|
+
return typeof b.retrieve === 'function'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isBlockAnnouncer (b: any): b is BlockAnnouncer {
|
|
28
|
+
return typeof b.announce === 'function'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NetworkedStorageComponents {
|
|
32
|
+
blockstore: Blockstore
|
|
33
|
+
logger: ComponentLogger
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Networked storage wraps a regular blockstore - when getting blocks if the
|
|
38
|
+
* blocks are not present Bitswap will be used to fetch them from network peers.
|
|
39
|
+
*/
|
|
40
|
+
export class NetworkedStorage implements Blocks, Startable {
|
|
41
|
+
private readonly child: Blockstore
|
|
42
|
+
private readonly blockRetrievers: BlockRetriever[]
|
|
43
|
+
private readonly blockAnnouncers: BlockAnnouncer[]
|
|
44
|
+
private readonly hashers: MultihashHasher[]
|
|
45
|
+
private started: boolean
|
|
46
|
+
private readonly log: Logger
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a new BlockStorage
|
|
50
|
+
*/
|
|
51
|
+
constructor (components: NetworkedStorageComponents, init: NetworkedStorageStorageInit) {
|
|
52
|
+
this.log = components.logger.forComponent('helia:networked-storage')
|
|
53
|
+
this.child = components.blockstore
|
|
54
|
+
this.blockRetrievers = (init.blockBrokers ?? []).filter(isBlockRetriever)
|
|
55
|
+
this.blockAnnouncers = (init.blockBrokers ?? []).filter(isBlockAnnouncer)
|
|
56
|
+
this.hashers = init.hashers ?? []
|
|
57
|
+
this.started = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isStarted (): boolean {
|
|
61
|
+
return this.started
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async start (): Promise<void> {
|
|
65
|
+
await start(this.child, ...new Set([...this.blockRetrievers, ...this.blockAnnouncers]))
|
|
66
|
+
this.started = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async stop (): Promise<void> {
|
|
70
|
+
await stop(this.child, ...new Set([...this.blockRetrievers, ...this.blockAnnouncers]))
|
|
71
|
+
this.started = false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
unwrap (): Blockstore {
|
|
75
|
+
return this.child
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Put a block to the underlying datastore
|
|
80
|
+
*/
|
|
81
|
+
async put (cid: CID, block: Uint8Array, options: AbortOptions & ProgressOptions<PutBlockProgressEvents> = {}): Promise<CID> {
|
|
82
|
+
if (await this.child.has(cid)) {
|
|
83
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:put:duplicate', cid))
|
|
84
|
+
return cid
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:put:providers:notify', cid))
|
|
88
|
+
|
|
89
|
+
this.blockAnnouncers.forEach(provider => {
|
|
90
|
+
provider.announce(cid, block, options)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:put:blockstore:put', cid))
|
|
94
|
+
|
|
95
|
+
return this.child.put(cid, block, options)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Put a multiple blocks to the underlying datastore
|
|
100
|
+
*/
|
|
101
|
+
async * putMany (blocks: AwaitIterable<{ cid: CID, block: Uint8Array }>, options: AbortOptions & ProgressOptions<PutManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
|
|
102
|
+
const missingBlocks = filter(blocks, async ({ cid }): Promise<boolean> => {
|
|
103
|
+
const has = await this.child.has(cid)
|
|
104
|
+
|
|
105
|
+
if (has) {
|
|
106
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:put-many:duplicate', cid))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return !has
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const notifyEach = forEach(missingBlocks, ({ cid, block }): void => {
|
|
113
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:put-many:providers:notify', cid))
|
|
114
|
+
this.blockAnnouncers.forEach(provider => {
|
|
115
|
+
provider.announce(cid, block, options)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
options.onProgress?.(new CustomProgressEvent('blocks:put-many:blockstore:put-many'))
|
|
120
|
+
yield * this.child.putMany(notifyEach, options)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get a block by cid
|
|
125
|
+
*/
|
|
126
|
+
async get (cid: CID, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetBlockProgressEvents> = {}): Promise<Uint8Array> {
|
|
127
|
+
if (options.offline !== true && !(await this.child.has(cid))) {
|
|
128
|
+
// we do not have the block locally, get it from a block provider
|
|
129
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:providers:get', cid))
|
|
130
|
+
const block = await raceBlockRetrievers(cid, this.blockRetrievers, this.hashers, {
|
|
131
|
+
...options,
|
|
132
|
+
log: this.log
|
|
133
|
+
})
|
|
134
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:blockstore:put', cid))
|
|
135
|
+
await this.child.put(cid, block, options)
|
|
136
|
+
|
|
137
|
+
// notify other block providers of the new block
|
|
138
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:providers:notify', cid))
|
|
139
|
+
this.blockAnnouncers.forEach(provider => {
|
|
140
|
+
provider.announce(cid, block, options)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return block
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:get:blockstore:get', cid))
|
|
147
|
+
|
|
148
|
+
return this.child.get(cid, options)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get multiple blocks back from an (async) iterable of cids
|
|
153
|
+
*/
|
|
154
|
+
async * getMany (cids: AwaitIterable<CID>, options: GetOfflineOptions & AbortOptions & ProgressOptions<GetManyBlocksProgressEvents> = {}): AsyncIterable<Pair> {
|
|
155
|
+
options.onProgress?.(new CustomProgressEvent('blocks:get-many:blockstore:get-many'))
|
|
156
|
+
|
|
157
|
+
yield * this.child.getMany(forEach(cids, async (cid): Promise<void> => {
|
|
158
|
+
if (options.offline !== true && !(await this.child.has(cid))) {
|
|
159
|
+
// we do not have the block locally, get it from a block provider
|
|
160
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:get-many:providers:get', cid))
|
|
161
|
+
const block = await raceBlockRetrievers(cid, this.blockRetrievers, this.hashers, {
|
|
162
|
+
...options,
|
|
163
|
+
log: this.log
|
|
164
|
+
})
|
|
165
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:get-many:blockstore:put', cid))
|
|
166
|
+
await this.child.put(cid, block, options)
|
|
167
|
+
|
|
168
|
+
// notify other block providers of the new block
|
|
169
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:get-many:providers:notify', cid))
|
|
170
|
+
this.blockAnnouncers.forEach(provider => {
|
|
171
|
+
provider.announce(cid, block, options)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Delete a block from the blockstore
|
|
179
|
+
*/
|
|
180
|
+
async delete (cid: CID, options: AbortOptions & ProgressOptions<DeleteBlockProgressEvents> = {}): Promise<void> {
|
|
181
|
+
options.onProgress?.(new CustomProgressEvent<CID>('blocks:delete:blockstore:delete', cid))
|
|
182
|
+
|
|
183
|
+
await this.child.delete(cid, options)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Delete multiple blocks from the blockstore
|
|
188
|
+
*/
|
|
189
|
+
async * deleteMany (cids: AwaitIterable<CID>, options: AbortOptions & ProgressOptions<DeleteManyBlocksProgressEvents> = {}): AsyncIterable<CID> {
|
|
190
|
+
options.onProgress?.(new CustomProgressEvent('blocks:delete-many:blockstore:delete-many'))
|
|
191
|
+
yield * this.child.deleteMany((async function * (): AsyncGenerator<CID> {
|
|
192
|
+
for await (const cid of cids) {
|
|
193
|
+
yield cid
|
|
194
|
+
}
|
|
195
|
+
}()), options)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async has (cid: CID, options: AbortOptions = {}): Promise<boolean> {
|
|
199
|
+
return this.child.has(cid, options)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async * getAll (options: AbortOptions & ProgressOptions<GetAllBlocksProgressEvents> = {}): AwaitIterable<Pair> {
|
|
203
|
+
options.onProgress?.(new CustomProgressEvent('blocks:get-all:blockstore:get-many'))
|
|
204
|
+
yield * this.child.getAll(options)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const getCidBlockVerifierFunction = (cid: CID, hashers: MultihashHasher[]): Required<BlockRetrievalOptions>['validateFn'] => {
|
|
209
|
+
const hasher = hashers.find(hasher => hasher.code === cid.multihash.code)
|
|
210
|
+
|
|
211
|
+
if (hasher == null) {
|
|
212
|
+
throw new CodeError(`No hasher configured for multihash code 0x${cid.multihash.code.toString(16)}, please configure one. You can look up which hash this is at https://github.com/multiformats/multicodec/blob/master/table.csv`, 'ERR_UNKNOWN_HASH_ALG')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return async (block: Uint8Array): Promise<void> => {
|
|
216
|
+
// verify block
|
|
217
|
+
const hash = await hasher.digest(block)
|
|
218
|
+
|
|
219
|
+
if (!uint8ArrayEquals(hash.digest, cid.multihash.digest)) {
|
|
220
|
+
// if a hash mismatch occurs for a TrustlessGatewayBlockBroker, we should try another gateway
|
|
221
|
+
throw new CodeError('Hash of downloaded block did not match multihash from passed CID', 'ERR_HASH_MISMATCH')
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Race block providers cancelling any pending requests once the block has been
|
|
228
|
+
* found.
|
|
229
|
+
*/
|
|
230
|
+
async function raceBlockRetrievers (cid: CID, providers: BlockRetriever[], hashers: MultihashHasher[], options: AbortOptions & LoggerOptions): Promise<Uint8Array> {
|
|
231
|
+
const validateFn = getCidBlockVerifierFunction(cid, hashers)
|
|
232
|
+
|
|
233
|
+
const controller = new AbortController()
|
|
234
|
+
const signal = anySignal([controller.signal, options.signal])
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
return await Promise.any(
|
|
238
|
+
providers.map(async provider => {
|
|
239
|
+
try {
|
|
240
|
+
let blocksWereValidated = false
|
|
241
|
+
const block = await provider.retrieve(cid, {
|
|
242
|
+
...options,
|
|
243
|
+
signal,
|
|
244
|
+
validateFn: async (block: Uint8Array): Promise<void> => {
|
|
245
|
+
await validateFn(block)
|
|
246
|
+
blocksWereValidated = true
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (!blocksWereValidated) {
|
|
251
|
+
// the blockBroker either did not throw an error when attempting to validate the block
|
|
252
|
+
// or did not call the validateFn at all. We should validate the block ourselves
|
|
253
|
+
await validateFn(block)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return block
|
|
257
|
+
} catch (err) {
|
|
258
|
+
options.log.error('could not retrieve verified block for %c', cid, err)
|
|
259
|
+
throw err
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
} finally {
|
|
264
|
+
signal.clear()
|
|
265
|
+
}
|
|
266
|
+
}
|