Skip to content
This repository was archived by the owner on Jan 7, 2026. It is now read-only.
Merged
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
9 changes: 7 additions & 2 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
export { encodeHex } from 'https://deno.land/std@0.203.0/encoding/hex.ts'
export { decodeBase64 } from 'https://deno.land/std@0.203.0/encoding/base64.ts'
export { decode as decodeVarint } from 'https://deno.land/x/varint@v2.0.0/varint.ts'
export { retry } from 'https://deno.land/std@0.203.0/async/retry.ts';


// Deno Bundle does not support npm dependencies, we have to load them via CDN
export { CarBlockIterator } from 'https://cdn.skypack.dev/@ipld/car@5.3.2/?dts'
export {
getIndexProviderPeerId,
MINER_TO_PEERID_CONTRACT_ADDRESS,
MINER_TO_PEERID_CONTRACT_ABI,
} from 'https://cdn.jsdelivr.net/npm/index-provider-peer-id@1.0.1/index.js/+esm'
export { ethers } from 'https://cdn.jsdelivr.net/npm/ethers@6.13.5/dist/ethers.min.js'

export {
UnsupportedHashError,
HashMismatchError,
Expand Down
104 changes: 27 additions & 77 deletions lib/miner-info.js
Original file line number Diff line number Diff line change
@@ -1,89 +1,39 @@
import { retry } from '../vendor/deno-deps.js'
import { RPC_URL, RPC_AUTH } from './constants.js'
import {
getIndexProviderPeerId as getPeerId,
MINER_TO_PEERID_CONTRACT_ADDRESS,
MINER_TO_PEERID_CONTRACT_ABI
, ethers
} from '../vendor/deno-deps.js'

async function getChainHead ({ maxAttempts = 5 } = {}) {
try {
const res = await retry(() => rpc('Filecoin.ChainHead'), {
// The maximum amount of attempts until failure.
maxAttempts,
// The initial and minimum amount of milliseconds between attempts.
minTimeout: 5_000,
// How much to backoff after each retry.
multiplier: 1.5
})
return res.Cids
} catch (err) {
if (err.name === 'RetryError' && err.cause) {
// eslint-disable-next-line no-ex-assign
err = err.cause
}
err.message = `Cannot obtain chain head: ${err.message}`
throw err
}
}
// Initialize your ethers contract instance
const fetchRequest = new ethers.FetchRequest(RPC_URL)
fetchRequest.setHeader('Authorization', `Bearer ${RPC_AUTH}`)
const provider = new ethers.JsonRpcProvider(fetchRequest)
const smartContractClient = new ethers.Contract(
MINER_TO_PEERID_CONTRACT_ADDRESS,
MINER_TO_PEERID_CONTRACT_ABI,
provider
)

/**
* @param {string} minerId A miner actor id, e.g. `f0142637`
* @param {object} options
* @param {number} [options.maxAttempts]
* @returns {Promise<string>} Miner's PeerId, e.g. `12D3KooWMsPmAA65yHAHgbxgh7CPkEctJHZMeM3rAvoW8CZKxtpG`
* @param {string} minerId - The ID of the miner.
* @param {object} options - Options for the function.
* @param {number} options.maxAttempts - The maximum number of attempts to fetch the peer ID.
* @returns {Promise<string>} The peer ID of the miner.
*/
export async function getMinerPeerId (minerId, { maxAttempts = 5 } = {}) {
const chainHead = await getChainHead({ maxAttempts })
export async function getIndexProviderPeerId (minerId, { maxAttempts = 5 } = {}) {
try {
const res = await retry(() => rpc('Filecoin.StateMinerInfo', minerId, chainHead), {
// The maximum amount of attempts until failure.
const { peerId, source } = await getPeerId(minerId, smartContractClient, {
rpcUrl: RPC_URL,
rpcAuth: RPC_AUTH,
maxAttempts,
// The initial and minimum amount of milliseconds between attempts.
minTimeout: 5_000,
// How much to backoff after each retry.
multiplier: 1.5
signal: AbortSignal.timeout(60_000)
})
return res.PeerId
console.log(`Peer ID fetched from ${source}.`)
return peerId
} catch (err) {
if (err.name === 'RetryError' && err.cause) {
// eslint-disable-next-line no-ex-assign
err = err.cause
}
err.message = `Cannot obtain miner info for ${minerId}: ${err.message}`
throw err
}
}

/**
* @param {string} method
* @param {unknown[]} params
*/
async function rpc (method, ...params) {
const req = new Request(RPC_URL, {
method: 'POST',
headers: {
'content-type': 'application/json',
accepts: 'application/json',
authorization: `Bearer ${RPC_AUTH}`
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params
})
})
const res = await fetch(req, {
signal: AbortSignal.timeout(60_000)
})

if (!res.ok) {
throw new Error(`JSON RPC failed with ${res.code}: ${(await res.text()).trimEnd()}`)
}

const body = await res.json()
if (body.error) {
const err = new Error(body.error.message)
err.name = 'FilecoinRpcError'
err.code = body.code
console.error(err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the caller of the function should log. With that removed, we can remove the try/catch altogether.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the use case scenario here different to CheckerNetwork/spark-checker#129 (comment) ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I don't think so. It also seems to me that your commit wasn't yet approved by Miro, right?

My understanding is that it makes sense to wrap a function body in try/catch if you want to modify the error to be consistent, and then rethrow it. Here we're just logging the error and then throwing it. That's not providing any extra value, so should be removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approved CheckerNetwork/spark-checker#129, it was already landed.

I agree we may not need the try/catch block here, but I consider it a detail I can live with.

Copy link
Contributor Author

@NikolasHaimerl NikolasHaimerl Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juliangruber do you want a change here or should we keep the code in sync with spark-checker?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, I would prefer to change here and in spark-checker, but don't want to delay the work stream any further. If we keep this as is, I'm sure someone will eventually refactor it.

throw err
}

return body.result
}
10 changes: 5 additions & 5 deletions lib/spot-checker.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global Zinnia */

import { queryTheIndex } from './ipni-client.js'
import { getMinerPeerId as defaultGetMinerPeerId } from './miner-info.js'
import { getIndexProviderPeerId as defaultGetIndexProviderPeerId } from './miner-info.js'
import { Tasker } from './tasker.js'

import {
Expand All @@ -19,15 +19,15 @@ import { validateHttpMultiaddr } from './multiaddr.js'

export default class SpotChecker {
#fetch
#getMinerPeerId
#getIndexProviderPeerId
#tasker

constructor ({
fetch = globalThis.fetch,
getMinerPeerId = defaultGetMinerPeerId
getIndexProviderPeerId = defaultGetIndexProviderPeerId
} = {}) {
this.#fetch = fetch
this.#getMinerPeerId = getMinerPeerId
this.#getIndexProviderPeerId = getIndexProviderPeerId
this.#tasker = new Tasker({ fetch: this.#fetch })
}

Expand All @@ -41,7 +41,7 @@ export default class SpotChecker {
async executeSpotCheck ({ task, stats, maxByteLength }) {
console.log(`Calling Filecoin JSON-RPC to get PeerId of miner ${task.minerId}`)
try {
const peerId = await this.#getMinerPeerId(task.minerId)
const peerId = await this.#getIndexProviderPeerId(task.minerId)
console.log(`Found peer id: ${peerId}`)
stats.providerId = peerId
} catch (err) {
Expand Down
8 changes: 4 additions & 4 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SpotChecker from './lib/spot-checker.js'
import { getMinerPeerId as defaultGetMinerPeerId } from './lib/miner-info.js'
import { getIndexProviderPeerId as defaultGetIndexProviderPeerId } from './lib/miner-info.js'
import { roundId, maxTasks, minerId, maxByteLength, retrievalTasks } from './config.js'

/**
Expand All @@ -12,10 +12,10 @@ import { roundId, maxTasks, minerId, maxByteLength, retrievalTasks } from './con
* Configuration options can be found in config.js
*
*/
const getMinerPeerId = (minerId) =>
const getIndexProviderPeerId = (minerId) =>
minerId === 'f0frisbii'
? '12D3KooWC8gXxg9LoJ9h3hy3jzBkEAxamyHEQJKtRmAuBuvoMzpr'
: defaultGetMinerPeerId(minerId)
: defaultGetIndexProviderPeerId(minerId)

const checker = new SpotChecker({ getMinerPeerId })
const checker = new SpotChecker({ getIndexProviderPeerId })
await checker.run({ roundId, maxTasks, retrievalTasks, minerId, maxByteLength })
8 changes: 4 additions & 4 deletions test/integration.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SpotChecker, { newStats } from '../lib/spot-checker.js'
import { test } from 'zinnia:test'
import { assertEquals } from 'zinnia:assert'
import { getMinerPeerId as defaultGetMinerPeerId } from '../lib/miner-info.js'
import { getIndexProviderPeerId as defaultGetIndexProviderPeerId } from '../lib/miner-info.js'

test('can execute spot check for our CID', async () => {
// The task to check, replace with your own values
Expand All @@ -10,13 +10,13 @@ test('can execute spot check for our CID', async () => {
minerId: 'f0frisbii'
}

const getMinerPeerId = (minerId) =>
const getIndexProviderPeerId = (minerId) =>
minerId === 'f0frisbii'
? '12D3KooWC8gXxg9LoJ9h3hy3jzBkEAxamyHEQJKtRmAuBuvoMzpr'
: defaultGetMinerPeerId(minerId)
: defaultGetIndexProviderPeerId(minerId)

// Run the check
const spark = new SpotChecker({ getMinerPeerId })
const spark = new SpotChecker({ getIndexProviderPeerId })
const stats = { ...task, ...newStats() }
await spark.executeSpotCheck({ task, stats })

Expand Down
10 changes: 5 additions & 5 deletions test/miner-info.test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { test } from 'zinnia:test'
import { assertMatch, AssertionError } from 'zinnia:assert'
import { getMinerPeerId } from '../lib/miner-info.js'
import { getIndexProviderPeerId } from '../lib/miner-info.js'

const KNOWN_MINER_ID = 'f0142637'

test('get peer id of a known miner', async () => {
const result = await getMinerPeerId(KNOWN_MINER_ID)
const result = await getIndexProviderPeerId(KNOWN_MINER_ID)
assertMatch(result, /^12D3KooW/)
})

test('get peer id of a miner that does not exist', async () => {
try {
const result = await getMinerPeerId('f010', { maxAttempts: 1 })
const result = await getIndexProviderPeerId('f010', { maxAttempts: 1 })
throw new AssertionError(
`Expected "getMinerPeerId()" to fail, but it resolved with "${result}" instead.`
`Expected "getIndexProviderPeerId()" to fail, but it resolved with "${result}" instead.`
)
} catch (err) {
assertMatch(err.toString(), /\bf010\b.*\bactor code is not miner/)
assertMatch(err.toString(), /Error fetching index provider PeerID for miner f010/)
}
})
Loading