From bdd3c7c3ae22d2de9b8d67c33ba7a3dba722ac4d Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 21 Jun 2026 11:12:09 -0400 Subject: [PATCH] Revert "Fix DATUM PPLNS coinbaser snapshots" This reverts commit 1943ea776802bab27418853bc35b7e914752be01. --- .env.example | 2 - docker-compose.external-db.yml | 2 - docker-compose.yml | 2 - full-setup/docker-compose-mainnet.yml | 2 - .../payout-snapshot.service.spec.ts | 59 +------------------ .../payout-snapshot.service.ts | 53 +++++------------ src/services/datum.service.spec.ts | 56 ------------------ src/services/datum.service.ts | 56 +----------------- 8 files changed, 18 insertions(+), 214 deletions(-) diff --git a/.env.example b/.env.example index 45f617f..f69b7a4 100644 --- a/.env.example +++ b/.env.example @@ -58,8 +58,6 @@ STRATUM_TCP_KEEPALIVE_INITIAL_DELAY_MS=60000 #PPLNS_DATUM_PORTS= #DATUM_POOL_PAYOUT_ADDRESS= #DATUM_SHARE_DIFFICULTY=1 -#DATUM_PAYOUT_MAX_COINBASE_OUTPUTS=10 -#DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET=2000 # 32-byte hex seed for the DATUM server identity key. DATUM clients must be # configured with the matching logged public key. #DATUM_IDENTITY_SEED= diff --git a/docker-compose.external-db.yml b/docker-compose.external-db.yml index aee4e43..48840cb 100644 --- a/docker-compose.external-db.yml +++ b/docker-compose.external-db.yml @@ -95,8 +95,6 @@ services: PPLNS_SV2_TDP_PORTS: ${PPLNS_SV2_TDP_PORTS:-} DATUM_PORTS: ${DATUM_PORTS:-} PPLNS_DATUM_PORTS: ${PPLNS_DATUM_PORTS:-} - DATUM_PAYOUT_MAX_COINBASE_OUTPUTS: ${DATUM_PAYOUT_MAX_COINBASE_OUTPUTS:-10} - DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET: ${DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET:-2000} SHARE_ACCOUNTING_BATCH_SIZE: ${SHARE_ACCOUNTING_BATCH_SIZE:-500} SHARE_ACCOUNTING_FLUSH_INTERVAL_MS: ${SHARE_ACCOUNTING_FLUSH_INTERVAL_MS:-25} SHARE_ACCOUNTING_MAX_QUEUE_SIZE: ${SHARE_ACCOUNTING_MAX_QUEUE_SIZE:-50000} diff --git a/docker-compose.yml b/docker-compose.yml index 27732c6..072efb1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -124,8 +124,6 @@ services: PPLNS_SV2_TDP_PORTS: ${PPLNS_SV2_TDP_PORTS:-} DATUM_PORTS: ${DATUM_PORTS:-} PPLNS_DATUM_PORTS: ${PPLNS_DATUM_PORTS:-} - DATUM_PAYOUT_MAX_COINBASE_OUTPUTS: ${DATUM_PAYOUT_MAX_COINBASE_OUTPUTS:-10} - DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET: ${DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET:-2000} healthcheck: test: ["CMD-SHELL", "node -e \"const http=require('http'); const https=require('https'); const secure=process.env.API_SECURE==='true'; const client=secure?https:http; const req=client.get({hostname:'127.0.0.1',port:process.env.API_PORT||3334,path:'/api/network',rejectUnauthorized:false},res=>process.exit(res.statusCode<500?0:1)); req.on('error',()=>process.exit(1)); req.setTimeout(5000,()=>{req.destroy(); process.exit(1);});\""] interval: 30s diff --git a/full-setup/docker-compose-mainnet.yml b/full-setup/docker-compose-mainnet.yml index 4f5e1d9..cf0726d 100644 --- a/full-setup/docker-compose-mainnet.yml +++ b/full-setup/docker-compose-mainnet.yml @@ -127,8 +127,6 @@ services: PAYOUT_FEE_ADDRESS: ${PAYOUT_FEE_ADDRESS:-} PAYOUT_FEE_PERCENT: ${PAYOUT_FEE_PERCENT:-0} PAYOUT_COINBASE_WEIGHT_BUDGET: ${PAYOUT_COINBASE_WEIGHT_BUDGET:-50000} - DATUM_PAYOUT_MAX_COINBASE_OUTPUTS: ${DATUM_PAYOUT_MAX_COINBASE_OUTPUTS:-10} - DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET: ${DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET:-2000} networks: bitcoin: diff --git a/src/ORM/payout-snapshot/payout-snapshot.service.spec.ts b/src/ORM/payout-snapshot/payout-snapshot.service.spec.ts index db56b91..1bbd99a 100644 --- a/src/ORM/payout-snapshot/payout-snapshot.service.spec.ts +++ b/src/ORM/payout-snapshot/payout-snapshot.service.spec.ts @@ -95,63 +95,6 @@ describe('PayoutSnapshotService', () => { expect(manager.query.mock.calls[0][1]).toEqual([100, 'pplns']); }); - it('should allow protocol-specific snapshot methods and coinbase limits', async () => { - manager.query - .mockResolvedValueOnce([{ - startBatchId: '10', - endBatchId: '12', - windowStartShareIndex: '1000', - windowEndShareIndex: '2000', - totalCreditedDifficulty: '100', - totalAcceptedShareCount: '5', - }]) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce([ - { address: ADDRESS_A, creditedDifficulty: 60, acceptedShareCount: 3 }, - { address: ADDRESS_B, creditedDifficulty: 40, acceptedShareCount: 2 }, - ]) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce([{ id: '57' }]) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce([{ - id: '57', - method: 'pplns-datum', - blockHeight: 900002, - coinbaseValueSats: '1000', - windowStartShareIndex: '1000', - windowEndShareIndex: '2000', - totalCreditedDifficulty: 100, - totalAcceptedShareCount: '5', - eligibleAddressCount: 2, - includedOutputCount: 1, - distributedSats: '1000', - unallocatedRemainderSats: '0', - }]) - .mockResolvedValueOnce([ - { address: ADDRESS_A, payoutSats: '1000' }, - ]); - - const snapshot = await service.createSnapshotForTemplate({ - blockHeight: 900002, - coinbaseValueSats: 1000, - networkDifficulty: 25, - method: 'pplns-datum', - maxCoinbaseOutputs: 1, - coinbaseWeightBudget: 2000, - }); - - expect(snapshot).toEqual(expect.objectContaining({ - id: '57', - method: 'pplns-datum', - includedOutputCount: 1, - })); - expect(manager.query.mock.calls[1][1][0]).toBe('pplns-datum'); - expect(manager.query.mock.calls[4][1][0]).toBe('pplns-datum'); - expect(manager.query.mock.calls[4][1][10]).toBe(2000); - expect(manager.query.mock.calls[4][1][18]).toBe(1); - }); - it('should bootstrap the PPLNS window from paid block count up to the configured factor', async () => { process.env.PAYOUT_BOOTSTRAP_WINDOW = 'true'; service = new PayoutSnapshotService(dataSource as unknown as DataSource); @@ -360,7 +303,7 @@ describe('PayoutSnapshotService', () => { createdAt: new Date('2026-06-16T12:00:00.000Z'), percent: 60, }); - expect(dataSource.query.mock.calls[0][1]).toEqual([ADDRESS_A, 'pplns', 'pplns']); + expect(dataSource.query.mock.calls[0][1]).toEqual([ADDRESS_A, 'pplns']); expect(dataSource.query.mock.calls[0][0]).toContain('WITH latest_snapshot AS'); }); diff --git a/src/ORM/payout-snapshot/payout-snapshot.service.ts b/src/ORM/payout-snapshot/payout-snapshot.service.ts index 380b0c0..d3638d4 100644 --- a/src/ORM/payout-snapshot/payout-snapshot.service.ts +++ b/src/ORM/payout-snapshot/payout-snapshot.service.ts @@ -51,15 +51,6 @@ export interface ExpectedPayout { percent: number; } -export interface CreatePayoutSnapshotInput { - blockHeight: number; - coinbaseValueSats: number; - networkDifficulty: number; - method?: string; - maxCoinbaseOutputs?: number; - coinbaseWeightBudget?: number; -} - const DEFAULT_MAX_COINBASE_OUTPUTS = 10; const DEFAULT_MIN_OUTPUT_SATS = 546; const DEFAULT_PAYOUT_METHOD = 'pplns'; @@ -84,15 +75,15 @@ export class PayoutSnapshotService { private readonly dataSource: DataSource, ) { } - public async createSnapshotForTemplate(input: CreatePayoutSnapshotInput): Promise { + public async createSnapshotForTemplate(input: { + blockHeight: number; + coinbaseValueSats: number; + networkDifficulty: number; + }): Promise { if (!this.snapshotsEnabled || input.coinbaseValueSats <= 0 || input.networkDifficulty <= 0) { return null; } - const method = this.normalizeMethod(input.method); - const maxCoinbaseOutputs = this.resolvePositiveInt(input.maxCoinbaseOutputs, this.maxCoinbaseOutputs); - const coinbaseWeightBudget = this.resolvePositiveInt(input.coinbaseWeightBudget, this.coinbaseWeightBudget); - return this.dataSource.transaction(async manager => { const effectiveWindowFactor = await this.getEffectiveWindowFactor(manager); const windowTargetDifficulty = input.networkDifficulty * effectiveWindowFactor; @@ -102,7 +93,6 @@ export class PayoutSnapshotService { } const existing = await this.getExistingSnapshot(manager, { - method, blockHeight: input.blockHeight, coinbaseValueSats: input.coinbaseValueSats, windowEndShareIndex: window.windowEndShareIndex, @@ -124,9 +114,9 @@ export class PayoutSnapshotService { feeAddress: this.feeAddress, feePercent: this.feePercent, minOutputSats: this.minOutputSats, - coinbaseWeightBudget, + coinbaseWeightBudget: this.coinbaseWeightBudget, }); - const entries = this.limitCoinbaseOutputs(distribution.entries, maxCoinbaseOutputs); + const entries = this.limitCoinbaseOutputs(distribution.entries); if (entries.every(entry => !entry.includedInCoinbase)) { return null; } @@ -186,7 +176,7 @@ export class PayoutSnapshotService { ) RETURNING "id"::text AS "id" `, [ - method, + this.method, PPLNS_PAYOUT_MODE, input.blockHeight, input.coinbaseValueSats.toString(), @@ -196,7 +186,7 @@ export class PayoutSnapshotService { this.feeAddress, distribution.feeSats.toString(), this.minOutputSats, - coinbaseWeightBudget, + this.coinbaseWeightBudget, window.startBatchId, window.endBatchId, window.windowStartShareIndex, @@ -400,10 +390,9 @@ export class PayoutSnapshotService { FROM "payout_snapshot" WHERE "status" = 'finalized' AND "payoutMode" = $1 - AND "method" = $2 ORDER BY "createdAt" DESC, "id" DESC LIMIT 1 - `, [PPLNS_PAYOUT_MODE, this.method]); + `, [PPLNS_PAYOUT_MODE]); if (snapshot?.id == null) { return null; } @@ -425,7 +414,6 @@ export class PayoutSnapshotService { FROM "payout_snapshot" WHERE "status" = 'finalized' AND "payoutMode" = $2 - AND "method" = $3 ORDER BY "createdAt" DESC, "id" DESC LIMIT 1 ) @@ -449,7 +437,7 @@ export class PayoutSnapshotService { AND e."includedInCoinbase" = true AND e."payoutSats" > 0 LIMIT 1 - `, [address, PPLNS_PAYOUT_MODE, this.method]); + `, [address, PPLNS_PAYOUT_MODE]); if (row == null) { return null; @@ -566,7 +554,7 @@ export class PayoutSnapshotService { private async getExistingSnapshot( manager: EntityManager, - input: { method: string; blockHeight: number; coinbaseValueSats: number; windowEndShareIndex: string }, + input: { blockHeight: number; coinbaseValueSats: number; windowEndShareIndex: string }, ): Promise { const [snapshot] = await manager.query(` SELECT "id"::text AS "id" @@ -580,7 +568,7 @@ export class PayoutSnapshotService { ORDER BY "id" DESC LIMIT 1 `, [ - input.method, + this.method, input.blockHeight, input.coinbaseValueSats.toString(), input.windowEndShareIndex, @@ -701,7 +689,7 @@ export class PayoutSnapshotService { ]); } - private limitCoinbaseOutputs(entries: PayoutDistributionEntry[], maxCoinbaseOutputs: number): PayoutDistributionEntry[] { + private limitCoinbaseOutputs(entries: PayoutDistributionEntry[]): PayoutDistributionEntry[] { let included = 0; let removedPayoutSats = 0; const limitedEntries = entries.map(entry => { @@ -709,7 +697,7 @@ export class PayoutSnapshotService { return entry; } included++; - if (included <= maxCoinbaseOutputs) { + if (included <= this.maxCoinbaseOutputs) { return entry; } removedPayoutSats += entry.payoutSats; @@ -773,17 +761,6 @@ export class PayoutSnapshotService { return Number.isInteger(value) && value > 0 ? value : defaultValue; } - private resolvePositiveInt(value: number | undefined, defaultValue: number): number { - return Number.isInteger(value) && value > 0 ? value : defaultValue; - } - - private normalizeMethod(method?: string): string { - const normalized = method?.trim(); - return normalized == null || normalized.length === 0 - ? this.method - : normalized.slice(0, 32); - } - private readNonNegativeInt(name: string, defaultValue: number): number { const value = Number(process.env[name]); return Number.isInteger(value) && value >= 0 ? value : defaultValue; diff --git a/src/services/datum.service.spec.ts b/src/services/datum.service.spec.ts index 7fc6747..08bab77 100644 --- a/src/services/datum.service.spec.ts +++ b/src/services/datum.service.spec.ts @@ -9,7 +9,6 @@ function createService(overrides: { configService?: any; clientService?: any; redisMessagingService?: any; - payoutSnapshotService?: any; } = {}): DatumService { const configService = overrides.configService ?? { get: jest.fn((key: string) => { @@ -44,7 +43,6 @@ function createService(overrides: { {} as any, templateProvider as unknown as TemplateProviderService, overrides.redisMessagingService, - overrides.payoutSnapshotService, ); } @@ -391,60 +389,6 @@ describe('DatumService job validation', () => { ).valid).toBe(true); }); - it('creates DATUM-sized payout snapshots for PPLNS coinbaser fetches', async () => { - const payoutSnapshotService = { - createSnapshotForTemplate: jest.fn().mockResolvedValue({ - id: 'datum-snapshot-1', - payoutOutputs: [ - { address: 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', amountSats: 596 }, - ], - }), - }; - const configService = { - get: jest.fn((key: string) => { - if (key === 'NETWORK') { - return 'testnet'; - } - if (key === 'DATUM_PAYOUT_MAX_COINBASE_OUTPUTS') { - return '6'; - } - if (key === 'DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET') { - return '1500'; - } - return undefined; - }), - }; - const service = createService({ configService, payoutSnapshotService }) as any; - - const result = await service.getDatumCoinbaserPayoutContext({ - blockData: { - height: 5010000, - networkDifficulty: 42, - payoutSnapshotId: 'template-snapshot', - payoutOutputs: [ - { address: 'tb1q9r8gvnx3j4d6jvl0fqjrmy3dar4k4l3052af7q', amountSats: 596 }, - ], - }, - }, 596, 'pplns'); - - expect(payoutSnapshotService.createSnapshotForTemplate).toHaveBeenCalledWith({ - blockHeight: 5010000, - coinbaseValueSats: 596, - networkDifficulty: 42, - method: 'pplns-datum', - maxCoinbaseOutputs: 6, - coinbaseWeightBudget: 1500, - }); - expect(result.payoutSnapshotId).toBe('datum-snapshot-1'); - expect(result.payoutOutputs).toEqual([{ - value: 596n, - scriptPubKey: bitcoinjs.address.toOutputScript( - 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', - bitcoinjs.networks.testnet, - ), - }]); - }); - it('accepts DATUM coinbases matching another recent pool-issued coinbaser context for the same height', () => { const service = createService() as any; const extranonce = Buffer.alloc(12, 1); diff --git a/src/services/datum.service.ts b/src/services/datum.service.ts index f6c5e8c..e9f27ec 100644 --- a/src/services/datum.service.ts +++ b/src/services/datum.service.ts @@ -45,9 +45,6 @@ import { parsePayoutModePorts, PayoutMode } from '../types/payout-mode'; const DEFAULT_DATUM_SHARE_DIFFICULTY = 1; const DEFAULT_DATUM_PING_INTERVAL_MS = 30_000; const DEFAULT_CLIENT_HASHRATE_PERSIST_INTERVAL_MS = 60_000; -const DEFAULT_DATUM_PAYOUT_MAX_COINBASE_OUTPUTS = 10; -const DEFAULT_DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET = 2_000; -const DATUM_PAYOUT_METHOD = 'pplns-datum'; @Injectable() export class DatumService implements OnModuleInit { @@ -224,8 +221,7 @@ export class DatumService implements OnModuleInit { private async handleCoinbaserFetch(socket: Socket, state: DatumClientState, payload: Buffer): Promise { const fetch = deserializeDatumCoinbaserFetch(payload); const latestTemplate = await firstValueFrom(this.jobsService.newMiningJob$); - const payoutContext = await this.getDatumCoinbaserPayoutContext(latestTemplate, Number(fetch.rewardValue), state.payoutMode); - const payoutOutputs = payoutContext.payoutOutputs; + const payoutOutputs = this.getDatumPayoutOutputs(latestTemplate, Number(fetch.rewardValue), state.payoutMode); if (payoutOutputs.length === 0) { throw new Error('DATUM_POOL_PAYOUT_ADDRESS or DEV_FEE_ADDRESS must be set before DATUM coinbaser fetches can be served'); } @@ -233,7 +229,7 @@ export class DatumService implements OnModuleInit { const coinbaserId = this.nextDatumCoinbaserId(state); state.coinbaserPayoutContexts.set(coinbaserId, { payoutOutputs, - payoutSnapshotId: payoutContext.payoutSnapshotId, + payoutSnapshotId: latestTemplate.blockData.payoutSnapshotId ?? null, blockHeight: latestTemplate.blockData.height, payoutMode: state.payoutMode, }); @@ -248,40 +244,6 @@ export class DatumService implements OnModuleInit { await this.writeRaw(socket, state.session.encryptChannelFrame(DatumProtocolCommand.MINING, response)); } - private async getDatumCoinbaserPayoutContext( - latestTemplate: IJobTemplate, - rewardValue: number, - payoutMode: PayoutMode, - ): Promise<{ payoutOutputs: DatumPayoutOutput[]; payoutSnapshotId?: string | null }> { - if (payoutMode === 'pplns' && this.payoutSnapshotService != null) { - try { - const snapshot = await this.payoutSnapshotService.createSnapshotForTemplate({ - blockHeight: latestTemplate.blockData.height, - coinbaseValueSats: rewardValue, - networkDifficulty: latestTemplate.blockData.networkDifficulty, - method: DATUM_PAYOUT_METHOD, - maxCoinbaseOutputs: this.getDatumPayoutMaxCoinbaseOutputs(), - coinbaseWeightBudget: this.getDatumPayoutCoinbaseWeightBudget(), - }); - if (snapshot?.payoutOutputs?.length > 0) { - return { - payoutOutputs: this.getDatumPayoutOutputs({ blockData: { payoutOutputs: snapshot.payoutOutputs } }, rewardValue, payoutMode), - payoutSnapshotId: snapshot.id, - }; - } - } catch (error) { - console.error(`[DATUM] Error creating DATUM payout snapshot: ${error.message}`); - } - } - - return { - payoutOutputs: this.getDatumPayoutOutputs(latestTemplate, rewardValue, payoutMode), - payoutSnapshotId: payoutMode === 'pplns' - ? latestTemplate.blockData.payoutSnapshotId ?? null - : null, - }; - } - private async handlePowSubmit(socket: Socket, state: DatumClientState, payload: Buffer): Promise { const pow = deserializeDatumPowSubmit(payload); const { address, workerName } = this.parseUserIdentity(pow.username); @@ -947,20 +909,6 @@ export class DatumService implements OnModuleInit { return Math.pow(2, pow.targetByte); } - private getDatumPayoutMaxCoinbaseOutputs(): number { - const configured = Number(this.configService.get('DATUM_PAYOUT_MAX_COINBASE_OUTPUTS')); - return Number.isInteger(configured) && configured > 0 - ? configured - : DEFAULT_DATUM_PAYOUT_MAX_COINBASE_OUTPUTS; - } - - private getDatumPayoutCoinbaseWeightBudget(): number { - const configured = Number(this.configService.get('DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET')); - return Number.isInteger(configured) && configured > 0 - ? configured - : DEFAULT_DATUM_PAYOUT_COINBASE_WEIGHT_BUDGET; - } - private mapDatumTemplateRejectReason(errorCode?: string): DatumRejectReason { switch (errorCode) { case 'prevhash-mismatch':