From 0acc1910db6cd507353abd1af734b0c90153c42a Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 8 Jun 2026 18:17:49 -0400 Subject: [PATCH] Use completed share rollups for pool accounting --- .../share-accounting.service.spec.ts | 37 ++++------ .../share-accounting.service.ts | 68 ++++++++----------- 2 files changed, 41 insertions(+), 64 deletions(-) diff --git a/src/ORM/share-accounting/share-accounting.service.spec.ts b/src/ORM/share-accounting/share-accounting.service.spec.ts index 4de1e78..0b31d36 100644 --- a/src/ORM/share-accounting/share-accounting.service.spec.ts +++ b/src/ORM/share-accounting/share-accounting.service.spec.ts @@ -249,7 +249,7 @@ describe('ShareAccountingService', () => { ); }); - it('should overlay live pool data and best share from the current round', async () => { + it('should refresh pool summaries from completed rollup buckets and current round rollups', async () => { const redis = { getJsonCache: jest.fn().mockResolvedValue(null), setJsonCache: jest.fn().mockResolvedValue(undefined), @@ -259,27 +259,19 @@ describe('ShareAccountingService', () => { .mockResolvedValueOnce([{ totalAcceptedShares: '3', totalCreditedDifficulty: '96', - acceptedSharesLast10Minutes: '0', - creditedDifficultyLast10Minutes: '0', + acceptedSharesLast10Minutes: '2', + creditedDifficultyLast10Minutes: '64', acceptedSharesLastHour: '3', creditedDifficultyLastHour: '96', acceptedSharesLastDay: '3', creditedDifficultyLastDay: '96', - hashRateLast10Minutes: '0', + hashRateLast10Minutes: '458129844.9', hashRateLastHour: '114532461.2', latestShareAt: new Date('2026-06-07T12:10:00Z'), }]) - .mockResolvedValueOnce([{ - acceptedSharesLast10Minutes: '7', - creditedDifficultyLast10Minutes: '224', - hashRateLast10Minutes: '1603451170.77', - latestShareAt: new Date('2026-06-07T12:20:00Z'), - }]) .mockResolvedValueOnce([{ bestSubmissionDifficulty: '4096', - bestSubmissionDifficultyAt: new Date('2026-06-07T12:19:00Z'), - }]) - .mockResolvedValueOnce([{ + bestSubmissionDifficultyAt: new Date('2026-06-07T12:10:00Z'), currentRoundAcceptedShares: '11', workSinceLastBlock: '352', currentRoundNetworkDifficulty: '1000', @@ -288,25 +280,24 @@ describe('ShareAccountingService', () => { const service = new ShareAccountingService(repository as any, redis as any); await expect(service.refreshPoolSummary()).resolves.toEqual(expect.objectContaining({ - acceptedSharesLast10Minutes: 7, - creditedDifficultyLast10Minutes: 224, - hashRateLast10Minutes: 1603451170.77, + acceptedSharesLast10Minutes: 2, + creditedDifficultyLast10Minutes: 64, + hashRateLast10Minutes: 458129844.9, bestSubmissionDifficulty: 4096, - bestSubmissionDifficultyAt: '2026-06-07T12:19:00.000Z', + bestSubmissionDifficultyAt: '2026-06-07T12:10:00.000Z', workSinceLastBlock: 352, currentRoundAcceptedShares: 11, currentRoundNetworkDifficulty: 1000, networkDifficultyPercent: 35.2, - latestShareAt: '2026-06-07T12:20:00.000Z', + latestShareAt: '2026-06-07T12:10:00.000Z', })); expect(repository.query).toHaveBeenNthCalledWith( - 3, - expect.stringContaining('WHERE "blockHeight" > latest_found_block."height"'), - ); - expect(repository.query).toHaveBeenNthCalledWith( - 4, + 2, expect.stringContaining('FROM "accepted_share_block_10m"'), ); + expect(repository.query).not.toHaveBeenCalledWith( + expect.stringContaining('FROM "accepted_share_entity"'), + ); }); it('should cache accounting summaries briefly to protect hot dashboard endpoints', async () => { diff --git a/src/ORM/share-accounting/share-accounting.service.ts b/src/ORM/share-accounting/share-accounting.service.ts index 17f4ce8..106b728 100644 --- a/src/ORM/share-accounting/share-accounting.service.ts +++ b/src/ORM/share-accounting/share-accounting.service.ts @@ -156,7 +156,7 @@ export class ShareAccountingService implements OnModuleDestroy { } public async refreshPoolSummary(): Promise { - const summary = await this.withPoolLiveOverlay(await this.getSummary({})); + const summary = await this.withPoolRollupOverlay(await this.getSummary({})); await this.redisMessagingService ?.setJsonCache(this.poolSummaryCacheKey, summary, 10 * 60 * 1000) .catch(error => { @@ -165,68 +165,52 @@ export class ShareAccountingService implements OnModuleDestroy { return summary; } - private async withPoolLiveOverlay(summary: ShareAccountingSummary): Promise { + private async withPoolRollupOverlay(summary: ShareAccountingSummary): Promise { if (process.env.API_ONLY === 'true') { return summary; } - const [[liveWindow], [bestDifficultyRow], [currentRoundRow]] = await Promise.all([ - this.acceptedShareRepository.query(` - SELECT - COUNT(*)::int AS "acceptedSharesLast10Minutes", - COALESCE(SUM("creditedDifficulty"), 0)::float AS "creditedDifficultyLast10Minutes", - COALESCE((SUM("creditedDifficulty") * ${HASHES_PER_DIFFICULTY}) / 600, 0)::float AS "hashRateLast10Minutes", - MAX("acceptedAt") AS "latestShareAt" - FROM "accepted_share_entity" - WHERE "acceptedAt" > NOW() - INTERVAL '10 minutes' - `), - this.acceptedShareRepository.query(` - WITH latest_found_block AS ( - SELECT COALESCE(MAX("height"), 0) AS "height" - FROM "blocks_entity" - ) - SELECT - COALESCE("submissionDifficulty", 0)::float AS "bestSubmissionDifficulty", - "acceptedAt" AS "bestSubmissionDifficultyAt" - FROM "accepted_share_entity", latest_found_block - WHERE "blockHeight" > latest_found_block."height" - ORDER BY "submissionDifficulty" DESC, "acceptedAt" DESC - LIMIT 1 - `), - this.acceptedShareRepository.query(` + const [currentRoundRow] = await this.acceptedShareRepository.query(` WITH latest_found_block AS ( SELECT COALESCE(MAX("height"), 0) AS "height" FROM "blocks_entity" + ), + filtered_rows AS ( + SELECT "accepted_share_block_10m".* + FROM "accepted_share_block_10m", latest_found_block + WHERE "blockHeight" > latest_found_block."height" + ), + best_share AS ( + SELECT + "bestSubmissionDifficulty", + "bucket" + FROM filtered_rows + ORDER BY "bestSubmissionDifficulty" DESC, "bucket" DESC + LIMIT 1 ) SELECT COALESCE(SUM("acceptedCount"), 0)::int AS "currentRoundAcceptedShares", COALESCE(SUM("shares"), 0)::float AS "workSinceLastBlock", - COALESCE(MAX("networkDifficulty"), 0)::float AS "currentRoundNetworkDifficulty" - FROM "accepted_share_block_10m", latest_found_block - WHERE "blockHeight" > latest_found_block."height" - `), - ]); + COALESCE(MAX("networkDifficulty"), 0)::float AS "currentRoundNetworkDifficulty", + COALESCE((SELECT "bestSubmissionDifficulty" FROM best_share), 0)::float AS "bestSubmissionDifficulty", + (SELECT "bucket" FROM best_share) AS "bestSubmissionDifficultyAt" + FROM filtered_rows + `); const currentRoundNetworkDifficulty = this.toNumber(currentRoundRow?.currentRoundNetworkDifficulty); const workSinceLastBlock = this.toNumber(currentRoundRow?.workSinceLastBlock); return { ...summary, - acceptedSharesLast10Minutes: this.toNumber(liveWindow?.acceptedSharesLast10Minutes), - creditedDifficultyLast10Minutes: this.toNumber(liveWindow?.creditedDifficultyLast10Minutes), - hashRateLast10Minutes: this.toNumber(liveWindow?.hashRateLast10Minutes), - bestSubmissionDifficulty: this.toNumber(bestDifficultyRow?.bestSubmissionDifficulty), - bestSubmissionDifficultyAt: bestDifficultyRow?.bestSubmissionDifficultyAt == null + bestSubmissionDifficulty: this.toNumber(currentRoundRow?.bestSubmissionDifficulty), + bestSubmissionDifficultyAt: currentRoundRow?.bestSubmissionDifficultyAt == null ? null - : new Date(bestDifficultyRow.bestSubmissionDifficultyAt).toISOString(), + : new Date(currentRoundRow.bestSubmissionDifficultyAt).toISOString(), workSinceLastBlock, currentRoundAcceptedShares: this.toNumber(currentRoundRow?.currentRoundAcceptedShares), currentRoundNetworkDifficulty, networkDifficultyPercent: currentRoundNetworkDifficulty > 0 ? this.roundPercent((workSinceLastBlock / currentRoundNetworkDifficulty) * 100) : 0, - latestShareAt: liveWindow?.latestShareAt == null - ? summary.latestShareAt - : new Date(liveWindow.latestShareAt).toISOString(), }; } @@ -365,7 +349,9 @@ export class ShareAccountingService implements OnModuleDestroy { filtered_rows AS ( SELECT "accepted_share_10m".*, bounds."currentBucket", bounds."lastCompletedBucket" FROM "accepted_share_10m", bounds - ${whereSql} + ${whereSql.length > 0 + ? `${whereSql} AND "accepted_share_10m"."bucket" < bounds."currentBucket"` + : `WHERE "accepted_share_10m"."bucket" < bounds."currentBucket"`} ) SELECT COALESCE(SUM("acceptedCount"), 0)::int AS "totalAcceptedShares",