From 9998c93eae3024b44ad415bec7bd1437f465227e Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 21 Jun 2026 11:39:01 -0400 Subject: [PATCH] Reject truncated DATUM PPLNS coinbases --- .env.example | 4 +- docker-compose.external-db.yml | 4 +- full-setup/docker-compose-mainnet.yml | 4 +- .../payout-snapshot.service.ts | 4 +- src/services/datum.service.spec.ts | 43 +++++++++++++++++++ 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index f69b7a4..c3b8bd8 100644 --- a/.env.example +++ b/.env.example @@ -113,11 +113,11 @@ PAYOUT_BOOTSTRAP_WINDOW=true # solo preserves current one-address coinbase payouts. Set to snapshot to pay # the latest payout snapshot directly in coinbase outputs. PAYOUT_COINBASE_MODE=solo -PAYOUT_MAX_COINBASE_OUTPUTS=10 +PAYOUT_MAX_COINBASE_OUTPUTS=200 PAYOUT_MIN_OUTPUT_SATS=546 PAYOUT_FEE_ADDRESS= PAYOUT_FEE_PERCENT=0 -PAYOUT_COINBASE_WEIGHT_BUDGET=50000 +PAYOUT_COINBASE_WEIGHT_BUDGET=26000 #redis REDIS_URL=redis://redis:6379 diff --git a/docker-compose.external-db.yml b/docker-compose.external-db.yml index 48840cb..c55f85b 100644 --- a/docker-compose.external-db.yml +++ b/docker-compose.external-db.yml @@ -108,11 +108,11 @@ services: PAYOUT_METHOD: ${PAYOUT_METHOD:-pplns} PAYOUT_WINDOW_FACTOR: ${PAYOUT_WINDOW_FACTOR:-4} PAYOUT_BOOTSTRAP_WINDOW: ${PAYOUT_BOOTSTRAP_WINDOW:-true} - PAYOUT_MAX_COINBASE_OUTPUTS: ${PAYOUT_MAX_COINBASE_OUTPUTS:-10} + PAYOUT_MAX_COINBASE_OUTPUTS: ${PAYOUT_MAX_COINBASE_OUTPUTS:-200} PAYOUT_MIN_OUTPUT_SATS: ${PAYOUT_MIN_OUTPUT_SATS:-546} PAYOUT_FEE_ADDRESS: ${PAYOUT_FEE_ADDRESS:-} PAYOUT_FEE_PERCENT: ${PAYOUT_FEE_PERCENT:-0} - PAYOUT_COINBASE_WEIGHT_BUDGET: ${PAYOUT_COINBASE_WEIGHT_BUDGET:-50000} + PAYOUT_COINBASE_WEIGHT_BUDGET: ${PAYOUT_COINBASE_WEIGHT_BUDGET:-26000} 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 cf0726d..f59777e 100644 --- a/full-setup/docker-compose-mainnet.yml +++ b/full-setup/docker-compose-mainnet.yml @@ -122,11 +122,11 @@ services: PAYOUT_WINDOW_FACTOR: ${PAYOUT_WINDOW_FACTOR:-4} PAYOUT_BOOTSTRAP_WINDOW: ${PAYOUT_BOOTSTRAP_WINDOW:-true} PAYOUT_COINBASE_MODE: ${PAYOUT_COINBASE_MODE:-solo} - PAYOUT_MAX_COINBASE_OUTPUTS: ${PAYOUT_MAX_COINBASE_OUTPUTS:-10} + PAYOUT_MAX_COINBASE_OUTPUTS: ${PAYOUT_MAX_COINBASE_OUTPUTS:-200} PAYOUT_MIN_OUTPUT_SATS: ${PAYOUT_MIN_OUTPUT_SATS:-546} PAYOUT_FEE_ADDRESS: ${PAYOUT_FEE_ADDRESS:-} PAYOUT_FEE_PERCENT: ${PAYOUT_FEE_PERCENT:-0} - PAYOUT_COINBASE_WEIGHT_BUDGET: ${PAYOUT_COINBASE_WEIGHT_BUDGET:-50000} + PAYOUT_COINBASE_WEIGHT_BUDGET: ${PAYOUT_COINBASE_WEIGHT_BUDGET:-26000} networks: bitcoin: diff --git a/src/ORM/payout-snapshot/payout-snapshot.service.ts b/src/ORM/payout-snapshot/payout-snapshot.service.ts index d3638d4..39a9579 100644 --- a/src/ORM/payout-snapshot/payout-snapshot.service.ts +++ b/src/ORM/payout-snapshot/payout-snapshot.service.ts @@ -51,11 +51,11 @@ export interface ExpectedPayout { percent: number; } -const DEFAULT_MAX_COINBASE_OUTPUTS = 10; +const DEFAULT_MAX_COINBASE_OUTPUTS = 200; const DEFAULT_MIN_OUTPUT_SATS = 546; const DEFAULT_PAYOUT_METHOD = 'pplns'; const DEFAULT_PAYOUT_WINDOW_FACTOR = 4; -const DEFAULT_COINBASE_WEIGHT_BUDGET = 50_000; +const DEFAULT_COINBASE_WEIGHT_BUDGET = 26_000; const DEFAULT_PAYOUT_BOOTSTRAP_WINDOW = true; const PPLNS_PAYOUT_MODE: PayoutMode = 'pplns'; diff --git a/src/services/datum.service.spec.ts b/src/services/datum.service.spec.ts index 08bab77..b3fdcac 100644 --- a/src/services/datum.service.spec.ts +++ b/src/services/datum.service.spec.ts @@ -329,6 +329,49 @@ describe('DatumService job validation', () => { expect(validation.error).toBe('output-count-mismatch'); }); + it('rejects DATUM PPLNS coinbases that only fit a smaller profile and add a pool remainder', () => { + const configService = { + get: jest.fn((key: string) => { + if (key === 'NETWORK') { + return 'testnet'; + } + if (key === 'DATUM_POOL_PAYOUT_ADDRESS') { + return 'tb1qdyjakeepue4trak9d3hvyelrd0aw7mwju2d0c2'; + } + return undefined; + }), + }; + const service = createService({ configService }) as any; + const extranonce = Buffer.alloc(12, 1); + const expectedOutputs = [ + { address: 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', amountSats: 421 }, + { address: 'tb1q9r8gvnx3j4d6jvl0fqjrmy3dar4k4l3052af7q', amountSats: 133 }, + { address: 'tb1qdyjakeepue4trak9d3hvyelrd0aw7mwju2d0c2', amountSats: 42 }, + ]; + const coinbase = createDatumCoinbaseSplit([ + { address: 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', amountSats: 421 }, + { address: 'tb1qdyjakeepue4trak9d3hvyelrd0aw7mwju2d0c2', amountSats: 175 }, + ], extranonce); + const latestTemplate = { + blockData: { + coinbasevalue: 596, + payoutOutputs: expectedOutputs, + }, + }; + + const validation = service.validateDatumCoinbasePayouts( + coinbase, + { extranonce, targetByte: 0 }, + latestTemplate, + 596n, + undefined, + 'pplns', + ); + + expect(validation.valid).toBe(false); + expect(validation.error).toBe('output-count-mismatch'); + }); + it('rejects DATUM coinbases that do not pay the pool snapshot outputs', () => { const service = createService() as any; const extranonce = Buffer.alloc(12, 1);