Revert "Fix DATUM PPLNS coinbaser snapshots"

This reverts commit 1943ea7768.
This commit is contained in:
Ben
2026-06-21 11:12:09 -04:00
parent 1943ea7768
commit bdd3c7c3ae
8 changed files with 18 additions and 214 deletions
-2
View File
@@ -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=
-2
View File
@@ -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}
-2
View File
@@ -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
-2
View File
@@ -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:
@@ -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');
});
@@ -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<PayoutSnapshotForTemplate | null> {
public async createSnapshotForTemplate(input: {
blockHeight: number;
coinbaseValueSats: number;
networkDifficulty: number;
}): Promise<PayoutSnapshotForTemplate | null> {
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<PayoutSnapshotForTemplate | null> {
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;
-56
View File
@@ -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);
+2 -54
View File
@@ -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<void> {
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<void> {
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<string>('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<string>('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':