Compare commits

4 Commits
Author SHA1 Message Date
Ben 9998c93eae Reject truncated DATUM PPLNS coinbases 2026-06-21 11:39:01 -04:00
Ben bdd3c7c3ae Revert "Fix DATUM PPLNS coinbaser snapshots"
This reverts commit 1943ea7768.
2026-06-21 11:12:09 -04:00
Ben 1943ea7768 Fix DATUM PPLNS coinbaser snapshots 2026-06-21 11:07:41 -04:00
Ben 6dcbb8436d Reduce DATUM false rejects and filter miner report 2026-06-21 10:27:58 -04:00
12 changed files with 278 additions and 14 deletions
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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';
+2
View File
@@ -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 {
+146
View File
@@ -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 = [{
+53 -3
View File
@@ -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
+12 -3
View File
@@ -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),
}])); }]));
}); });