From f31cdef7f048d338fed6394e8617197e979d64f8 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 29 Jul 2026 00:55:26 -0400 Subject: [PATCH] Fix retained share high score ordering --- ...eHighScoreNumericRetention1781402000000.ts | 250 ++++++++++++++++++ .../share-high-score.service.spec.ts | 67 +++++ .../share-high-score.service.ts | 127 ++++++++- src/database.config.ts | 2 + 4 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 src/ORM/_migrations/AcceptedShareHighScoreNumericRetention1781402000000.ts create mode 100644 src/ORM/share-accounting/share-high-score.service.spec.ts diff --git a/src/ORM/_migrations/AcceptedShareHighScoreNumericRetention1781402000000.ts b/src/ORM/_migrations/AcceptedShareHighScoreNumericRetention1781402000000.ts new file mode 100644 index 0000000..06f5815 --- /dev/null +++ b/src/ORM/_migrations/AcceptedShareHighScoreNumericRetention1781402000000.ts @@ -0,0 +1,250 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AcceptedShareHighScoreNumericRetention1781402000000 implements MigrationInterface { + public name = 'AcceptedShareHighScoreNumericRetention1781402000000'; + public transaction = false; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_accepted_share_block_10m_best" + ON "accepted_share_block_10m" ("bestSubmissionDifficulty" DESC, "bucket" DESC) + WHERE "bestSubmissionDifficulty" > 0 + `); + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_accepted_share_block_10m_mode_best" + ON "accepted_share_block_10m" ("payoutMode", "bestSubmissionDifficulty" DESC, "bucket" DESC) + WHERE "bestSubmissionDifficulty" > 0 + `); + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_client_best_difficulty_mode" + ON "client_entity" ("payoutMode", "bestDifficulty" DESC, "updatedAt" DESC) + WHERE "bestDifficulty" > 0 + `); + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_address_settings_best_difficulty" + ON "address_settings_entity" ("bestDifficulty" DESC, "updatedAt" DESC) + WHERE "bestDifficulty" > 0 + `); + + await this.backfillRollupAllTime(queryRunner); + await this.backfillClientAllTime(queryRunner); + await this.backfillAddressSettingsAllTime(queryRunner); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_address_settings_best_difficulty"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_client_best_difficulty_mode"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_accepted_share_block_10m_mode_best"`); + await queryRunner.query(`DROP INDEX IF EXISTS "IDX_accepted_share_block_10m_best"`); + } + + private async backfillRollupAllTime(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + WITH candidates AS ( + SELECT + "payoutMode", + "bucket", + "blockHeight", + "bestSubmissionDifficulty" + FROM "accepted_share_block_10m" + WHERE "bestSubmissionDifficulty" IS NOT NULL + AND "bestSubmissionDifficulty" > 0 + + UNION ALL + + SELECT + 'all' AS "payoutMode", + "bucket", + "blockHeight", + "bestSubmissionDifficulty" + FROM "accepted_share_block_10m" + WHERE "bestSubmissionDifficulty" IS NOT NULL + AND "bestSubmissionDifficulty" > 0 + ), + best_rows AS ( + SELECT DISTINCT ON ("payoutMode") + "payoutMode", + "bucket", + "blockHeight", + "bestSubmissionDifficulty" + FROM candidates + ORDER BY "payoutMode", "bestSubmissionDifficulty" DESC, "bucket" DESC + ) + INSERT INTO "accepted_share_high_score" ( + "scope", + "payoutMode", + "bucketDate", + "submissionDifficulty", + "acceptedAt", + "bucket", + "blockHeight", + "address", + "clientName", + "protocol" + ) + SELECT + 'all_time', + "payoutMode", + DATE '1970-01-01', + "bestSubmissionDifficulty", + "bucket", + "bucket", + "blockHeight", + NULL, + NULL, + NULL + FROM best_rows + ON CONFLICT ("scope", "payoutMode", "bucketDate") + DO UPDATE SET + "submissionDifficulty" = EXCLUDED."submissionDifficulty", + "acceptedAt" = EXCLUDED."acceptedAt", + "bucket" = EXCLUDED."bucket", + "blockHeight" = EXCLUDED."blockHeight", + "address" = EXCLUDED."address", + "clientName" = EXCLUDED."clientName", + "protocol" = EXCLUDED."protocol", + "updatedAt" = NOW() + WHERE EXCLUDED."submissionDifficulty" > "accepted_share_high_score"."submissionDifficulty" + OR ( + EXCLUDED."submissionDifficulty" = "accepted_share_high_score"."submissionDifficulty" + AND EXCLUDED."acceptedAt" > COALESCE("accepted_share_high_score"."acceptedAt", '-infinity'::timestamptz) + ) + `); + } + + private async backfillClientAllTime(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + WITH candidates AS ( + SELECT + "payoutMode", + "bestDifficulty", + "updatedAt", + "address", + "clientName", + "userAgent" + FROM "client_entity" + WHERE "bestDifficulty" IS NOT NULL + AND "bestDifficulty" > 0 + + UNION ALL + + SELECT + 'all' AS "payoutMode", + "bestDifficulty", + "updatedAt", + "address", + "clientName", + "userAgent" + FROM "client_entity" + WHERE "bestDifficulty" IS NOT NULL + AND "bestDifficulty" > 0 + ), + best_rows AS ( + SELECT DISTINCT ON ("payoutMode") + "payoutMode", + "bestDifficulty", + "updatedAt", + "address", + "clientName", + "userAgent" + FROM candidates + ORDER BY "payoutMode", "bestDifficulty" DESC, "updatedAt" DESC + ) + INSERT INTO "accepted_share_high_score" ( + "scope", + "payoutMode", + "bucketDate", + "submissionDifficulty", + "acceptedAt", + "bucket", + "blockHeight", + "address", + "clientName", + "protocol" + ) + SELECT + 'all_time', + "payoutMode", + DATE '1970-01-01', + "bestDifficulty", + "updatedAt", + NULL, + NULL, + "address", + "clientName", + "userAgent" + FROM best_rows + ON CONFLICT ("scope", "payoutMode", "bucketDate") + DO UPDATE SET + "submissionDifficulty" = EXCLUDED."submissionDifficulty", + "acceptedAt" = EXCLUDED."acceptedAt", + "bucket" = EXCLUDED."bucket", + "blockHeight" = EXCLUDED."blockHeight", + "address" = EXCLUDED."address", + "clientName" = EXCLUDED."clientName", + "protocol" = EXCLUDED."protocol", + "updatedAt" = NOW() + WHERE EXCLUDED."submissionDifficulty" > "accepted_share_high_score"."submissionDifficulty" + OR ( + EXCLUDED."submissionDifficulty" = "accepted_share_high_score"."submissionDifficulty" + AND EXCLUDED."acceptedAt" > COALESCE("accepted_share_high_score"."acceptedAt", '-infinity'::timestamptz) + ) + `); + } + + private async backfillAddressSettingsAllTime(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + WITH best_row AS ( + SELECT + "bestDifficulty", + "updatedAt", + "address", + "bestDifficultyUserAgent" + FROM "address_settings_entity" + WHERE "bestDifficulty" IS NOT NULL + AND "bestDifficulty" > 0 + ORDER BY "bestDifficulty" DESC, "updatedAt" DESC + LIMIT 1 + ) + INSERT INTO "accepted_share_high_score" ( + "scope", + "payoutMode", + "bucketDate", + "submissionDifficulty", + "acceptedAt", + "bucket", + "blockHeight", + "address", + "clientName", + "protocol" + ) + SELECT + 'all_time', + 'all', + DATE '1970-01-01', + "bestDifficulty", + "updatedAt", + NULL, + NULL, + "address", + NULL, + "bestDifficultyUserAgent" + FROM best_row + ON CONFLICT ("scope", "payoutMode", "bucketDate") + DO UPDATE SET + "submissionDifficulty" = EXCLUDED."submissionDifficulty", + "acceptedAt" = EXCLUDED."acceptedAt", + "bucket" = EXCLUDED."bucket", + "blockHeight" = EXCLUDED."blockHeight", + "address" = EXCLUDED."address", + "clientName" = EXCLUDED."clientName", + "protocol" = EXCLUDED."protocol", + "updatedAt" = NOW() + WHERE EXCLUDED."submissionDifficulty" > "accepted_share_high_score"."submissionDifficulty" + OR ( + EXCLUDED."submissionDifficulty" = "accepted_share_high_score"."submissionDifficulty" + AND EXCLUDED."acceptedAt" > COALESCE("accepted_share_high_score"."acceptedAt", '-infinity'::timestamptz) + ) + `); + } +} diff --git a/src/ORM/share-accounting/share-high-score.service.spec.ts b/src/ORM/share-accounting/share-high-score.service.spec.ts new file mode 100644 index 0000000..ade3524 --- /dev/null +++ b/src/ORM/share-accounting/share-high-score.service.spec.ts @@ -0,0 +1,67 @@ +import { DataSource, EntityManager } from 'typeorm'; + +import { ShareHighScoreService } from './share-high-score.service'; + +describe('ShareHighScoreService', () => { + it('orders aggregate candidates numerically and retains all-time client/address highs', async () => { + const upserts: unknown[][] = []; + const manager = { + query: jest.fn(async (sql: string, params: unknown[] = []) => { + if (sql.includes('pg_try_advisory_xact_lock')) { + return [{ locked: true }]; + } + if (sql.includes('WITH candidate_rows')) { + expect(sql).toContain('"bestSubmissionDifficulty"::numeric DESC'); + return []; + } + if (sql.includes('FROM "accepted_share_block_10m"')) { + return []; + } + if (sql.includes('FROM "client_entity"')) { + const payoutMode = params[0] as string | undefined; + const difficultyByMode = { + solo: '10828324014691.838', + pplns: '2688488439606.743', + all: '10828324014691.838', + }; + const mode = payoutMode ?? 'all'; + return [{ + submissionDifficulty: difficultyByMode[mode], + acceptedAt: new Date('2026-07-16T06:33:41.270Z'), + address: 'bc1qe4zjanpz5tg96a278ew3l2g0h9h0qddgcdvp43', + clientName: 'bitaxe', + protocol: 'bitaxe', + }]; + } + if (sql.includes('FROM "address_settings_entity"')) { + return [{ + submissionDifficulty: '294141974944674.8', + acceptedAt: new Date('2026-07-10T03:31:15.987Z'), + address: 'bc1q0pp74ghs25vpn2ah6auz4vkehvzy8z8ddyzts7', + protocol: 'bitaxe', + }]; + } + if (sql.includes('INSERT INTO "accepted_share_high_score"')) { + upserts.push(params); + return [{ inserted: 1 }]; + } + throw new Error(`Unexpected query: ${sql}`); + }), + } as unknown as EntityManager; + const dataSource = { + transaction: jest.fn((callback: (manager: EntityManager) => Promise) => callback(manager)), + } as unknown as DataSource; + const service = new ShareHighScoreService(dataSource); + + await expect(service.refreshHighScores()).resolves.toEqual({ + processed: true, + updatedRows: 4, + }); + + expect(upserts).toEqual(expect.arrayContaining([ + expect.arrayContaining(['all_time', 'all', '1970-01-01', '294141974944674.8']), + expect.arrayContaining(['all_time', 'solo', '1970-01-01', '10828324014691.838']), + expect.arrayContaining(['all_time', 'pplns', '1970-01-01', '2688488439606.743']), + ])); + }); +}); diff --git a/src/ORM/share-accounting/share-high-score.service.ts b/src/ORM/share-accounting/share-high-score.service.ts index e1e7f8f..e1fac9f 100644 --- a/src/ORM/share-accounting/share-high-score.service.ts +++ b/src/ORM/share-accounting/share-high-score.service.ts @@ -139,6 +139,11 @@ export class ShareHighScoreService implements OnModuleInit, OnModuleDestroy { }); } + const allTimeRecords = await this.loadAllTimeRecords(manager, payoutMode); + for (const record of allTimeRecords) { + updatedRows += await this.upsertHighScore(manager, record); + } + return updatedRows; } @@ -167,10 +172,130 @@ export class ShareHighScoreService implements OnModuleInit, OnModuleDestroy { "blockHeight"::text AS "blockHeight", "bestSubmissionDifficulty"::text AS "bestSubmissionDifficulty" FROM candidate_rows - ORDER BY "bucketDate", "bestSubmissionDifficulty" DESC, "bucket" DESC + ORDER BY "bucketDate", "bestSubmissionDifficulty"::numeric DESC, "bucket" DESC `, params); } + private async loadAllTimeRecords(manager: EntityManager, payoutMode: HighScorePayoutMode): Promise { + const records: HighScoreRecord[] = []; + const rollupCandidate = await this.loadRollupAllTimeCandidate(manager, payoutMode); + if (rollupCandidate != null) { + records.push(this.toRecord('all_time', payoutMode, ALL_TIME_BUCKET_DATE, rollupCandidate, null)); + } + + const clientRecord = await this.loadClientAllTimeRecord(manager, payoutMode); + if (clientRecord != null) { + records.push(clientRecord); + } + + if (payoutMode === 'all') { + const addressRecord = await this.loadAddressSettingsAllTimeRecord(manager); + if (addressRecord != null) { + records.push(addressRecord); + } + } + + return records; + } + + private async loadRollupAllTimeCandidate( + manager: EntityManager, + payoutMode: HighScorePayoutMode, + ): Promise { + const params: unknown[] = []; + const modeFilter = payoutMode === 'all' + ? '' + : `AND "payoutMode" = $${params.push(payoutMode)}`; + const rows = await manager.query(` + SELECT + date_trunc('day', "bucket")::date::text AS "bucketDate", + "bucket", + "blockHeight"::text AS "blockHeight", + "bestSubmissionDifficulty"::text AS "bestSubmissionDifficulty" + FROM "accepted_share_block_10m" + WHERE "bestSubmissionDifficulty" IS NOT NULL + AND "bestSubmissionDifficulty" > 0 + ${modeFilter} + ORDER BY "bestSubmissionDifficulty" DESC, "bucket" DESC + LIMIT 1 + `, params); + + return rows[0] ?? null; + } + + private async loadClientAllTimeRecord( + manager: EntityManager, + payoutMode: HighScorePayoutMode, + ): Promise { + const params: unknown[] = []; + const modeFilter = payoutMode === 'all' + ? '' + : `AND "payoutMode" = $${params.push(payoutMode)}`; + const rows = await manager.query(` + SELECT + "bestDifficulty"::text AS "submissionDifficulty", + "updatedAt" AS "acceptedAt", + "address", + "clientName", + "userAgent" AS "protocol" + FROM "client_entity" + WHERE "bestDifficulty" IS NOT NULL + AND "bestDifficulty" > 0 + ${modeFilter} + ORDER BY "bestDifficulty" DESC, "updatedAt" DESC + LIMIT 1 + `, params); + const row = rows[0]; + if (row == null) { + return null; + } + + return { + scope: 'all_time', + payoutMode, + bucketDate: ALL_TIME_BUCKET_DATE, + submissionDifficulty: row.submissionDifficulty, + acceptedAt: row.acceptedAt ?? null, + bucket: null, + blockHeight: null, + address: row.address ?? null, + clientName: row.clientName ?? null, + protocol: row.protocol ?? null, + }; + } + + private async loadAddressSettingsAllTimeRecord(manager: EntityManager): Promise { + const rows = await manager.query(` + SELECT + "bestDifficulty"::text AS "submissionDifficulty", + "updatedAt" AS "acceptedAt", + "address", + "bestDifficultyUserAgent" AS "protocol" + FROM "address_settings_entity" + WHERE "bestDifficulty" IS NOT NULL + AND "bestDifficulty" > 0 + ORDER BY "bestDifficulty" DESC, "updatedAt" DESC + LIMIT 1 + `); + const row = rows[0]; + if (row == null) { + return null; + } + + return { + scope: 'all_time', + payoutMode: 'all', + bucketDate: ALL_TIME_BUCKET_DATE, + submissionDifficulty: row.submissionDifficulty, + acceptedAt: row.acceptedAt ?? null, + bucket: null, + blockHeight: null, + address: row.address ?? null, + clientName: null, + protocol: row.protocol ?? null, + }; + } + private async loadExactHighScore( manager: EntityManager, candidate: HighScoreCandidate, diff --git a/src/database.config.ts b/src/database.config.ts index e8708da..9aba2c0 100644 --- a/src/database.config.ts +++ b/src/database.config.ts @@ -20,6 +20,7 @@ import { AcceptedShareHighScores1781309000000 } from './ORM/_migrations/Accepted import { UserAgentReportNonzeroHashrate1781313000000 } from './ORM/_migrations/UserAgentReportNonzeroHashrate1781313000000'; import { PoolSummaryContinuousAggregate1781400000000 } from './ORM/_migrations/PoolSummaryContinuousAggregate1781400000000'; import { PoolSummaryRefreshWindow1781401000000 } from './ORM/_migrations/PoolSummaryRefreshWindow1781401000000'; +import { AcceptedShareHighScoreNumericRetention1781402000000 } from './ORM/_migrations/AcceptedShareHighScoreNumericRetention1781402000000'; import { UserAgentReportView } from './ORM/_views/user-agent-report/user-agent-report.view'; import { AcceptedShareEntity } from './ORM/accepted-share/accepted-share.entity'; import { AddressSettingsEntity } from './ORM/address-settings/address-settings.entity'; @@ -66,6 +67,7 @@ export const databaseMigrations = [ UserAgentReportNonzeroHashrate1781313000000, PoolSummaryContinuousAggregate1781400000000, PoolSummaryRefreshWindow1781401000000, + AcceptedShareHighScoreNumericRetention1781402000000, ]; export function createDatabaseOptions(env: NodeJS.ProcessEnv): TypeOrmModuleOptions & DataSourceOptions {