mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9998c93eae | ||
|
|
bdd3c7c3ae | ||
|
|
1943ea7768 | ||
|
|
6dcbb8436d |
+2
-2
@@ -113,11 +113,11 @@ PAYOUT_BOOTSTRAP_WINDOW=true
|
|||||||
# solo preserves current one-address coinbase payouts. Set to snapshot to pay
|
# solo preserves current one-address coinbase payouts. Set to snapshot to pay
|
||||||
# the latest payout snapshot directly in coinbase outputs.
|
# the latest payout snapshot directly in coinbase outputs.
|
||||||
PAYOUT_COINBASE_MODE=solo
|
PAYOUT_COINBASE_MODE=solo
|
||||||
PAYOUT_MAX_COINBASE_OUTPUTS=10
|
PAYOUT_MAX_COINBASE_OUTPUTS=200
|
||||||
PAYOUT_MIN_OUTPUT_SATS=546
|
PAYOUT_MIN_OUTPUT_SATS=546
|
||||||
PAYOUT_FEE_ADDRESS=
|
PAYOUT_FEE_ADDRESS=
|
||||||
PAYOUT_FEE_PERCENT=0
|
PAYOUT_FEE_PERCENT=0
|
||||||
PAYOUT_COINBASE_WEIGHT_BUDGET=50000
|
PAYOUT_COINBASE_WEIGHT_BUDGET=26000
|
||||||
|
|
||||||
#redis
|
#redis
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|||||||
@@ -108,11 +108,11 @@ services:
|
|||||||
PAYOUT_METHOD: ${PAYOUT_METHOD:-pplns}
|
PAYOUT_METHOD: ${PAYOUT_METHOD:-pplns}
|
||||||
PAYOUT_WINDOW_FACTOR: ${PAYOUT_WINDOW_FACTOR:-4}
|
PAYOUT_WINDOW_FACTOR: ${PAYOUT_WINDOW_FACTOR:-4}
|
||||||
PAYOUT_BOOTSTRAP_WINDOW: ${PAYOUT_BOOTSTRAP_WINDOW:-true}
|
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_MIN_OUTPUT_SATS: ${PAYOUT_MIN_OUTPUT_SATS:-546}
|
||||||
PAYOUT_FEE_ADDRESS: ${PAYOUT_FEE_ADDRESS:-}
|
PAYOUT_FEE_ADDRESS: ${PAYOUT_FEE_ADDRESS:-}
|
||||||
PAYOUT_FEE_PERCENT: ${PAYOUT_FEE_PERCENT:-0}
|
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:
|
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);});\""]
|
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
|
interval: 30s
|
||||||
|
|||||||
@@ -122,11 +122,11 @@ services:
|
|||||||
PAYOUT_WINDOW_FACTOR: ${PAYOUT_WINDOW_FACTOR:-4}
|
PAYOUT_WINDOW_FACTOR: ${PAYOUT_WINDOW_FACTOR:-4}
|
||||||
PAYOUT_BOOTSTRAP_WINDOW: ${PAYOUT_BOOTSTRAP_WINDOW:-true}
|
PAYOUT_BOOTSTRAP_WINDOW: ${PAYOUT_BOOTSTRAP_WINDOW:-true}
|
||||||
PAYOUT_COINBASE_MODE: ${PAYOUT_COINBASE_MODE:-solo}
|
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_MIN_OUTPUT_SATS: ${PAYOUT_MIN_OUTPUT_SATS:-546}
|
||||||
PAYOUT_FEE_ADDRESS: ${PAYOUT_FEE_ADDRESS:-}
|
PAYOUT_FEE_ADDRESS: ${PAYOUT_FEE_ADDRESS:-}
|
||||||
PAYOUT_FEE_PERCENT: ${PAYOUT_FEE_PERCENT:-0}
|
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:
|
networks:
|
||||||
bitcoin:
|
bitcoin:
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UserAgentReportNonzeroHashrate1781313000000 implements MigrationInterface {
|
||||||
|
public name = 'UserAgentReportNonzeroHashrate1781313000000';
|
||||||
|
public transaction = false;
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP MATERIALIZED VIEW IF EXISTS "user_agent_report_view"`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE MATERIALIZED VIEW "user_agent_report_view" AS
|
||||||
|
SELECT
|
||||||
|
"userAgent",
|
||||||
|
COUNT("userAgent") AS "count",
|
||||||
|
MAX("bestDifficulty") AS "bestDifficulty",
|
||||||
|
SUM("hashRate") AS "totalHashRate"
|
||||||
|
FROM "client_entity"
|
||||||
|
WHERE "deletedAt" IS NULL
|
||||||
|
AND "hashRate" > 0
|
||||||
|
GROUP BY "userAgent"
|
||||||
|
ORDER BY "totalHashRate" DESC
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_client_working_report_updated_user_agent"
|
||||||
|
ON "client_entity" ("updatedAt" DESC, "userAgent")
|
||||||
|
INCLUDE ("hashRate", "bestDifficulty")
|
||||||
|
WHERE "deletedAt" IS NULL
|
||||||
|
AND "hashRate" > 0
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`DROP INDEX CONCURRENTLY IF EXISTS "IDX_client_active_report_updated_user_agent"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX CONCURRENTLY IF EXISTS "IDX_client_working_report_updated_user_agent"`);
|
||||||
|
await queryRunner.query(`DROP MATERIALIZED VIEW IF EXISTS "user_agent_report_view"`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE MATERIALIZED VIEW "user_agent_report_view" AS
|
||||||
|
SELECT
|
||||||
|
"userAgent",
|
||||||
|
COUNT("userAgent") AS "count",
|
||||||
|
MAX("bestDifficulty") AS "bestDifficulty",
|
||||||
|
SUM("hashRate") AS "totalHashRate"
|
||||||
|
FROM "client_entity"
|
||||||
|
WHERE "deletedAt" IS NULL
|
||||||
|
GROUP BY "userAgent"
|
||||||
|
ORDER BY "totalHashRate" DESC
|
||||||
|
`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_client_active_report_updated_user_agent"
|
||||||
|
ON "client_entity" ("updatedAt" DESC, "userAgent")
|
||||||
|
INCLUDE ("hashRate", "bestDifficulty")
|
||||||
|
WHERE "deletedAt" IS NULL
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ describe('UserAgentReportService', () => {
|
|||||||
'client.updatedAt > :activeSince',
|
'client.updatedAt > :activeSince',
|
||||||
expect.objectContaining({ activeSince: expect.any(Date) }),
|
expect.objectContaining({ activeSince: expect.any(Date) }),
|
||||||
);
|
);
|
||||||
|
expect(clientRepository.queryBuilder.andWhere).toHaveBeenCalledWith('client.hashRate > 0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the materialized view when no active database clients exist', async () => {
|
it('falls back to the materialized view when no active database clients exist', async () => {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export class UserAgentReportService {
|
|||||||
.addSelect('COALESCE(SUM(client.hashRate), 0)', 'totalHashRate')
|
.addSelect('COALESCE(SUM(client.hashRate), 0)', 'totalHashRate')
|
||||||
.where('client.deletedAt IS NULL')
|
.where('client.deletedAt IS NULL')
|
||||||
.andWhere('client.updatedAt > :activeSince', { activeSince })
|
.andWhere('client.updatedAt > :activeSince', { activeSince })
|
||||||
|
.andWhere('client.hashRate > 0')
|
||||||
.groupBy('COALESCE(NULLIF(client.userAgent, \'\'), \'Other\')')
|
.groupBy('COALESCE(NULLIF(client.userAgent, \'\'), \'Other\')')
|
||||||
.orderBy('"totalHashRate"', 'DESC')
|
.orderBy('"totalHashRate"', 'DESC')
|
||||||
.getRawMany<UserAgentReportView>();
|
.getRawMany<UserAgentReportView>();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ClientEntity } from '../../client/client.entity';
|
|||||||
.addSelect('SUM(client.hashRate)', 'totalHashRate')
|
.addSelect('SUM(client.hashRate)', 'totalHashRate')
|
||||||
.from(ClientEntity, 'client')
|
.from(ClientEntity, 'client')
|
||||||
.where('client.deletedAt IS NULL')
|
.where('client.deletedAt IS NULL')
|
||||||
|
.andWhere('client.hashRate > 0')
|
||||||
.groupBy('client.userAgent')
|
.groupBy('client.userAgent')
|
||||||
.orderBy('"totalHashRate"', 'DESC')
|
.orderBy('"totalHashRate"', 'DESC')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ export interface ExpectedPayout {
|
|||||||
percent: number;
|
percent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MAX_COINBASE_OUTPUTS = 10;
|
const DEFAULT_MAX_COINBASE_OUTPUTS = 200;
|
||||||
const DEFAULT_MIN_OUTPUT_SATS = 546;
|
const DEFAULT_MIN_OUTPUT_SATS = 546;
|
||||||
const DEFAULT_PAYOUT_METHOD = 'pplns';
|
const DEFAULT_PAYOUT_METHOD = 'pplns';
|
||||||
const DEFAULT_PAYOUT_WINDOW_FACTOR = 4;
|
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 DEFAULT_PAYOUT_BOOTSTRAP_WINDOW = true;
|
||||||
const PPLNS_PAYOUT_MODE: PayoutMode = 'pplns';
|
const PPLNS_PAYOUT_MODE: PayoutMode = 'pplns';
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { BlocksSubmissionMetadata1781220000000 } from './ORM/_migrations/BlocksS
|
|||||||
import { PayoutModes1781300000000 } from './ORM/_migrations/PayoutModes1781300000000';
|
import { PayoutModes1781300000000 } from './ORM/_migrations/PayoutModes1781300000000';
|
||||||
import { ShareRollupStoragePolicy1781305000000 } from './ORM/_migrations/ShareRollupStoragePolicy1781305000000';
|
import { ShareRollupStoragePolicy1781305000000 } from './ORM/_migrations/ShareRollupStoragePolicy1781305000000';
|
||||||
import { AcceptedShareHighScores1781309000000 } from './ORM/_migrations/AcceptedShareHighScores1781309000000';
|
import { AcceptedShareHighScores1781309000000 } from './ORM/_migrations/AcceptedShareHighScores1781309000000';
|
||||||
|
import { UserAgentReportNonzeroHashrate1781313000000 } from './ORM/_migrations/UserAgentReportNonzeroHashrate1781313000000';
|
||||||
import { UserAgentReportView } from './ORM/_views/user-agent-report/user-agent-report.view';
|
import { UserAgentReportView } from './ORM/_views/user-agent-report/user-agent-report.view';
|
||||||
import { AcceptedShareEntity } from './ORM/accepted-share/accepted-share.entity';
|
import { AcceptedShareEntity } from './ORM/accepted-share/accepted-share.entity';
|
||||||
import { AddressSettingsEntity } from './ORM/address-settings/address-settings.entity';
|
import { AddressSettingsEntity } from './ORM/address-settings/address-settings.entity';
|
||||||
@@ -60,6 +61,7 @@ export const databaseMigrations = [
|
|||||||
PayoutModes1781300000000,
|
PayoutModes1781300000000,
|
||||||
ShareRollupStoragePolicy1781305000000,
|
ShareRollupStoragePolicy1781305000000,
|
||||||
AcceptedShareHighScores1781309000000,
|
AcceptedShareHighScores1781309000000,
|
||||||
|
UserAgentReportNonzeroHashrate1781313000000,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function createDatabaseOptions(env: NodeJS.ProcessEnv): TypeOrmModuleOptions & DataSourceOptions {
|
export function createDatabaseOptions(env: NodeJS.ProcessEnv): TypeOrmModuleOptions & DataSourceOptions {
|
||||||
|
|||||||
@@ -329,6 +329,49 @@ describe('DatumService job validation', () => {
|
|||||||
expect(validation.error).toBe('output-count-mismatch');
|
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', () => {
|
it('rejects DATUM coinbases that do not pay the pool snapshot outputs', () => {
|
||||||
const service = createService() as any;
|
const service = createService() as any;
|
||||||
const extranonce = Buffer.alloc(12, 1);
|
const extranonce = Buffer.alloc(12, 1);
|
||||||
@@ -389,6 +432,109 @@ describe('DatumService job validation', () => {
|
|||||||
).valid).toBe(true);
|
).valid).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
const staleOutputs = (service as any).getDatumPayoutOutputs({
|
||||||
|
blockData: {
|
||||||
|
payoutOutputs: [{ address: 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', amountSats: 596 }],
|
||||||
|
},
|
||||||
|
}, 596, 'pplns');
|
||||||
|
const submittedOutputs = (service as any).getDatumPayoutOutputs({
|
||||||
|
blockData: {
|
||||||
|
payoutOutputs: [{ address: 'tb1q9r8gvnx3j4d6jvl0fqjrmy3dar4k4l3052af7q', amountSats: 596 }],
|
||||||
|
},
|
||||||
|
}, 596, 'pplns');
|
||||||
|
const coinbase = createDatumCoinbaseSplit([
|
||||||
|
{ address: 'tb1q9r8gvnx3j4d6jvl0fqjrmy3dar4k4l3052af7q', amountSats: 596 },
|
||||||
|
], extranonce);
|
||||||
|
const latestTemplate = {
|
||||||
|
blockData: {
|
||||||
|
coinbasevalue: 596,
|
||||||
|
payoutOutputs: [{ address: 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', amountSats: 596 }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const datumJob = {
|
||||||
|
height: 4991366,
|
||||||
|
coinbaseValue: 596n,
|
||||||
|
expectedPayoutOutputs: staleOutputs,
|
||||||
|
coinbasePairs: new Map(),
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
payoutMode: 'pplns',
|
||||||
|
coinbaserPayoutContexts: new Map([
|
||||||
|
[1, {
|
||||||
|
payoutOutputs: staleOutputs,
|
||||||
|
payoutSnapshotId: 'stale',
|
||||||
|
blockHeight: 4991366,
|
||||||
|
payoutMode: 'pplns',
|
||||||
|
}],
|
||||||
|
[2, {
|
||||||
|
payoutOutputs: submittedOutputs,
|
||||||
|
payoutSnapshotId: 'submitted',
|
||||||
|
blockHeight: 4991366,
|
||||||
|
payoutMode: 'pplns',
|
||||||
|
}],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.validateDatumCoinbasePayoutContext(
|
||||||
|
coinbase,
|
||||||
|
{ extranonce, targetByte: 0 },
|
||||||
|
latestTemplate,
|
||||||
|
datumJob,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.validation.valid).toBe(true);
|
||||||
|
expect(result.context?.payoutSnapshotId).toBe('submitted');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still rejects DATUM coinbases that do not match any pool-issued coinbaser context', () => {
|
||||||
|
const service = createService() as any;
|
||||||
|
const extranonce = Buffer.alloc(12, 1);
|
||||||
|
const poolOutputs = (service as any).getDatumPayoutOutputs({
|
||||||
|
blockData: {
|
||||||
|
payoutOutputs: [{ address: 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', amountSats: 596 }],
|
||||||
|
},
|
||||||
|
}, 596, 'pplns');
|
||||||
|
const soloCoinbase = createDatumCoinbaseSplit([
|
||||||
|
{ address: 'tb1qdyjakeepue4trak9d3hvyelrd0aw7mwju2d0c2', amountSats: 596 },
|
||||||
|
], extranonce);
|
||||||
|
const latestTemplate = {
|
||||||
|
blockData: {
|
||||||
|
coinbasevalue: 596,
|
||||||
|
payoutOutputs: [{ address: 'tb1q42vtlphyjjcun9wcv9f0d9pkhup9dcf5z9k4gh', amountSats: 596 }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const datumJob = {
|
||||||
|
height: 4991366,
|
||||||
|
coinbaseValue: 596n,
|
||||||
|
expectedPayoutOutputs: poolOutputs,
|
||||||
|
coinbasePairs: new Map(),
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
payoutMode: 'pplns',
|
||||||
|
coinbaserPayoutContexts: new Map([[1, {
|
||||||
|
payoutOutputs: poolOutputs,
|
||||||
|
payoutSnapshotId: 'pool',
|
||||||
|
blockHeight: 4991366,
|
||||||
|
payoutMode: 'pplns',
|
||||||
|
}]]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = service.validateDatumCoinbasePayoutContext(
|
||||||
|
soloCoinbase,
|
||||||
|
{ extranonce, targetByte: 0 },
|
||||||
|
latestTemplate,
|
||||||
|
datumJob,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.validation.valid).toBe(false);
|
||||||
|
expect(result.validation.error).toBe('output-mismatch');
|
||||||
|
});
|
||||||
|
|
||||||
it('carries the coinbaser payout snapshot id into the DATUM job cache', () => {
|
it('carries the coinbaser payout snapshot id into the DATUM job cache', () => {
|
||||||
const service = createService() as any;
|
const service = createService() as any;
|
||||||
const payoutOutputs = [{
|
const payoutOutputs = [{
|
||||||
|
|||||||
@@ -296,12 +296,16 @@ export class DatumService implements OnModuleInit {
|
|||||||
await this.sendShareResponse(socket, state, DatumShareResponseStatus.REJECTED, DatumRejectReason.OTHER, pow.nonce, pow.targetByte, pow.jobId);
|
await this.sendShareResponse(socket, state, DatumShareResponseStatus.REJECTED, DatumRejectReason.OTHER, pow.nonce, pow.targetByte, pow.jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payoutValidation = this.validateDatumCoinbasePayouts(coinbase, pow, latestTemplate, datumJob.coinbaseValue, datumJob.expectedPayoutOutputs, state.payoutMode);
|
const payoutValidation = this.validateDatumCoinbasePayoutContext(coinbase, pow, latestTemplate, datumJob, state);
|
||||||
if (!payoutValidation.valid) {
|
if (!payoutValidation.validation.valid) {
|
||||||
this.logDatumCoinbaseMismatchOnce(state, payoutValidation);
|
this.logDatumCoinbaseMismatchOnce(state, payoutValidation.validation);
|
||||||
await this.sendShareResponse(socket, state, DatumShareResponseStatus.REJECTED, DatumRejectReason.BAD_COINBASER_ID, pow.nonce, pow.targetByte, pow.jobId);
|
await this.sendShareResponse(socket, state, DatumShareResponseStatus.REJECTED, DatumRejectReason.BAD_COINBASER_ID, pow.nonce, pow.targetByte, pow.jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (payoutValidation.context != null) {
|
||||||
|
datumJob.expectedPayoutOutputs = payoutValidation.context.payoutOutputs;
|
||||||
|
datumJob.payoutSnapshotId = payoutValidation.context.payoutSnapshotId;
|
||||||
|
}
|
||||||
|
|
||||||
const shareDifficulty = this.getDatumSubmittedShareDifficulty(pow);
|
const shareDifficulty = this.getDatumSubmittedShareDifficulty(pow);
|
||||||
const nBits = datumJob.nBits.readUInt32LE(0);
|
const nBits = datumJob.nBits.readUInt32LE(0);
|
||||||
@@ -623,6 +627,52 @@ export class DatumService implements OnModuleInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validateDatumCoinbasePayoutContext(
|
||||||
|
coinbase: { coinb1: Buffer; coinb2: Buffer },
|
||||||
|
pow: DatumPowSubmit,
|
||||||
|
latestTemplate: IJobTemplate,
|
||||||
|
datumJob: DatumJobCache,
|
||||||
|
state: DatumClientState,
|
||||||
|
): { validation: DatumCoinbasePayoutValidation; context?: DatumCoinbaserPayoutContext } {
|
||||||
|
const primaryValidation = this.validateDatumCoinbasePayouts(
|
||||||
|
coinbase,
|
||||||
|
pow,
|
||||||
|
latestTemplate,
|
||||||
|
datumJob.coinbaseValue,
|
||||||
|
datumJob.expectedPayoutOutputs,
|
||||||
|
state.payoutMode,
|
||||||
|
);
|
||||||
|
if (primaryValidation.valid) {
|
||||||
|
return { validation: primaryValidation };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const context of state.coinbaserPayoutContexts.values()) {
|
||||||
|
if (context.payoutMode != null && context.payoutMode !== state.payoutMode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (context.blockHeight != null && datumJob.height != null && context.blockHeight !== datumJob.height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (context.payoutOutputs === datumJob.expectedPayoutOutputs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = this.validateDatumCoinbasePayouts(
|
||||||
|
coinbase,
|
||||||
|
pow,
|
||||||
|
latestTemplate,
|
||||||
|
datumJob.coinbaseValue,
|
||||||
|
context.payoutOutputs,
|
||||||
|
state.payoutMode,
|
||||||
|
);
|
||||||
|
if (validation.valid) {
|
||||||
|
return { validation, context };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { validation: primaryValidation };
|
||||||
|
}
|
||||||
|
|
||||||
private getDatumPayoutOutputs(latestTemplate: { blockData?: { payoutOutputs?: AddressObject[] } }, rewardValue: number, payoutMode: PayoutMode = 'solo'): DatumPayoutOutput[] {
|
private getDatumPayoutOutputs(latestTemplate: { blockData?: { payoutOutputs?: AddressObject[] } }, rewardValue: number, payoutMode: PayoutMode = 'solo'): DatumPayoutOutput[] {
|
||||||
const configuredOutputs = latestTemplate?.blockData?.payoutOutputs;
|
const configuredOutputs = latestTemplate?.blockData?.payoutOutputs;
|
||||||
const payoutAddresses = payoutMode === 'pplns' && configuredOutputs?.length > 0
|
const payoutAddresses = payoutMode === 'pplns' && configuredOutputs?.length > 0
|
||||||
|
|||||||
@@ -603,15 +603,24 @@ describe('TimescaleDB and Redis integration', () => {
|
|||||||
redisMessagingService,
|
redisMessagingService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeClient = await repository.save({
|
await repository.save({
|
||||||
address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
||||||
clientName: 'active-live-worker',
|
clientName: 'idle-live-worker',
|
||||||
sessionId: '91b2c3d4',
|
sessionId: '91b2c3d4',
|
||||||
userAgent,
|
userAgent,
|
||||||
startTime: new Date(),
|
startTime: new Date(),
|
||||||
bestDifficulty: 11,
|
bestDifficulty: 11,
|
||||||
hashRate: 0,
|
hashRate: 0,
|
||||||
});
|
});
|
||||||
|
const activeClient = await repository.save({
|
||||||
|
address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
||||||
|
clientName: 'active-live-worker',
|
||||||
|
sessionId: '93b2c3d4',
|
||||||
|
userAgent,
|
||||||
|
startTime: new Date(),
|
||||||
|
bestDifficulty: 33,
|
||||||
|
hashRate: 300,
|
||||||
|
});
|
||||||
const staleClient = await repository.save({
|
const staleClient = await repository.save({
|
||||||
address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
address: 'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
||||||
clientName: 'stale-live-worker',
|
clientName: 'stale-live-worker',
|
||||||
@@ -628,7 +637,7 @@ describe('TimescaleDB and Redis integration', () => {
|
|||||||
userAgent,
|
userAgent,
|
||||||
count: '1',
|
count: '1',
|
||||||
bestDifficulty: Number(activeClient.bestDifficulty),
|
bestDifficulty: Number(activeClient.bestDifficulty),
|
||||||
totalHashRate: '0',
|
totalHashRate: String(activeClient.hashRate),
|
||||||
}]));
|
}]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user