Skip to content

Commit 5d8f494

Browse files
merged with main
2 parents 0030d2d + b98b08c commit 5d8f494

26 files changed

+5296
-256
lines changed

.github/workflows/ci.yml

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
name: CI
2-
on: [push]
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
36

47
env:
58
ZINNIA_VERSION: v0.20.2
69
jobs:
7-
build:
10+
test-linux:
811
runs-on: ubuntu-latest
912
steps:
1013
- uses: actions/checkout@v4
11-
- run: curl -L https://github.com/filecoin-station/zinnia/releases/download/${{ env.ZINNIA_VERSION }}/zinnia-linux-x64.tar.gz | tar -xz
12-
- uses: actions/setup-node@v4
13-
- run: npx standard
14+
- run:
15+
curl -L https://github.com/filecoin-station/zinnia/releases/download/${{
16+
env.ZINNIA_VERSION }}/zinnia-linux-x64.tar.gz | tar -xz
1417
- run: ./zinnia run test.js
1518

1619
test-windows:
@@ -25,3 +28,11 @@ jobs:
2528
extract: true
2629
token: ${{ secrets.GITHUB_TOKEN }}
2730
- run: ./zinnia.exe run test.js
31+
32+
lint:
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: actions/setup-node@v4
37+
- run: npm ci
38+
- run: npm run lint

