@@ -4,8 +4,14 @@ import { ActivityState } from './activity-state.js'
4
4
import { SPARK_VERSION , MAX_CAR_SIZE , APPROX_ROUND_LENGTH_IN_MS } from './constants.js'
5
5
import { queryTheIndex } from './ipni-client.js'
6
6
import { getMinerPeerId as defaultGetMinerPeerId } from './miner-info.js'
7
+ import { multiaddrToHttpUri } from './multiaddr.js'
8
+
7
9
import {
8
- encodeHex
10
+ CarBlockIterator ,
11
+ encodeHex ,
12
+ HashMismatchError ,
13
+ UnsupportedHashError ,
14
+ validateBlock
9
15
} from '../vendor/deno-deps.js'
10
16
11
17
const sleep = dt => new Promise ( resolve => setTimeout ( resolve , dt ) )
@@ -77,25 +83,15 @@ export default class Spark {
77
83
stats . protocol = provider . protocol
78
84
stats . providerAddress = provider . address
79
85
80
- const searchParams = new URLSearchParams ( {
81
- // See https://github.com/filecoin-project/lassie/blob/main/docs/HTTP_SPEC.md#dag-scope-request-query-parameter
82
- // Only the root block at the end of the path is returned after blocks required to verify the specified path segments.
83
- 'dag-scope' : 'block' ,
84
- protocols : provider . protocol ,
85
- providers : provider . address
86
- } )
87
- const url = `ipfs://${ retrieval . cid } ?${ searchParams . toString ( ) } `
88
86
try {
89
- await this . fetchCAR ( url , stats )
87
+ await this . fetchCAR ( provider . protocol , provider . address , retrieval . cid , stats )
90
88
} catch ( err ) {
91
- console . error ( `Failed to fetch ${ url } ` )
89
+ console . error ( `Failed to fetch ${ retrieval . cid } from ${ provider . address } using ${ provider . protocol } ` )
92
90
console . error ( err )
93
91
}
94
92
}
95
93
96
- async fetchCAR ( url , stats ) {
97
- console . log ( `Fetching: ${ url } ` )
98
-
94
+ async fetchCAR ( protocol , address , cid , stats ) {
99
95
// Abort if no progress was made for 60 seconds
100
96
const controller = new AbortController ( )
101
97
const { signal } = controller
@@ -116,6 +112,9 @@ export default class Spark {
116
112
const carBytes = new Uint8Array ( carBuffer )
117
113
118
114
try {
115
+ const url = getRetrievalUrl ( protocol , address , cid )
116
+ console . log ( `Fetching: ${ url } ` )
117
+
119
118
resetTimeout ( )
120
119
const res = await this . #fetch( url , { signal } )
121
120
stats . statusCode = res . status
@@ -146,6 +145,14 @@ export default class Spark {
146
145
}
147
146
148
147
if ( ! stats . carTooLarge ) {
148
+ try {
149
+ await verifyContent ( cid , carBytes )
150
+ stats . contentVerification = 'OK'
151
+ } catch ( err ) {
152
+ console . error ( 'Content verification failed.' , err )
153
+ stats . contentVerification = 'ERROR_' + ( err . code ?? 'UNKNOWN' )
154
+ }
155
+
149
156
const digest = await crypto . subtle . digest ( 'sha-256' , carBytes )
150
157
// 12 is the code for sha2-256
151
158
// 20 is the digest length (32 bytes = 256 bits)
@@ -155,6 +162,11 @@ export default class Spark {
155
162
console . error ( 'Retrieval failed with status code %s: %s' ,
156
163
res . status , ( await res . text ( ) ) . trimEnd ( ) )
157
164
}
165
+ } catch ( err ) {
166
+ if ( ! stats . statusCode ) {
167
+ stats . statusCode = mapErrorToStatusCode ( err )
168
+ }
169
+ throw err
158
170
} finally {
159
171
clearTimeout ( timeout )
160
172
}
@@ -240,6 +252,73 @@ export default class Spark {
240
252
}
241
253
}
242
254
255
+ function getRetrievalUrl ( protocol , address , cid ) {
256
+ if ( protocol === 'http' ) {
257
+ const baseUrl = multiaddrToHttpUri ( address )
258
+ return `${ baseUrl } /ipfs/${ cid } ?dag-scope=block`
259
+ }
260
+
261
+ const searchParams = new URLSearchParams ( {
262
+ // See https://github.com/filecoin-project/lassie/blob/main/docs/HTTP_SPEC.md#dag-scope-request-query-parameter
263
+ // Only the root block at the end of the path is returned after blocks required to verify the specified path segments.
264
+ 'dag-scope' : 'block' ,
265
+ protocols : protocol ,
266
+ providers : address
267
+ } )
268
+ return `ipfs://${ cid } ?${ searchParams . toString ( ) } `
269
+ }
270
+
271
+ /**
272
+ * @param {string } cid
273
+ * @param {Uint8Array } carBytes
274
+ */
275
+ async function verifyContent ( cid , carBytes ) {
276
+ const reader = await CarBlockIterator . fromBytes ( carBytes )
277
+ for await ( const block of reader ) {
278
+ if ( block . cid . toString ( ) !== cid . toString ( ) ) {
279
+ throw Object . assign (
280
+ new Error ( `Unexpected block CID ${ block . cid } . Expected: ${ cid } ` ) ,
281
+ { code : 'UNEXPECTED_CAR_BLOCK' }
282
+ )
283
+ }
284
+
285
+ await validateBlock ( block )
286
+ }
287
+ }
288
+
289
+ function mapErrorToStatusCode ( err ) {
290
+ // 90x codes for multiaddr parsing errors
291
+ switch ( err . code ) {
292
+ case 'UNSUPPORTED_MULTIADDR_PROTO' :
293
+ return 901
294
+ case 'UNSUPPORTED_MULTIADDR_SCHEME' :
295
+ return 902
296
+ case 'MULTIADDR_HAS_TOO_MANY_PARTS' :
297
+ return 903
298
+ }
299
+
300
+ // 92x for content verification errors
301
+ if ( err instanceof UnsupportedHashError ) {
302
+ return 921
303
+ } else if ( err instanceof HashMismatchError ) {
304
+ return 922
305
+ } else if ( err . code === 'UNEXPECTED_CAR_BLOCK' ) {
306
+ return 923
307
+ }
308
+
309
+ // 91x errors for network connection errors
310
+ // Unfortunately, the Fetch API does not support programmatic detection of various error
311
+ // conditions. We have to check the error message text.
312
+ if ( err . message . includes ( 'dns error' ) ) {
313
+ return 911
314
+ } else if ( err . message . includes ( 'tcp connect error' ) ) {
315
+ return 912
316
+ }
317
+
318
+ // Fallback code for unknown errors
319
+ return 900
320
+ }
321
+
243
322
async function assertOkResponse ( res , errorMsg ) {
244
323
if ( res . ok ) return
245
324
0 commit comments