-
Notifications
You must be signed in to change notification settings - Fork 4
Add alternative provider retrieval check #132
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
base: main
Are you sure you want to change the base?
Conversation
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.
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
Comments suppressed due to low confidence (1)
lib/spark.js:450
- Consider handling the case where filteredProviders is empty so that pickRandomWeightedItem receives a non-empty list, preventing potential errors.
const filteredProviders = providers.filter((provider) => provider.protocol !== 'bitswap')
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.
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
Comments suppressed due to low confidence (2)
lib/spark.js:478
- The prefix check for contextId is case-sensitive but the test data uses a different casing (e.g., 'ghA=='). Consider normalizing the case or using a consistent prefix for proper weight assignment.
if (provider.contextId.startsWith('gHa')) weight += 1
lib/ipni-client.js:56
- The condition is redundant because the loop already continues for non-matching provider IDs. Removing this check could simplify the code.
if (p.Provider.ID === providerId) {
lib/ipni-client.js
Outdated
* provider?: Provider; | ||
* providers?: Provider[]; |
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 find it confusing to have two properties called provider
and providers
, I cannot tell what's the difference.
How about calling the new property alternativeProviders
or altProviders
?
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.
Great start!
lib/spark.js
Outdated
// we will try to perform network wide retrieval from other providers | ||
if (noValidAdvertisement) { | ||
console.log('No valid advertisement found. Performing network-wide retrieval check...') | ||
return await this.testNetworkRetrieval(providers, retrieval.cid, stats) |
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.
The more I work in this codebase, the more I regret the design pattern using a mutable stats
parameter passed around. I would like to eventually refactor the code so that every function returns a stats
object with the relevant subset of fields.
Would you mind applying that design for this new function?
stats.networkRetrieval = await this.testNetworkRetrieval(providers, retrieval.cid, stats)
Also, let's find a different name for stats.networkRetrieval
, e.g. stats.altProvider
or stats.altProviderCheck
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 have changed it to alternativeProviderCheck
.
lib/spark.js
Outdated
/** | ||
* Picks a random item from an array based on their weight. The higher the weight, the higher the chance of being selected. | ||
* | ||
* @template T The type of the item in the list. | ||
* @param {Array<{weight: number}>} items The list of items, where each item has a `weight`property. | ||
* @returns {T} The randomly selected item based on its weight. | ||
* | ||
*/ | ||
function pickRandomWeightedItem(items) { | ||
const totalWeight = items.reduce((acc, item) => acc + item.weight, 0) | ||
let random = Math.random() * totalWeight | ||
|
||
// Iterate over items, subtracting the item's weight from the random number | ||
// until we find the item where the random number is less than the item's weight | ||
for (let i = 0; i < items.length; i++) { | ||
random -= items[i].weight | ||
if (random <= 0) { | ||
return items[i] | ||
} | ||
} | ||
} |
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 find this very problematic:
(1)
When using Math.random()
, each checker node will pick a different provider. As a result, we cannot create committees to find an honest majority in the data reported by the network.
Please use a DRAND beacon to get deterministic randomness so that all nodes pick the same alternative provider to check.
See how we are using DRAND beacon in other parts of this codebase. The important part is to use the DRAND beacon tied to the time when the Spark round started.
(2)
I am not sure if it's a good idea to do a random selection with weights. Shouldn't we always prefer Filecoin SPs serving HTTP retrievals over everybody else?
I would replace weights with the following algorithm:
- Is there a provider with ContextID starting with
ghsA
?- Yes:
Does any of those providers support HTTP?- Yes -> pick one of those HTTP providers at random
- No -> pick any of the Graphsync providers at random
- No:
Does any of the providers support HTTP?- Yes -> pick one of those HTTP providers at random
- No -> pick any of the Graphsync providers at random
- Yes:
Alternatively, keep using weight, but pick randomly only from the providers with the same highest weight.
Let's discuss what would be the best approach 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.
Thanks for your input!
(1)
I wasn't aware that we also need to form committees for the second (alternative) measurement as commitees were formed to evaluate measurement for a single storage provider. I therefore assumed that we can pick any provider at random as this measurement should not affect existing RSR.
In case we're going to use committees to evaluate this measurement I agree with using DRAND.
(2)
Alternative solution to your proposal would be to adjust the weights but I think the algorithm you proposed may be easier to understand. I don't mind changing the algorithm.
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.
- Would be nice if we could use committees
- I agree with the more explicit algorithm, it's easier to reason about
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 added both a pseudo-RNG and more a explicit algorithm.
lib/spark.js
Outdated
return { | ||
statusCode: null, | ||
timeout: false, | ||
endAt: null, | ||
carTooLarge: false, | ||
} |
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.
Please include the selected providerId
in the new stats object, see CheckerNetwork/roadmap#254 (comment)
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.
This is a bit problematic as for providerId we need to make sure that provider is a Filecoin storage provider and that we have their miner Id.
Are you aware of some way to reverse Peer ID to Miner ID?
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.
Let's use the retrieval provider peer ID found in the IPNI response instead of the Filecoin miner ID.
We are already doing that for the "regular" retrieval checks, see here:
Lines 54 to 55 in 9c29967
console.log(`Found peer id: ${peerId}`) | |
stats.providerId = peerId |
Co-authored-by: Miroslav Bajtoš <[email protected]>
Co-authored-by: Miroslav Bajtoš <[email protected]>
Co-authored-by: Miroslav Bajtoš <[email protected]>
lib/ipni-client.js
Outdated
} | ||
} | ||
} |
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.
It's confusing to me that in case of HTTP we return immediately, but in case of graph sync we don't. I don't see a reason for this behavior. Could we also directly return in the case of graphsync?
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.
This was the way we use to do it even before introduction of "alternative providers" to the response. As I understood it: we're iterating trough list of providers and in case we find record serving content via graphsync first we'll return it only if there's no HTTP alternative. In case there's a record serving content via HTTP we return it immediately.
lib/spark.js
Outdated
/** | ||
* Picks a random item from an array based on their weight. The higher the weight, the higher the chance of being selected. | ||
* | ||
* @template T The type of the item in the list. | ||
* @param {Array<{weight: number}>} items The list of items, where each item has a `weight`property. | ||
* @returns {T} The randomly selected item based on its weight. | ||
* | ||
*/ | ||
function pickRandomWeightedItem(items) { | ||
const totalWeight = items.reduce((acc, item) => acc + item.weight, 0) | ||
let random = Math.random() * totalWeight | ||
|
||
// Iterate over items, subtracting the item's weight from the random number | ||
// until we find the item where the random number is less than the item's weight | ||
for (let i = 0; i < items.length; i++) { | ||
random -= items[i].weight | ||
if (random <= 0) { | ||
return items[i] | ||
} | ||
} | ||
} |
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.
- Would be nice if we could use committees
- I agree with the more explicit algorithm, it's easier to reason about
lib/spark.js
Outdated
} | ||
|
||
stats.networkRetrieval = newNetworkRetrievalStats() | ||
const randomProvider = pickRandomProvider(providers) |
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'm surprised by this strategy. I might be out of sync with the current product plan, but wasn't it the goal to see if any provider can fetch the data? Here we're marking a failure if the 2nd provider we try fails, but don't try the other ones.
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've posted my opinions on that in this comment and I'm happy to discuss more!
Other motivation behind this was not to put too much pressure on the network serving content and on the checkers. I'm afraid of the worst case situation where we'd have to check every provider serving content.
Co-authored-by: Julian Gruber <[email protected]>
Co-authored-by: Julian Gruber <[email protected]>
Converting to draft as there's some major refactoring that needs to take place. |
…n-station/spark into add/network-wide-retrieval-check
@@ -34,3 +34,6 @@ export { | |||
export { assertOkResponse } from 'https://cdn.skypack.dev/[email protected]/?dts' | |||
import pRetry from 'https://cdn.skypack.dev/[email protected]/?dts' | |||
export { pRetry } | |||
|
|||
import Prando from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm' | |||
export { Prando } |
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 have opted for using package instead of the custom implementation for the pRNG. There's lack of good packages for pRNG so I have settled in the end for Prando. I also wanted to use Deno's random package but from what I realize they have added it to newer versions of the std package which we don't use yet.
This may be a good thing to update in the future.
*/ | ||
async next() { | ||
await this.#updateCurrentRound() | ||
return this.#remainingRoundTasks.pop() | ||
const retrievalTask = this.#remainingRoundTasks.pop() | ||
return { retrievalTask, randomness: this.#randomness } |
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 somehow need to export the round randomness so I have opted for returning object with randomness
attribute from the next
function.
Maybe adding the randomness
property to the retrieval task wouldn't be a bad thing either.
* @param {number} randomness | ||
* @returns {Provider | undefined} | ||
*/ | ||
export function pickRandomProvider(providers, randomness) { |
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.
pickRandomProvider
now picks random provider based on the priority rather then weight and generated pseudo-random number.
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.
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Co-authored-by: Copilot <[email protected]>
This pull requests includes a new check that measures retrievability for alternative provider.
Alternative provider retrieval check is performed ONLY when there is no valid advertisement found for the storage provider (miner) we are checking.
If there is no no valid advertisement found we are going to pick one random provider at random that advertises the CID we want to retrieve. Note that providers are ranked by proprity based on their attributes (context ID they advertise, protocol they use) so some providers may have higher chance of getting picked. Pseudo-random number generator is used to pick the retrieval provider from the given list.
In case there is a valid advertisement, standard retrieval result status will be used to calculate alternative-provider retrieval score (see CheckerNetwork/spark-evaluate#518).
Changelog
Provider
type and updated thequeryTheIndex
function to return a list of alternative providers in case no valid advertisement are found.checkRetrievalFromAlternativeProvider
in theSpark
class to perform alternative provider retrieval check on a random provider when no valid advertisement is found.pickRandomProvider
, to select a provider based on their priority.checkRetrievalFromAlternativeProvider
method and updated existing tests to include thecontextId
field in the provider object.prando
package to generate pseudo-random numbers.Closes #130
Relates to: