mirror of
https://github.com/mempool/mempool.git
synced 2025-02-22 14:22:44 +01:00
Add audit data replication service
This commit is contained in:
parent
f15f0570d4
commit
69e6b164b9
9 changed files with 267 additions and 0 deletions
|
@ -125,5 +125,16 @@
|
||||||
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
|
||||||
"BISQ_URL": "https://bisq.markets/api",
|
"BISQ_URL": "https://bisq.markets/api",
|
||||||
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
|
||||||
|
},
|
||||||
|
"REPLICATION": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"AUDIT": false,
|
||||||
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
|
"SERVERS": [
|
||||||
|
"list",
|
||||||
|
"of",
|
||||||
|
"trusted",
|
||||||
|
"servers"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,5 +121,11 @@
|
||||||
},
|
},
|
||||||
"CLIGHTNING": {
|
"CLIGHTNING": {
|
||||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||||
|
},
|
||||||
|
"REPLICATION": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"AUDIT": false,
|
||||||
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
|
"SERVERS": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,13 @@ describe('Mempool Backend Config', () => {
|
||||||
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||||
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(config.REPLICATION).toStrictEqual({
|
||||||
|
ENABLED: false,
|
||||||
|
AUDIT: false,
|
||||||
|
AUDIT_START_HEIGHT: 774000,
|
||||||
|
SERVERS: []
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,12 @@ interface IConfig {
|
||||||
GEOLITE2_ASN: string;
|
GEOLITE2_ASN: string;
|
||||||
GEOIP2_ISP: string;
|
GEOIP2_ISP: string;
|
||||||
},
|
},
|
||||||
|
REPLICATION: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
AUDIT: boolean;
|
||||||
|
AUDIT_START_HEIGHT: number;
|
||||||
|
SERVERS: string[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
|
@ -264,6 +270,12 @@ const defaults: IConfig = {
|
||||||
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
'GEOLITE2_ASN': '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
||||||
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
'GEOIP2_ISP': '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
||||||
},
|
},
|
||||||
|
'REPLICATION': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'AUDIT': false,
|
||||||
|
'AUDIT_START_HEIGHT': 774000,
|
||||||
|
'SERVERS': [],
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class Config implements IConfig {
|
class Config implements IConfig {
|
||||||
|
@ -283,6 +295,7 @@ class Config implements IConfig {
|
||||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||||
MAXMIND: IConfig['MAXMIND'];
|
MAXMIND: IConfig['MAXMIND'];
|
||||||
|
REPLICATION: IConfig['REPLICATION'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const configs = this.merge(configFromFile, defaults);
|
const configs = this.merge(configFromFile, defaults);
|
||||||
|
@ -302,6 +315,7 @@ class Config implements IConfig {
|
||||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||||
this.MAXMIND = configs.MAXMIND;
|
this.MAXMIND = configs.MAXMIND;
|
||||||
|
this.REPLICATION = configs.REPLICATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
merge = (...objects: object[]): IConfig => {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import bitcoinClient from './api/bitcoin/bitcoin-client';
|
||||||
import priceUpdater from './tasks/price-updater';
|
import priceUpdater from './tasks/price-updater';
|
||||||
import PricesRepository from './repositories/PricesRepository';
|
import PricesRepository from './repositories/PricesRepository';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import auditReplicator from './replication/AuditReplication';
|
||||||
|
|
||||||
export interface CoreIndex {
|
export interface CoreIndex {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -136,6 +137,7 @@ class Indexer {
|
||||||
await blocks.$generateBlocksSummariesDatabase();
|
await blocks.$generateBlocksSummariesDatabase();
|
||||||
await blocks.$generateCPFPDatabase();
|
await blocks.$generateCPFPDatabase();
|
||||||
await blocks.$generateAuditStats();
|
await blocks.$generateAuditStats();
|
||||||
|
await auditReplicator.$sync();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.indexerRunning = false;
|
this.indexerRunning = false;
|
||||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
|
|
@ -236,6 +236,15 @@ export interface BlockSummary {
|
||||||
transactions: TransactionStripped[];
|
transactions: TransactionStripped[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuditSummary extends BlockAudit {
|
||||||
|
timestamp?: number,
|
||||||
|
size?: number,
|
||||||
|
weight?: number,
|
||||||
|
tx_count?: number,
|
||||||
|
transactions: TransactionStripped[];
|
||||||
|
template?: TransactionStripped[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface BlockPrice {
|
export interface BlockPrice {
|
||||||
height: number;
|
height: number;
|
||||||
priceId: number;
|
priceId: number;
|
||||||
|
|
123
backend/src/replication/AuditReplication.ts
Normal file
123
backend/src/replication/AuditReplication.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
import { AuditSummary } from '../mempool.interfaces';
|
||||||
|
import blocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
|
import blocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
|
import { $sync } from './replicator';
|
||||||
|
import config from '../config';
|
||||||
|
import { Common } from '../api/common';
|
||||||
|
|
||||||
|
const BATCH_SIZE = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs missing block template and audit data from trusted servers
|
||||||
|
*/
|
||||||
|
class AuditReplication {
|
||||||
|
inProgress: boolean = false;
|
||||||
|
skip: Set<string> = new Set();
|
||||||
|
|
||||||
|
public async $sync(): Promise<void> {
|
||||||
|
if (!config.REPLICATION.ENABLED || !config.REPLICATION.AUDIT) {
|
||||||
|
// replication not enabled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.inProgress) {
|
||||||
|
logger.info(`AuditReplication sync already in progress`, 'Replication');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inProgress = true;
|
||||||
|
|
||||||
|
const missingAudits = await this.$getMissingAuditBlocks();
|
||||||
|
|
||||||
|
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
||||||
|
|
||||||
|
let totalSynced = 0;
|
||||||
|
let totalMissed = 0;
|
||||||
|
let loggerTimer = Date.now();
|
||||||
|
// process missing audits in batches of
|
||||||
|
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
|
||||||
|
const results = await Promise.all(missingAudits.slice(i * BATCH_SIZE, (i + 1) * BATCH_SIZE).map(hash => this.$syncAudit(hash)));
|
||||||
|
const synced = results.reduce((total, status) => status ? total + 1 : total, 0);
|
||||||
|
totalSynced += synced;
|
||||||
|
totalMissed += (BATCH_SIZE - synced);
|
||||||
|
if (Date.now() - loggerTimer > 10000) {
|
||||||
|
loggerTimer = Date.now();
|
||||||
|
logger.info(`Found ${totalSynced} / ${totalSynced + totalMissed} of ${missingAudits.length} missing audits`, 'Replication');
|
||||||
|
}
|
||||||
|
await Common.sleep$(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Fetched ${totalSynced} audits, ${totalMissed} still missing`, 'Replication');
|
||||||
|
|
||||||
|
this.inProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $syncAudit(hash: string): Promise<boolean> {
|
||||||
|
if (this.skip.has(hash)) {
|
||||||
|
// we already know none of our trusted servers have this audit
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
// start with a random server so load is uniformly spread
|
||||||
|
const syncResult = await $sync(`/api/v1/block/${hash}/audit-summary`);
|
||||||
|
if (syncResult) {
|
||||||
|
if (syncResult.data?.template?.length) {
|
||||||
|
await this.$saveAuditData(hash, syncResult.data);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
if (!syncResult.data && !syncResult.exists) {
|
||||||
|
this.skip.add(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getMissingAuditBlocks(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const startHeight = config.REPLICATION.AUDIT_START_HEIGHT || 0;
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT auditable.hash, auditable.height
|
||||||
|
FROM (
|
||||||
|
SELECT hash, height
|
||||||
|
FROM blocks
|
||||||
|
WHERE height >= ?
|
||||||
|
) AS auditable
|
||||||
|
LEFT JOIN blocks_audits ON auditable.hash = blocks_audits.hash
|
||||||
|
WHERE blocks_audits.hash IS NULL
|
||||||
|
ORDER BY auditable.height DESC
|
||||||
|
`, [startHeight]);
|
||||||
|
return rows.map(row => row.hash);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot fetch missing audit blocks from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $saveAuditData(blockHash: string, auditSummary: AuditSummary): Promise<void> {
|
||||||
|
// save audit & template to DB
|
||||||
|
await blocksSummariesRepository.$saveTemplate({
|
||||||
|
height: auditSummary.height,
|
||||||
|
template: {
|
||||||
|
id: blockHash,
|
||||||
|
transactions: auditSummary.template || []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await blocksAuditsRepository.$saveAudit({
|
||||||
|
hash: blockHash,
|
||||||
|
height: auditSummary.height,
|
||||||
|
time: auditSummary.timestamp || auditSummary.time,
|
||||||
|
missingTxs: auditSummary.missingTxs || [],
|
||||||
|
addedTxs: auditSummary.addedTxs || [],
|
||||||
|
freshTxs: auditSummary.freshTxs || [],
|
||||||
|
sigopTxs: auditSummary.sigopTxs || [],
|
||||||
|
matchRate: auditSummary.matchRate,
|
||||||
|
expectedFees: auditSummary.expectedFees,
|
||||||
|
expectedWeight: auditSummary.expectedWeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuditReplication();
|
||||||
|
|
70
backend/src/replication/replicator.ts
Normal file
70
backend/src/replication/replicator.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import config from '../config';
|
||||||
|
import backendInfo from '../api/backend-info';
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
|
import * as https from 'https';
|
||||||
|
|
||||||
|
export async function $sync(path): Promise<{ data?: any, exists: boolean }> {
|
||||||
|
// start with a random server so load is uniformly spread
|
||||||
|
let allMissing = true;
|
||||||
|
const offset = Math.floor(Math.random() * config.REPLICATION.SERVERS.length);
|
||||||
|
for (let i = 0; i < config.REPLICATION.SERVERS.length; i++) {
|
||||||
|
const server = config.REPLICATION.SERVERS[(i + offset) % config.REPLICATION.SERVERS.length];
|
||||||
|
// don't query ourself
|
||||||
|
if (server === backendInfo.getBackendInfo().hostname) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await query(`https://${server}${path}`);
|
||||||
|
if (result) {
|
||||||
|
return { data: result, exists: true };
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.response?.status === 404) {
|
||||||
|
// this server is also missing this data
|
||||||
|
} else {
|
||||||
|
// something else went wrong
|
||||||
|
allMissing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { exists: !allMissing };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function query(path): Promise<object> {
|
||||||
|
type axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': string
|
||||||
|
};
|
||||||
|
timeout: number;
|
||||||
|
httpsAgent?: https.Agent;
|
||||||
|
};
|
||||||
|
const axiosOptions: axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
||||||
|
},
|
||||||
|
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
|
const socksOptions = {
|
||||||
|
agentOptions: {
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
hostname: config.SOCKS5PROXY.HOST,
|
||||||
|
port: config.SOCKS5PROXY.PORT,
|
||||||
|
username: config.SOCKS5PROXY.USERNAME || 'circuit0',
|
||||||
|
password: config.SOCKS5PROXY.PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AxiosResponse = await axios.get(path, axiosOptions);
|
||||||
|
if (data.statusText === 'error' || !data.data) {
|
||||||
|
throw new Error(`${data.status}`);
|
||||||
|
}
|
||||||
|
return data.data;
|
||||||
|
}
|
|
@ -48,5 +48,30 @@
|
||||||
"STATISTICS": {
|
"STATISTICS": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||||
|
},
|
||||||
|
"REPLICATION": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"AUDIT": true,
|
||||||
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
|
"SERVERS": [
|
||||||
|
"node201.fmt.mempool.space",
|
||||||
|
"node202.fmt.mempool.space",
|
||||||
|
"node203.fmt.mempool.space",
|
||||||
|
"node204.fmt.mempool.space",
|
||||||
|
"node205.fmt.mempool.space",
|
||||||
|
"node206.fmt.mempool.space",
|
||||||
|
"node201.fra.mempool.space",
|
||||||
|
"node202.fra.mempool.space",
|
||||||
|
"node203.fra.mempool.space",
|
||||||
|
"node204.fra.mempool.space",
|
||||||
|
"node205.fra.mempool.space",
|
||||||
|
"node206.fra.mempool.space",
|
||||||
|
"node201.tk7.mempool.space",
|
||||||
|
"node202.tk7.mempool.space",
|
||||||
|
"node203.tk7.mempool.space",
|
||||||
|
"node204.tk7.mempool.space",
|
||||||
|
"node205.tk7.mempool.space",
|
||||||
|
"node206.tk7.mempool.space"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue