Compare commits

2 Commits
Author SHA1 Message Date
Ben f49e7b5b8f Accept BIP320 version rolling in SV2 shares 2026-08-03 10:58:23 -04:00
Ben 3cb4cbfbdf Isolate API worker module 2026-08-03 10:27:03 -04:00
8 changed files with 118 additions and 14 deletions
+62
View File
@@ -0,0 +1,62 @@
import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AddressController } from './controllers/address/address.controller';
import { ClientController } from './controllers/client/client.controller';
import { UserAgentReportModule } from './ORM/_views/user-agent-report/user-agent-report.module';
import { AddressSettingsModule } from './ORM/address-settings/address-settings.module';
import { BlocksModule } from './ORM/blocks/blocks.module';
import { ClientStatisticsModule } from './ORM/client-statistics/client-statistics.module';
import { ClientModule } from './ORM/client/client.module';
import { PayoutSnapshotModule } from './ORM/payout-snapshot/payout-snapshot.module';
import { RpcBlocksModule } from './ORM/rpc-block/rpc-block.module';
import { ShareAccountingModule } from './ORM/share-accounting/share-accounting.module';
import { createDatabaseOptions } from './database.config';
import { BitcoinAddressValidator } from './models/validators/bitcoin-address.validator';
import { BitcoinRpcService } from './services/bitcoin-rpc.service';
import { RedisMessagingModule } from './services/redis-messaging.module';
import { Sv2AuthorityService } from './services/sv2-authority.service';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => createDatabaseOptions({
...process.env,
DB_HOST: configService.get('DB_HOST'),
DB_PORT: configService.get('DB_PORT'),
DB_USERNAME: configService.get('DB_USERNAME'),
DB_PASSWORD: configService.get('DB_PASSWORD'),
DB_DATABASE: configService.get('DB_DATABASE'),
DB_LOGGING: configService.get('DB_LOGGING'),
DB_POOL_SIZE: configService.get('DB_POOL_SIZE'),
}),
imports: [ConfigModule],
inject: [ConfigService],
}),
CacheModule.register(),
RedisMessagingModule,
ClientStatisticsModule,
ClientModule,
AddressSettingsModule,
BlocksModule,
RpcBlocksModule,
UserAgentReportModule,
ShareAccountingModule,
PayoutSnapshotModule,
],
controllers: [
AppController,
ClientController,
AddressController,
],
providers: [
BitcoinRpcService,
BitcoinAddressValidator,
Sv2AuthorityService,
],
})
export class ApiModule {}
+2 -2
View File
@@ -6,7 +6,7 @@ describe('AppController', () => {
blocksService?: any; blocksService?: any;
addressSettingsService?: any; addressSettingsService?: any;
userAgentReportService?: any; userAgentReportService?: any;
stratumV2Service?: any; sv2AuthorityService?: any;
redisMessagingService?: any; redisMessagingService?: any;
} = {}) => { } = {}) => {
return new AppController( return new AppController(
@@ -26,7 +26,7 @@ describe('AppController', () => {
overrides.userAgentReportService ?? { overrides.userAgentReportService ?? {
getReport: jest.fn().mockResolvedValue([]), getReport: jest.fn().mockResolvedValue([]),
}, },
overrides.stratumV2Service ?? { overrides.sv2AuthorityService ?? {
getPoolAuthorityPublicKey: jest.fn().mockResolvedValue({ getPoolAuthorityPublicKey: jest.fn().mockResolvedValue({
publicKey: '', publicKey: '',
configured: false, configured: false,
+3 -3
View File
@@ -10,9 +10,9 @@ import { ClientStatisticsService } from './ORM/client-statistics/client-statisti
import { ClientService } from './ORM/client/client.service'; import { ClientService } from './ORM/client/client.service';
import { BitcoinRpcService } from './services/bitcoin-rpc.service'; import { BitcoinRpcService } from './services/bitcoin-rpc.service';
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 { StratumV2Service } from './services/stratum-v2.service';
import { ShareAccountingService } from './ORM/share-accounting/share-accounting.service'; import { ShareAccountingService } from './ORM/share-accounting/share-accounting.service';
import { RedisMessagingService } from './services/redis-messaging.service'; import { RedisMessagingService } from './services/redis-messaging.service';
import { Sv2AuthorityService } from './services/sv2-authority.service';
import { normalizePayoutMode } from './types/payout-mode'; import { normalizePayoutMode } from './types/payout-mode';
@Controller() @Controller()
@@ -30,7 +30,7 @@ export class AppController {
private readonly bitcoinRpcService: BitcoinRpcService, private readonly bitcoinRpcService: BitcoinRpcService,
private readonly addressSettingsService: AddressSettingsService, private readonly addressSettingsService: AddressSettingsService,
private readonly userAgentReportService: UserAgentReportService, private readonly userAgentReportService: UserAgentReportService,
private readonly stratumV2Service: StratumV2Service, private readonly sv2AuthorityService: Sv2AuthorityService,
private readonly shareAccountingService: ShareAccountingService, private readonly shareAccountingService: ShareAccountingService,
private readonly redisMessagingService: RedisMessagingService private readonly redisMessagingService: RedisMessagingService
) { } ) { }
@@ -78,7 +78,7 @@ export class AppController {
const [blockData, highScores, poolAuthority, userAgentReport] = await Promise.all([ const [blockData, highScores, poolAuthority, userAgentReport] = await Promise.all([
withInfoTimeout('found blocks', this.blocksService.getFoundBlocks(), staleInfo?.blockData ?? []), withInfoTimeout('found blocks', this.blocksService.getFoundBlocks(), staleInfo?.blockData ?? []),
withInfoTimeout('high scores', this.addressSettingsService.getHighScores(), staleInfo?.highScores ?? []), withInfoTimeout('high scores', this.addressSettingsService.getHighScores(), staleInfo?.highScores ?? []),
withInfoTimeout('SV2 authority', this.stratumV2Service.getPoolAuthorityPublicKey(), { withInfoTimeout('SV2 authority', this.sv2AuthorityService.getPoolAuthorityPublicKey(), {
publicKey: staleInfo?.sv2?.poolAuthorityPublicKey ?? '', publicKey: staleInfo?.sv2?.poolAuthorityPublicKey ?? '',
configured: staleInfo?.sv2?.authorityKeyConfigured ?? false configured: staleInfo?.sv2?.authorityKeyConfigured ?? false
}), }),
+2
View File
@@ -33,6 +33,7 @@ import { StratumV1Service } from './services/stratum-v1.service';
import { Sv2JobDeclarationRegistryService } from './services/sv2-job-declaration-registry.service'; import { Sv2JobDeclarationRegistryService } from './services/sv2-job-declaration-registry.service';
import { Sv2JobDeclarationService } from './services/sv2-job-declaration.service'; import { Sv2JobDeclarationService } from './services/sv2-job-declaration.service';
import { Sv2TemplateDistributionService } from './services/sv2-template-distribution.service'; import { Sv2TemplateDistributionService } from './services/sv2-template-distribution.service';
import { Sv2AuthorityService } from './services/sv2-authority.service';
import { StratumV2Service } from './services/stratum-v2.service'; import { StratumV2Service } from './services/stratum-v2.service';
import { TelegramService } from './services/telegram.service'; import { TelegramService } from './services/telegram.service';
import { TemplateProviderService } from './services/template-provider.service'; import { TemplateProviderService } from './services/template-provider.service';
@@ -94,6 +95,7 @@ const ORMModules = [
Sv2JobDeclarationRegistryService, Sv2JobDeclarationRegistryService,
Sv2JobDeclarationService, Sv2JobDeclarationService,
Sv2TemplateDistributionService, Sv2TemplateDistributionService,
Sv2AuthorityService,
CustomWorkService, CustomWorkService,
DatumService, DatumService,
BTCPayService, BTCPayService,
+4 -2
View File
@@ -7,6 +7,7 @@ import { readFileSync, watch } from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as ecc from 'tiny-secp256k1'; import * as ecc from 'tiny-secp256k1';
import { ApiModule } from './api.module';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
async function bootstrap() { async function bootstrap() {
@@ -32,7 +33,8 @@ async function bootstrap() {
}; };
} }
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(options)); const appModule = process.env.API_ONLY === 'true' ? ApiModule : AppModule;
const app = await NestFactory.create<NestFastifyApplication>(appModule, new FastifyAdapter(options));
app.setGlobalPrefix('api'); app.setGlobalPrefix('api');
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
@@ -54,7 +56,7 @@ async function bootstrap() {
}); });
app.enableCors(); app.enableCors();
useContainer(app.select(AppModule), { fallbackOnErrors: true }); useContainer(app.select(appModule), { fallbackOnErrors: true });
// Taproot // Taproot
bitcoinjs.initEccLib(ecc); bitcoinjs.initEccLib(ecc);
+3 -2
View File
@@ -991,7 +991,7 @@ describe('StratumV2Client extended channels', () => {
.toBe(parseInt(activation.bits, 16)); .toBe(parseInt(activation.bits, 16));
}); });
it('validates fixed/rolling versions, required bits, and the advertised minimum nTime', async () => { it('validates BIP320 version rolling, required bits, and the advertised minimum nTime', async () => {
const { client } = await createClient(); const { client } = await createClient();
const now = process.hrtime.bigint(); const now = process.hrtime.bigint();
const context = { const context = {
@@ -1007,11 +1007,12 @@ describe('StratumV2Client extended channels', () => {
}; };
expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 100)).toBe(true); expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 100)).toBe(true);
expect((client as any).isSubmissionHeaderValid(context, 0x20002004, 100)).toBe(false); expect((client as any).isSubmissionHeaderValid(context, 0x20002004, 100)).toBe(true);
expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 99)).toBe(false); expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 99)).toBe(false);
expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 101)).toBe(true); expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 101)).toBe(true);
expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 0xffffffff)).toBe(true); expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 0xffffffff)).toBe(true);
expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 0x1_0000_0000)).toBe(false); expect((client as any).isSubmissionHeaderValid(context, 0x20000004, 0x1_0000_0000)).toBe(false);
expect((client as any).isSubmissionHeaderValid(context, 0x00000004, 100)).toBe(false);
(client as any).versionRollingEnabled = true; (client as any).versionRollingEnabled = true;
expect((client as any).isSubmissionHeaderValid(context, 0x20002004, 100)).toBe(true); expect((client as any).isSubmissionHeaderValid(context, 0x20002004, 100)).toBe(true);
+4 -4
View File
@@ -2088,13 +2088,13 @@ export class StratumV2Client {
): boolean { ): boolean {
const submitted = submittedVersion >>> 0; const submitted = submittedVersion >>> 0;
const base = headerContext.baseVersion >>> 0; const base = headerContext.baseVersion >>> 0;
if (this.versionRollingEnabled) { // Some SV2 clients roll BIP320 general-purpose version bits even when
// they did not explicitly require version rolling during setup. Those
// bits are non-consensus signalling space; reject only changes outside
// that mask and continue enforcing required bits below.
if (((submitted ^ base) & BIP320_CONSENSUS_VERSION_MASK) !== 0) { if (((submitted ^ base) & BIP320_CONSENSUS_VERSION_MASK) !== 0) {
return false; return false;
} }
} else if (submitted !== base) {
return false;
}
if ((submitted & headerContext.requiredVersionBits) !== headerContext.requiredVersionBits) { if ((submitted & headerContext.requiredVersionBits) !== headerContext.requiredVersionBits) {
return false; return false;
} }
+37
View File
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';
import { encodeSv2AuthorityPublicKey } from '../models/sv2/sv2-authority-key';
import { xOnlyPubKeyFromPriv } from '../models/sv2/sv2-noise';
@Injectable()
export class Sv2AuthorityService {
private authorityPublicKey: string | null = null;
private authorityKeyConfigured = false;
constructor(
private readonly configService: ConfigService,
) {}
public async getPoolAuthorityPublicKey(): Promise<{ publicKey: string; configured: boolean }> {
if (this.authorityPublicKey != null) {
return {
publicKey: this.authorityPublicKey,
configured: this.authorityKeyConfigured,
};
}
const configuredAuthorityKey = this.configService.get<string>('SV2_AUTHORITY_PRIVKEY');
this.authorityKeyConfigured = configuredAuthorityKey?.length === 64;
const authorityPrivKey = configuredAuthorityKey?.length === 64
? Buffer.from(configuredAuthorityKey, 'hex')
: crypto.randomBytes(32);
this.authorityPublicKey = encodeSv2AuthorityPublicKey(xOnlyPubKeyFromPriv(authorityPrivKey));
return {
publicKey: this.authorityPublicKey,
configured: this.authorityKeyConfigured,
};
}
}