.github/workflows/publish.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
steps:
99
- uses: actions/checkout@v4
1010
- run: curl -L ${{ github.event.release.tarball_url }} > source.tar.gz
11-
- uses: filecoin-station/publish-zinnia-module-action@2e0211ca6021228bca6c33b0aa55a73260518f68
11+
- uses: filecoin-station/publish-zinnia-module-action@v0.2.0
1212
id: publish
1313
with:
1414
source: source.tar.gz
@@ -37,7 +37,7 @@ jobs:
3737
}
3838
- uses: slackapi/slack-github-action@v1.27.0
3939
with:
40-
channel-id: spark-public
40+
channel-id: filecoin-slack-spark
4141
payload: |
4242
{
4343
"text": "SPARK checker version ${{ github.event.release.tag_name }} released",

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/vendor

.prettierrc.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
semi: false
2+
singleQuote: true
3+
printWidth: 100
4+
trailingComma: all
5+
proseWrap: always

README.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# spark
2+
23
SP Retrieval Checker Module
34

45
- [Roadmap](https://pl-strflt.notion.site/SPARK-Roadmap-ac729c11c49b409fbec54751d1bc6c8a)
@@ -9,8 +10,12 @@ SP Retrieval Checker Module
910
Install [Zinnia CLI](https://github.com/filecoin-station/zinnia).
1011

1112
```bash
13+
$ # Install dev tooling
14+
$ npm ci
1215
$ # Lint
13-
$ npx standard
16+
$ npm run lint
17+
$ # Fix linting issues
18+
$ npm run lint:fix
1419
$ # Run module
1520
$ zinnia run main.js
1621
$ # Test module
@@ -29,5 +34,4 @@ $ ./release.sh 1.0.0
2934

3035
Use GitHub's changelog feature to fill out the release notes.
3136

32-
Publish the new release and let the CI/CD workflow upload the sources
33-
to IPFS & IPNS.
37+
Publish the new release and let the CI/CD workflow upload the sources to IPFS & IPNS.

deps.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
// Run the following script after making change in this file:
44
// deno bundle deps.ts vendor/deno-deps.js
55
//
6+
// You must use a 1.x version of Deno, e.g. v1.43.1
67

78
export { encodeHex } from 'https://deno.land/std@0.203.0/encoding/hex.ts'
89
export { decodeBase64 } from 'https://deno.land/std@0.203.0/encoding/base64.ts'
910
export { decode as decodeVarint } from 'https://deno.land/x/varint@v2.0.0/varint.ts'
10-
export { retry } from 'https://deno.land/std@0.203.0/async/retry.ts';
11-
11+
export { retry } from 'https://deno.land/std@0.203.0/async/retry.ts'
1212

1313
// Deno Bundle does not support npm dependencies, we have to load them via CDN
14-
export { ethers } from "https://cdn.jsdelivr.net/npm/ethers@6.13.5/dist/ethers.min.js";
14+
export { ethers } from 'https://cdn.jsdelivr.net/npm/ethers@6.13.5/dist/ethers.min.js'
1515
export {
1616
getIndexProviderPeerId,
1717
MINER_TO_PEERID_CONTRACT_ADDRESS,
@@ -21,12 +21,16 @@ export { CarBlockIterator } from 'https://cdn.skypack.dev/@ipld/car@5.3.2/?dts'
2121
export {
2222
UnsupportedHashError,
2323
HashMismatchError,
24-
validateBlock
24+
validateBlock,
2525
} from 'https://cdn.skypack.dev/@web3-storage/car-block-validator@1.2.0/?dts'
2626
// cdn.skypack.dev cannot resolve import from @noble/hashes
2727
// jsdelivr.net seems to work better, it's also recommended by drand-client
2828
export {
2929
fetchBeaconByTime,
3030
HttpChainClient,
31-
HttpCachingChain
31+
HttpCachingChain,
3232
} from 'https://cdn.jsdelivr.net/npm/drand-client@1.2.6/index.js/+esm'
33+
34+
export { assertOkResponse } from 'https://cdn.skypack.dev/assert-ok-response@1.0.0/?dts'
35+
import pRetry from 'https://cdn.skypack.dev/p-retry@6.2.1/?dts'
36+
export { pRetry }

eslint.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import neostandard from 'neostandard'
2+
3+
export default neostandard({
4+
noStyle: true, // Disable style-related rules, we use Prettier
5+
ts: true,
6+
ignores: ['vendor/**', 'deps.ts'],
7+
})

lib/activity-state.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44
export class ActivityState {
55
#healthy = null
66

7-
onOutdatedClient () {
7+
onOutdatedClient() {
88
this.onError('SPARK is outdated. Please upgrade Filecoin Station to the latest version.')
99
}
1010

11-
onError (msg) {
11+
onError(msg) {
1212
if (this.#healthy === null || this.#healthy) {
1313
this.#healthy = false
1414
Zinnia.activity.error(msg ?? 'SPARK failed reporting retrieval')
1515
}
1616
}
1717

18-
onHealthy () {
18+
onHealthy() {
1919
if (this.#healthy === null) {
2020
this.#healthy = true
2121
Zinnia.activity.info('SPARK started reporting retrievals')

lib/constants.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
export const SPARK_VERSION = '1.17.2'
1+
export const SPARK_VERSION = '1.19.1'
22
export const MAX_CAR_SIZE = 200 * 1024 * 1024 // 200 MB
33
export const APPROX_ROUND_LENGTH_IN_MS = 20 * 60_000 // 20 minutes
4+
export const MAX_JITTER_BETWEEN_TASKS_IN_MS = 10_000 // 10 seconds
45
export const RPC_URL = 'https://api.node.glif.io/'
56
export const RPC_AUTH = 'KZLIUb9ejreYOm-mZFM3UNADE0ux6CrHjxnS2D2Qgb8='
67
export const MINER_TO_PEERID_CONTRACT_ADDRESS = '0x14183aD016Ddc83D638425D6328009aa390339Ce' // Contract address on the Filecoin EVM
8+
export const MAX_REQUEST_DURATION_MS = 90_000

lib/drand-client.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
fetchBeaconByTime,
3-
HttpChainClient,
4-
HttpCachingChain
5-
} from '../vendor/deno-deps.js'
1+
import { fetchBeaconByTime, HttpChainClient, HttpCachingChain } from '../vendor/deno-deps.js'
62

73
// See https://docs.filecoin.io/networks/mainnet#genesis
84
const FIL_MAINNET_GENESIS_TS = new Date('2020-08-24T22:00:00Z').getTime()
@@ -18,8 +14,9 @@ const DRAND_OPTIONS = {
1814
chainVerificationParams: {
1915
// quicknet
2016
chainHash: '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971',
21-
publicKey: '83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a'
22-
}
17+
publicKey:
18+
'83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a',
19+
},
2320
}
2421

2522
const DRAND_URL = `https://api2.drand.sh/${DRAND_OPTIONS.chainVerificationParams.chainHash}`
@@ -29,7 +26,7 @@ const client = new HttpChainClient(chain, DRAND_OPTIONS)
2926
/**
3027
* @param {number} roundStartEpoch
3128
*/
32-
export async function getRandomnessForSparkRound (roundStartEpoch) {
29+
export async function getRandomnessForSparkRound(roundStartEpoch) {
3330
const roundStartedAt = roundStartEpoch * FIL_MAINNET_BLOCK_TIME + FIL_MAINNET_GENESIS_TS
3431
const beacon = await fetchBeaconByTime(client, roundStartedAt)
3532
return beacon.randomness

lib/http-assertions.js

+8-20
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,19 @@
11
import { AssertionError } from 'zinnia:assert'
22

3-
/**
4-
* @param {Response} res
5-
* @param {string} [errorMsg]
6-
*/
7-
export async function assertOkResponse (res, errorMsg) {
8-
if (res.ok) return
9-
10-
let body
11-
try {
12-
body = await res.text()
13-
} catch {}
14-
const err = new Error(`${errorMsg ?? 'Fetch failed'} (${res.status}): ${body?.trimEnd()}`)
15-
err.statusCode = res.status
16-
err.serverMessage = body
17-
throw err
18-
}
3+
export { assertOkResponse } from '../vendor/deno-deps.js'
194

205
/**
216
* @param {Response} res
227
* @param {string} [errorMsg]
238
*/
24-
export async function assertRedirectResponse (res, errorMsg) {
9+
export async function assertRedirectResponse(res, errorMsg) {
2510
if ([301, 302, 303, 304, 307, 308].includes(res.status)) {
2611
const location = res.headers.get('location')
2712
if (!location) {
28-
const msg = (errorMsg ? errorMsg + ' ' : '') +
13+
const msg =
14+
(errorMsg ? errorMsg + ' ' : '') +
2915
'The server response is missing the Location header. Headers found:\n' +
30-
Array.from(res.headers.keys()).join('\n')
16+
Array.from(res.headers.keys()).join('\n')
3117
throw new AssertionError(msg)
3218
}
3319
return
@@ -37,7 +23,9 @@ export async function assertRedirectResponse (res, errorMsg) {
3723
try {
3824
body = await res.text()
3925
} catch {}
40-
const err = new Error(`${errorMsg ?? 'Server did not respond with redirect'} (${res.status}): ${body?.trimEnd()}`)
26+
const err = new Error(
27+
`${errorMsg ?? 'Server did not respond with redirect'} (${res.status}): ${body?.trimEnd()}`,
28+
)
4129
err.statusCode = res.status
4230
err.serverMessage = body
4331
throw err

lib/ipni-client.js

+29-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { decodeBase64, decodeVarint } from '../vendor/deno-deps.js'
1+
import { decodeBase64, decodeVarint, pRetry, assertOkResponse } from '../vendor/deno-deps.js'
22

33
/**
44
*
@@ -9,23 +9,25 @@ import { decodeBase64, decodeVarint } from '../vendor/deno-deps.js'
99
* provider?: { address: string; protocol: string };
1010
* }>}
1111
*/
12-
export async function queryTheIndex (cid, providerId) {
13-
const url = `https://cid.contact/cid/${encodeURIComponent(cid)}`
14-
12+
export async function queryTheIndex(cid, providerId) {
1513
let providerResults
1614
try {
17-
const res = await fetch(url)
18-
if (!res.ok) {
19-
console.error('IPNI query failed, HTTP response: %s %s', res.status, (await res.text()).trimEnd())
20-
return { indexerResult: `ERROR_${res.status}` }
21-
}
22-
23-
const result = await res.json()
24-
providerResults = result.MultihashResults.flatMap(r => r.ProviderResults)
25-
console.log('IPNI returned %s provider results: %o', providerResults.length, providerResults)
15+
providerResults = await pRetry(() => getRetrievalProviders(cid), {
16+
retries: 5,
17+
shouldRetry: (error) => {
18+
return error.statusCode && error.statusCode >= 500
19+
},
20+
onFailedAttempt: (error) => {
21+
console.error(error)
22+
console.error('IPNI query failed, retrying...')
23+
},
24+
})
25+
console.log('IPNI returned %s provider results', providerResults.length)
2626
} catch (err) {
2727
console.error('IPNI query failed.', err)
28-
return { indexerResult: 'ERROR_FETCH' }
28+
return {
29+
indexerResult: typeof err.statusCode === 'number' ? `ERROR_${err.statusCode}` : 'ERROR_FETCH',
30+
}
2931
}
3032

3133
let graphsyncProvider
@@ -37,7 +39,7 @@ export async function queryTheIndex (cid, providerId) {
3739
0x900: 'bitswap',
3840
0x910: 'graphsync',
3941
0x0920: 'http',
40-
4128768: 'graphsync'
42+
4128768: 'graphsync',
4143
}[protocolCode]
4244

4345
const address = p.Provider.Addrs[0]
@@ -47,14 +49,14 @@ export async function queryTheIndex (cid, providerId) {
4749
case 'http':
4850
return {
4951
indexerResult: 'OK',
50-
provider: { address, protocol }
52+
provider: { address, protocol },
5153
}
5254

5355
case 'graphsync':
5456
if (!graphsyncProvider) {
5557
graphsyncProvider = {
5658
address: `${address}/p2p/${p.Provider.ID}`,
57-
protocol
59+
protocol,
5860
}
5961
}
6062
}
@@ -63,10 +65,19 @@ export async function queryTheIndex (cid, providerId) {
6365
console.log('HTTP protocol is not advertised, falling back to Graphsync.')
6466
return {
6567
indexerResult: 'HTTP_NOT_ADVERTISED',
66-
provider: graphsyncProvider
68+
provider: graphsyncProvider,
6769
}
6870
}
6971

7072
console.log('All advertisements are from other miners or for unsupported protocols.')
7173
return { indexerResult: 'NO_VALID_ADVERTISEMENT' }
7274
}
75+
76+
async function getRetrievalProviders(cid) {
77+
const url = `https://cid.contact/cid/${encodeURIComponent(cid)}`
78+
const res = await fetch(url)
79+
await assertOkResponse(res)
80+
81+
const result = await res.json()
82+
return result.MultihashResults.flatMap((r) => r.ProviderResults)
83+
}

0 commit comments

Comments
 (0)