diff --git a/lib/committee.js b/lib/committee.js index 74ac2f52..83aeb218 100644 --- a/lib/committee.js +++ b/lib/committee.js @@ -10,7 +10,9 @@ const debug = createDebug('spark:committee') /** @typedef {Map} TaskIdToCommitteeMap */ /** @typedef {{ + hasIndexMajority: boolean; indexerResult: string | CommitteeCheckError; + hasRetrievalMajority: boolean; retrievalResult: RetrievalResult }} CommitteeEvaluation */ @@ -68,7 +70,9 @@ export class Committee { requiredCommitteeSize ) this.evaluation = { + hasIndexMajority: false, indexerResult: 'COMMITTEE_TOO_SMALL', + hasRetrievalMajority: false, retrievalResult: 'COMMITTEE_TOO_SMALL' } for (const m of this.#measurements) m.fraudAssessment = 'COMMITTEE_TOO_SMALL' @@ -84,6 +88,7 @@ export class Committee { 'protocol' ] const indexerResultMajority = this.#findMajority(indexerResultProps) + const hasIndexMajority = !!indexerResultMajority const indexerResult = indexerResultMajority ? indexerResultMajority.majorityValue.indexerResult : 'MAJORITY_NOT_FOUND' @@ -105,6 +110,7 @@ export class Committee { ] const retrievalResultMajority = this.#findMajority(retrievalResultProps) + const hasRetrievalMajority = !!retrievalResultMajority /** @type {CommitteeEvaluation['retrievalResult']} */ let retrievalResult if (retrievalResultMajority) { @@ -118,7 +124,9 @@ export class Committee { } this.evaluation = { + hasIndexMajority, indexerResult, + hasRetrievalMajority, retrievalResult } } diff --git a/lib/evaluate.js b/lib/evaluate.js index 28f9851d..b5d817d9 100644 --- a/lib/evaluate.js +++ b/lib/evaluate.js @@ -236,7 +236,14 @@ export const evaluate = async ({ if (createPgClient) { try { - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements: measurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements: measurements, + findDealClients: (minerId, cid) => sparkRoundDetails.retrievalTasks + .find(t => t.cid === cid && t.minerId === minerId)?.clients + }) } catch (err) { console.error('Cannot update public stats.', err) ignoredErrors.push(err) diff --git a/lib/public-stats.js b/lib/public-stats.js index d16a6670..b19de0af 100644 --- a/lib/public-stats.js +++ b/lib/public-stats.js @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/node' import createDebug from 'debug' import { updatePlatformStats } from './platform-stats.js' @@ -14,8 +15,9 @@ const debug = createDebug('spark:public-stats') * @param {Iterable} args.committees * @param {import('./preprocess.js').Measurement[]} args.honestMeasurements * @param {import('./preprocess.js').Measurement[]} args.allMeasurements + * @param {(minerId: string, cid: string) => (string[] | undefined)} args.findDealClients */ -export const updatePublicStats = async ({ createPgClient, committees, honestMeasurements, allMeasurements }) => { +export const updatePublicStats = async ({ createPgClient, committees, honestMeasurements, allMeasurements, findDealClients }) => { /** @type {Map} */ const minerRetrievalStats = new Map() for (const c of committees) { @@ -35,7 +37,7 @@ export const updatePublicStats = async ({ createPgClient, committees, honestMeas await updateRetrievalStats(pgClient, minerId, retrievalStats) } await updateIndexerQueryStats(pgClient, committees) - await updateDailyDealsStats(pgClient, committees) + await updateDailyDealsStats(pgClient, committees, findDealClients) await updatePlatformStats(pgClient, honestMeasurements, allMeasurements) } finally { await pgClient.end() @@ -104,37 +106,134 @@ const updateIndexerQueryStats = async (pgClient, committees) => { /** * @param {pg.Client} pgClient * @param {Iterable} committees + * @param {(minerId: string, cid: string) => (string[] | undefined)} findDealClients */ -const updateDailyDealsStats = async (pgClient, committees) => { - let total = 0 - let indexed = 0 - let retrievable = 0 +const updateDailyDealsStats = async (pgClient, committees, findDealClients) => { + /** @type {Map>} */ + const minerClientDealStats = new Map() for (const c of committees) { - total++ + const { minerId, cid } = c.retrievalTask + const clients = findDealClients(minerId, cid) + if (!clients || !clients.length) { + console.warn(`Invalid retrieval task (${minerId}, ${cid}): no deal clients found. Excluding the task from daily per-deal stats.`) + Sentry.captureException(new Error('Invalid retrieval task: no deal clients found.'), { + extra: { + minerId, + cid + } + }) + continue + } - const evaluation = c.evaluation - if (!evaluation) continue - if (evaluation.indexerResult === 'OK' || evaluation.indexerResult === 'HTTP_NOT_ADVERTISED') { - indexed++ + let clientDealStats = minerClientDealStats.get(minerId) + if (!clientDealStats) { + clientDealStats = new Map() + minerClientDealStats.set(minerId, clientDealStats) } - if (evaluation.retrievalResult === 'OK') { - retrievable++ + + for (const clientId of clients) { + let stats = clientDealStats.get(clientId) + if (!stats) { + stats = { + tested: 0, + index_majority_found: 0, + retrieval_majority_found: 0, + indexed: 0, + indexed_http: 0, + retrievable: 0 + } + clientDealStats.set(clientId, stats) + } + + stats.tested++ + + const evaluation = c.evaluation + if (!evaluation) continue + + if (evaluation.hasIndexMajority) { + stats.index_majority_found++ + } + + if (evaluation.indexerResult === 'OK' || evaluation.indexerResult === 'HTTP_NOT_ADVERTISED') { + stats.indexed++ + } + + if (evaluation.indexerResult === 'OK') { + stats.indexed_http++ + } + + if (evaluation.hasRetrievalMajority) { + stats.retrieval_majority_found++ + } + + if (evaluation.retrievalResult === 'OK') { + stats.retrievable++ + } } } - debug('Updating public stats - daily deals: total += %s indexed += %s retrievable += %s', total, indexed, retrievable) + // Convert the nested map to an array for the query + const flatStats = Array.from(minerClientDealStats.entries()).flatMap( + ([minerId, clientDealStats]) => Array.from(clientDealStats.entries()).flatMap( + ([clientId, stats]) => ({ minerId, clientId, ...stats }) + ) + ) + + if (debug.enabled) { + debug( + 'Updating public stats - daily deals: tested += %s index_majority_found += %s indexed += %s retrieval_majority_found += %s retrievable += %s', + flatStats.reduce((sum, stat) => sum + stat.tested, 0), + flatStats.reduce((sum, stat) => sum + stat.index_majority_found, 0), + flatStats.reduce((sum, stat) => sum + stat.indexed, 0), + flatStats.reduce((sum, stat) => sum + stat.retrieval_majority_found, 0), + flatStats.reduce((sum, stat) => sum + stat.retrievable, 0) + ) + } + await pgClient.query(` - INSERT INTO daily_deals - (day, total, indexed, retrievable) - VALUES - (now(), $1, $2, $3) - ON CONFLICT(day) DO UPDATE SET - total = daily_deals.total + $1, - indexed = daily_deals.indexed + $2, - retrievable = daily_deals.retrievable + $3 + INSERT INTO daily_deals ( + day, + miner_id, + client_id, + tested, + index_majority_found, + indexed, + indexed_http, + retrieval_majority_found, + retrievable + ) VALUES ( + now(), + unnest($1::text[]), + unnest($2::text[]), + unnest($3::int[]), + unnest($4::int[]), + unnest($5::int[]), + unnest($6::int[]), + unnest($7::int[]), + unnest($8::int[]) + ) + ON CONFLICT(day, miner_id, client_id) DO UPDATE SET + tested = daily_deals.tested + EXCLUDED.tested, + index_majority_found = daily_deals.index_majority_found + EXCLUDED.index_majority_found, + indexed = daily_deals.indexed + EXCLUDED.indexed, + indexed_http = daily_deals.indexed_http + EXCLUDED.indexed_http, + retrieval_majority_found = daily_deals.retrieval_majority_found + EXCLUDED.retrieval_majority_found, + retrievable = daily_deals.retrievable + EXCLUDED.retrievable `, [ - total, - indexed, - retrievable + flatStats.map(stat => stat.minerId), + flatStats.map(stat => stat.clientId), + flatStats.map(stat => stat.tested), + flatStats.map(stat => stat.index_majority_found), + flatStats.map(stat => stat.indexed), + flatStats.map(stat => stat.indexed_http), + flatStats.map(stat => stat.retrieval_majority_found), + flatStats.map(stat => stat.retrievable) ]) } diff --git a/lib/typings.d.ts b/lib/typings.d.ts index 873c91f5..df7e55bf 100644 --- a/lib/typings.d.ts +++ b/lib/typings.d.ts @@ -11,6 +11,7 @@ export { export interface RetrievalTask { cid: string; minerId: string; + clients?: string[]; } /** diff --git a/migrations/016.do.deal-retrievability-score.sql b/migrations/016.do.deal-retrievability-score.sql new file mode 100644 index 00000000..9a7c1262 --- /dev/null +++ b/migrations/016.do.deal-retrievability-score.sql @@ -0,0 +1,41 @@ +-- For each (day, miner_id, client_id), we want to know the following numbers (counts): +-- * `tested`: (NEW) total deals tested +-- * `indexed`: deals announcing retrievals to IPNI (HTTP or Graphsync retrievals) +-- * `indexed_http`: (NEW) deals announcing HTTP retrievals to IPNI +-- * `majority_found`: (NEW) deals where we found a majority agreeing on the same result +-- * `retrievable`: deals where the majority agrees the content can be retrieved + +ALTER TABLE daily_deals ADD COLUMN miner_id TEXT; +UPDATE daily_deals SET miner_id = 'all-combined'; +ALTER TABLE daily_deals ALTER COLUMN miner_id SET NOT NULL; + +ALTER TABLE daily_deals ADD COLUMN client_id TEXT; +UPDATE daily_deals SET client_id = 'all-combined'; +ALTER TABLE daily_deals ALTER COLUMN client_id SET NOT NULL; + +-- Change the primary key to a composite pair (day, miner_id, client_id) +ALTER TABLE daily_deals DROP CONSTRAINT daily_deals_pkey; +ALTER TABLE daily_deals ADD PRIMARY KEY (day, miner_id, client_id); + +CREATE INDEX daily_deals_day ON daily_deals (day); + +ALTER TABLE daily_deals ADD COLUMN retrieval_majority_found INT; +UPDATE daily_deals SET retrieval_majority_found = total; +ALTER TABLE daily_deals ALTER COLUMN retrieval_majority_found SET NOT NULL; + +ALTER TABLE daily_deals ADD COLUMN index_majority_found INT; +UPDATE daily_deals SET index_majority_found = total; +ALTER TABLE daily_deals ALTER COLUMN index_majority_found SET NOT NULL; + +-- Note: backfilling `tested = total` is not entirely accurate: +-- * Before we introduced committees & majorities, tested = total +-- * After that change we started to calculate total = majority_found +ALTER TABLE daily_deals RENAME COLUMN total to tested; + +ALTER TABLE daily_deals ADD COLUMN indexed_http INT; +-- We don't how many of the deals tested in the past offered HTTP retrievals. +-- Historically, this value was between 1/7 to 1/3 of indexed deals. +-- I am using 1/5 as an approximation to give us more meaningful data than 0. +UPDATE daily_deals SET indexed_http = indexed/5; +ALTER TABLE daily_deals ALTER COLUMN indexed_http SET NOT NULL; + diff --git a/test/committee.test.js b/test/committee.test.js index e292f904..55d3a6f9 100644 --- a/test/committee.test.js +++ b/test/committee.test.js @@ -23,7 +23,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: true, retrievalResult: 'OK' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -38,7 +40,9 @@ describe('Committee', () => { c.addMeasurement({ ...VALID_MEASUREMENT }) c.evaluate({ requiredCommitteeSize: 10 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: false, indexerResult: 'COMMITTEE_TOO_SMALL', + hasRetrievalMajority: false, retrievalResult: 'COMMITTEE_TOO_SMALL' }) assert.strictEqual(c.measurements[0].fraudAssessment, 'COMMITTEE_TOO_SMALL') @@ -53,7 +57,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: false, indexerResult: 'MAJORITY_NOT_FOUND', + hasRetrievalMajority: false, retrievalResult: 'MAJORITY_NOT_FOUND' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -73,7 +79,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: true, retrievalResult: 'OK' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -92,7 +100,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: false, retrievalResult: 'MAJORITY_NOT_FOUND' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -112,7 +122,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: true, retrievalResult: 'CONTENT_VERIFICATION_FAILED' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -131,7 +143,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: false, indexerResult: 'MAJORITY_NOT_FOUND', + hasRetrievalMajority: false, retrievalResult: 'MAJORITY_NOT_FOUND' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -151,7 +165,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'HTTP_NOT_ADVERTISED', + hasRetrievalMajority: true, retrievalResult: 'OK' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -171,7 +187,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: false, retrievalResult: 'MAJORITY_NOT_FOUND' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -191,7 +209,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: true, retrievalResult: 'OK' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -210,7 +230,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: false, retrievalResult: 'MAJORITY_NOT_FOUND' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ @@ -230,7 +252,9 @@ describe('Committee', () => { c.evaluate({ requiredCommitteeSize: 2 }) assert.deepStrictEqual(c.evaluation, { + hasIndexMajority: true, indexerResult: 'OK', + hasRetrievalMajority: true, retrievalResult: 'OK' }) assert.deepStrictEqual(c.measurements.map(m => m.fraudAssessment), [ diff --git a/test/helpers/test-data.js b/test/helpers/test-data.js index eea2b980..fe7b0769 100644 --- a/test/helpers/test-data.js +++ b/test/helpers/test-data.js @@ -8,7 +8,8 @@ export const VALID_INET_GROUP = 'some-group-id' export const VALID_TASK = { cid: 'QmUuEoBdjC8D1PfWZCc7JCSK8nj7TV6HbXWDHYHzZHCVGS', - minerId: 'f1test' + minerId: 'f1test', + clients: ['f1client'] } Object.freeze(VALID_TASK) @@ -72,7 +73,9 @@ export const buildEvaluatedCommitteesFromMeasurements = (acceptedMeasurements) = const committees = [...groupMeasurementsToCommittees(acceptedMeasurements).values()] for (const c of committees) { c.evaluation = { + hasIndexMajority: true, indexerResult: c.measurements[0].indexerResult, + hasRetrievalMajority: true, retrievalResult: c.measurements[0].retrievalResult } } diff --git a/test/public-stats.test.js b/test/public-stats.test.js index 66e85d14..2fbdd078 100644 --- a/test/public-stats.test.js +++ b/test/public-stats.test.js @@ -55,7 +55,13 @@ describe('public-stats', () => { const allMeasurements = honestMeasurements let committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: created } = await pgClient.query( 'SELECT day::TEXT, total, successful FROM retrieval_stats' @@ -66,7 +72,13 @@ describe('public-stats', () => { honestMeasurements.push({ ...VALID_MEASUREMENT, retrievalResult: 'UNKNOWN_ERROR' }) committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: updated } = await pgClient.query( 'SELECT day::TEXT, total, successful FROM retrieval_stats' @@ -86,8 +98,14 @@ describe('public-stats', () => { const allMeasurements = honestMeasurements let committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: created } = await pgClient.query( 'SELECT day::TEXT, miner_id, total, successful FROM retrieval_stats' ) @@ -100,7 +118,13 @@ describe('public-stats', () => { honestMeasurements.push({ ...VALID_MEASUREMENT, minerId: 'f1second', retrievalResult: 'UNKNOWN_ERROR' }) committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: updated } = await pgClient.query( 'SELECT day::TEXT, miner_id, total, successful FROM retrieval_stats' @@ -131,7 +155,13 @@ describe('public-stats', () => { // The last measurement is rejected because it's a minority result honestMeasurements.splice(2) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: created } = await pgClient.query( 'SELECT day::TEXT, total, successful FROM retrieval_stats' @@ -153,7 +183,13 @@ describe('public-stats', () => { const allMeasurements = honestMeasurements let committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: created } = await pgClient.query( 'SELECT day::TEXT, deals_tested, deals_advertising_http FROM indexer_query_stats' @@ -169,7 +205,13 @@ describe('public-stats', () => { honestMeasurements.push({ ...VALID_MEASUREMENT, cid: 'bafy4', indexerResult: 'UNKNOWN_ERROR' }) committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: updated } = await pgClient.query( 'SELECT day::TEXT, deals_tested, deals_advertising_http FROM indexer_query_stats' @@ -193,13 +235,19 @@ describe('public-stats', () => { const allMeasurements = honestMeasurements let committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: created } = await pgClient.query( - 'SELECT day::TEXT, total, indexed, retrievable FROM daily_deals' + 'SELECT day::TEXT, tested, indexed, retrievable FROM daily_deals' ) assert.deepStrictEqual(created, [ - { day: today, total: 4, indexed: 3, retrievable: 1 } + { day: today, tested: 4, indexed: 3, retrievable: 1 } ]) // Notice: this measurement is for the same task as honestMeasurements[0], therefore it's @@ -210,18 +258,280 @@ describe('public-stats', () => { honestMeasurements.push({ ...VALID_MEASUREMENT, cid: 'bafy5', indexerResult: 'UNKNOWN_ERROR', retrievalResult: 'IPNI_UNKNOWN_ERROR' }) committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) - await updatePublicStats({ createPgClient, committees, honestMeasurements, allMeasurements }) + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients: (_minerId, _cid) => ['f0client'] + }) const { rows: updated } = await pgClient.query( - 'SELECT day::TEXT, total, indexed, retrievable FROM daily_deals' + 'SELECT day::TEXT, miner_id, tested, indexed, retrievable FROM daily_deals' ) assert.deepStrictEqual(updated, [{ day: today, - total: 2 * 4 + 1 /* added bafy5 */, + miner_id: VALID_MEASUREMENT.minerId, + tested: 2 * 4 + 1 /* added bafy5 */, indexed: 2 * 3 + 1 /* bafy5 is indexed */, retrievable: 2 * 1 + 0 /* bafy5 not retrievable */ }]) }) + + it('records client_id by creating one row per client', async () => { + const findDealClients = (_minerId, _cid) => ['f0clientA', 'f0clientB'] + + // Create new records + { + /** @type {Measurement[]} */ + const honestMeasurements = [ + { ...VALID_MEASUREMENT } + + ] + const allMeasurements = honestMeasurements + const committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) + + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients + }) + + const { rows: created } = await pgClient.query( + 'SELECT day::TEXT, miner_id, client_id, tested, indexed, retrievable FROM daily_deals' + ) + assert.deepStrictEqual(created, [ + { day: today, miner_id: VALID_MEASUREMENT.minerId, client_id: 'f0clientA', tested: 1, indexed: 1, retrievable: 1 }, + { day: today, miner_id: VALID_MEASUREMENT.minerId, client_id: 'f0clientB', tested: 1, indexed: 1, retrievable: 1 } + ]) + } + + // Update existing records + { + /** @type {Measurement[]} */ + const honestMeasurements = [ + { ...VALID_MEASUREMENT, cid: 'bafy5', indexerResult: 'UNKNOWN_ERROR', retrievalResult: 'IPNI_UNKNOWN_ERROR' } + ] + const allMeasurements = honestMeasurements + const committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) + + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients + }) + + const { rows: updated } = await pgClient.query( + 'SELECT day::TEXT, miner_id, client_id, tested, indexed, retrievable FROM daily_deals' + ) + assert.deepStrictEqual(updated, [ + { day: today, miner_id: VALID_MEASUREMENT.minerId, client_id: 'f0clientA', tested: 2, indexed: 1, retrievable: 1 }, + { day: today, miner_id: VALID_MEASUREMENT.minerId, client_id: 'f0clientB', tested: 2, indexed: 1, retrievable: 1 } + ]) + } + }) + + it('records index_majority_found, indexed, indexed_http', async () => { + const findDealClients = (_minerId, _cid) => ['f0client'] + + // Create new record(s) + { + /** @type {Measurement[]} */ + const honestMeasurements = [ + // a majority is found, indexerResult = OK + { ...VALID_MEASUREMENT, indexerResult: 'OK' }, + { ...VALID_MEASUREMENT, indexerResult: 'OK' }, + { ...VALID_MEASUREMENT, indexerResult: 'ERROR_404' }, + + // a majority is found, indexerResult = HTTP_NOT_ADVERTISED + { ...VALID_MEASUREMENT, cid: 'bafy2', indexerResult: 'HTTP_NOT_ADVERTISED' }, + { ...VALID_MEASUREMENT, cid: 'bafy2', indexerResult: 'HTTP_NOT_ADVERTISED' }, + { ...VALID_MEASUREMENT, cid: 'bafy2', indexerResult: 'ERROR_404' }, + + // a majority is found, indexerResult = ERROR_404 + { ...VALID_MEASUREMENT, cid: 'bafy3', indexerResult: 'OK' }, + { ...VALID_MEASUREMENT, cid: 'bafy3', indexerResult: 'ERROR_404' }, + { ...VALID_MEASUREMENT, cid: 'bafy3', indexerResult: 'ERROR_404' }, + + // committee is too small + { ...VALID_MEASUREMENT, cid: 'bafy4', indexerResult: 'OK' }, + + // no majority was found + { ...VALID_MEASUREMENT, cid: 'bafy5', indexerResult: 'OK' }, + { ...VALID_MEASUREMENT, cid: 'bafy5', indexerResult: 'NO_VALID_ADVERTISEMENT' }, + { ...VALID_MEASUREMENT, cid: 'bafy5', indexerResult: 'ERROR_404' } + ] + honestMeasurements.forEach(m => { m.fraudAssessment = 'OK' }) + const allMeasurements = honestMeasurements + const committees = [...groupMeasurementsToCommittees(honestMeasurements).values()] + committees.forEach(c => c.evaluate({ requiredCommitteeSize: 3 })) + + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients + }) + + const { rows: created } = await pgClient.query( + 'SELECT day::TEXT, tested, index_majority_found, indexed, indexed_http FROM daily_deals' + ) + assert.deepStrictEqual(created, [ + { day: today, tested: 5, index_majority_found: 3, indexed: 2, indexed_http: 1 } + ]) + } + + // Update existing record(s) + { + /** @type {Measurement[]} */ + const honestMeasurements = [ + // a majority is found, indexerResult = OK + { ...VALID_MEASUREMENT, indexerResult: 'OK' }, + + // a majority is found, indexerResult = HTTP_NOT_ADVERTISED + { ...VALID_MEASUREMENT, cid: 'bafy2', indexerResult: 'HTTP_NOT_ADVERTISED' }, + + // a majority is found, indexerResult = ERROR_404 + { ...VALID_MEASUREMENT, cid: 'bafy3', indexerResult: 'ERROR_404' }, + + // committee is too small + { ...VALID_MEASUREMENT, cid: 'bafy4', indexerResult: 'OK' } + ] + const allMeasurements = honestMeasurements + const committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) + Object.assign(committees.find(c => c.retrievalTask.cid === 'bafy4').evaluation, { + hasIndexMajority: false, + indexerResult: 'COMMITTEE_TOO_SMALL' + }) + + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients + }) + + const { rows: created } = await pgClient.query( + 'SELECT day::TEXT, tested, index_majority_found, indexed, indexed_http FROM daily_deals' + ) + assert.deepStrictEqual(created, [ + { day: today, tested: 5 + 4, index_majority_found: 3 + 3, indexed: 2 + 2, indexed_http: 1 + 1 } + ]) + } + }) + + it('records retrieval_majority_found, retrievable', async () => { + const findDealClients = (_minerId, _cid) => ['f0client'] + + // Create new record(s) + { + /** @type {Measurement[]} */ + const honestMeasurements = [ + // a majority is found, retrievalResult = OK + { ...VALID_MEASUREMENT, retrievalResult: 'OK' }, + { ...VALID_MEASUREMENT, retrievalResult: 'OK' }, + { ...VALID_MEASUREMENT, retrievalResult: 'ERROR_404' }, + + // a majority is found, retrievalResult = ERROR_404 + { ...VALID_MEASUREMENT, cid: 'bafy3', retrievalResult: 'OK' }, + { ...VALID_MEASUREMENT, cid: 'bafy3', retrievalResult: 'ERROR_404' }, + { ...VALID_MEASUREMENT, cid: 'bafy3', retrievalResult: 'ERROR_404' }, + + // committee is too small + { ...VALID_MEASUREMENT, cid: 'bafy4', retrievalResult: 'OK' }, + + // no majority was found + { ...VALID_MEASUREMENT, cid: 'bafy5', retrievalResult: 'OK' }, + { ...VALID_MEASUREMENT, cid: 'bafy5', retrievalResult: 'ERROR_404' }, + { ...VALID_MEASUREMENT, cid: 'bafy5', retrievalResult: 'ERROR_502' } + ] + honestMeasurements.forEach(m => { m.fraudAssessment = 'OK' }) + const allMeasurements = honestMeasurements + const committees = [...groupMeasurementsToCommittees(honestMeasurements).values()] + committees.forEach(c => c.evaluate({ requiredCommitteeSize: 3 })) + + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients + }) + + const { rows: created } = await pgClient.query( + 'SELECT day::TEXT, tested, retrieval_majority_found, retrievable FROM daily_deals' + ) + assert.deepStrictEqual(created, [ + { day: today, tested: 4, retrieval_majority_found: 2, retrievable: 1 } + ]) + } + + // Update existing record(s) + { + /** @type {Measurement[]} */ + const honestMeasurements = [ + // a majority is found, retrievalResult = OK + { ...VALID_MEASUREMENT, retrievalResult: 'OK' }, + + // a majority is found, retrievalResult = ERROR_404 + { ...VALID_MEASUREMENT, cid: 'bafy3', retrievalResult: 'ERROR_404' }, + + // committee is too small + { ...VALID_MEASUREMENT, cid: 'bafy4', retrievalResult: 'OK' } + ] + const allMeasurements = honestMeasurements + const committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) + Object.assign(committees.find(c => c.retrievalTask.cid === 'bafy4').evaluation, { + hasRetrievalMajority: false, + retrievalResult: 'COMMITTEE_TOO_SMALL' + }) + + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients + }) + const { rows: created } = await pgClient.query( + 'SELECT day::TEXT, tested, retrieval_majority_found, retrievable FROM daily_deals' + ) + assert.deepStrictEqual(created, [ + { day: today, tested: 4 + 3, retrieval_majority_found: 2 + 2, retrievable: 1 + 1 } + ]) + } + }) + + it('handles a task not linked to any clients', async () => { + const findDealClients = (_minerId, _cid) => undefined + + /** @type {Measurement[]} */ + const honestMeasurements = [ + { ...VALID_MEASUREMENT } + + ] + const allMeasurements = honestMeasurements + const committees = buildEvaluatedCommitteesFromMeasurements(honestMeasurements) + + await updatePublicStats({ + createPgClient, + committees, + honestMeasurements, + allMeasurements, + findDealClients + }) + + const { rows: created } = await pgClient.query( + 'SELECT day::TEXT, miner_id, client_id FROM daily_deals' + ) + assert.deepStrictEqual(created, []) + }) }) const getCurrentDate = async () => {