Compare commits

...

6 Commits

Author SHA1 Message Date
Mononaut
5682b157c1
support for acceleration mempool blocks animation 2023-06-13 13:35:25 -04:00
Mononaut
77b0a8eccc
Refactor accelerated audits 2023-06-07 16:47:54 -04:00
Mononaut
6af6274f43
Implement accelerations API & config setting 2023-06-07 16:47:54 -04:00
Mononaut
379fa3fe49
Refactor acceleration tracking 2023-06-07 16:47:54 -04:00
Mononaut
fe76a0fd08
use accelerated rates for block templates & show in viz 2023-06-07 16:47:54 -04:00
Mononaut
412d83f961
include accelerated tx data in block audits 2023-06-07 16:47:52 -04:00
31 changed files with 285 additions and 55 deletions

View File

@ -122,5 +122,9 @@
"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"
},
"MEMPOOL_SERVICES": {
"API": "https://mempool.space/api",
"ACCELERATIONS": false
} }
} }

View File

@ -118,5 +118,9 @@
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"
},
"MEMPOOl_SERVICES": {
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": "__MEMPOOL_SERVICES_ACCELERATIONS__"
} }
} }

View File

@ -117,6 +117,11 @@ 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.MEMPOOL_SERVICES).toStrictEqual({
API: "",
ACCELERATIONS: false,
});
}); });
}); });
@ -150,6 +155,8 @@ describe('Mempool Backend Config', () => {
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER); expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER); expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
}); });
}); });

View File

@ -5,15 +5,16 @@ import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.in
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit { class Audit {
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], score: number, similarity: number } { : { censored: string[], added: string[], fresh: string[], sigop: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], score: 0, similarity: 1 }; return { censored: [], added: [], fresh: [], sigop: [], accelerated: [], score: 0, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
let displacedWeight = 0; let displacedWeight = 0;
@ -26,6 +27,9 @@ class Audit {
const now = Math.round((Date.now() / 1000)); const now = Math.round((Date.now() / 1000));
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = tx; inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
}
} }
// coinbase is always expected // coinbase is always expected
if (transactions[0]) { if (transactions[0]) {
@ -138,6 +142,7 @@ class Audit {
added, added,
fresh, fresh,
sigop: [], sigop: [],
accelerated,
score, score,
similarity, similarity,
}; };

View File

@ -213,6 +213,7 @@ class BitcoinRoutes {
effectiveFeePerVsize: tx.effectiveFeePerVsize || null, effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops, sigops: tx.sigops,
adjustedVsize: tx.adjustedVsize, adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
}); });
return; return;
} }

View File

@ -101,6 +101,7 @@ export class Common {
fee: tx.fee, fee: tx.fee,
vsize: tx.weight / 4, vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0), value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
acc: tx.acceleration || undefined,
rate: tx.effectiveFeePerVsize, rate: tx.effectiveFeePerVsize,
}; };
} }

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 61; private static currentVersion = 62;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -533,6 +533,10 @@ class DatabaseMigration {
await this.updateToSchemaVersion(61); await this.updateToSchemaVersion(61);
} }
if (databaseSchemaVersion < 62 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(61);
}
} }
/** /**

View File

@ -4,6 +4,7 @@ import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config'; import config from '../config';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
import path from 'path'; import path from 'path';
import mempool from './mempool';
class MempoolBlocks { class MempoolBlocks {
private mempoolBlocks: MempoolBlockWithTransactions[] = []; private mempoolBlocks: MempoolBlockWithTransactions[] = [];
@ -169,7 +170,7 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = []; let added: TransactionStripped[] = [];
let removed: string[] = []; let removed: string[] = [];
const changed: { txid: string, rate: number | undefined }[] = []; const changed: { txid: string, rate: number | undefined, acc: number | undefined }[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) { if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions; added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) { } else if (!mempoolBlocks[i] && prevBlocks[i]) {
@ -191,8 +192,8 @@ class MempoolBlocks {
mempoolBlocks[i].transactions.forEach(tx => { mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) { if (!prevIds[tx.txid]) {
added.push(tx); added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate) { } else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
changed.push({ txid: tx.txid, rate: tx.rate }); changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
} }
}); });
} }
@ -205,15 +206,17 @@ class MempoolBlocks {
return mempoolBlockDeltas; return mempoolBlockDeltas;
} }
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> { public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now(); const start = Date.now();
// reset mempool short ids // reset mempool short ids
this.resetUids(); this.resetUids();
for (const tx of Object.values(newMempool)) { for (const tx of Object.values(newMempool)) {
this.setUid(tx); this.setUid(tx, true);
} }
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const strippedMempool: Map<number, CompactThreadTransaction> = new Map(); const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
@ -221,7 +224,7 @@ class MempoolBlocks {
if (entry.uid != null) { if (entry.uid != null) {
strippedMempool.set(entry.uid, { strippedMempool.set(entry.uid, {
uid: entry.uid, uid: entry.uid,
fee: entry.fee, fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
weight: (entry.adjustedVsize * 4), weight: (entry.adjustedVsize * 4),
sigops: entry.sigops, sigops: entry.sigops,
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize, feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
@ -260,7 +263,7 @@ class MempoolBlocks {
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults); const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`); logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed; return processed;
} catch (e) { } catch (e) {
@ -269,25 +272,29 @@ class MempoolBlocks {
return this.mempoolBlocks; return this.mempoolBlocks;
} }
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], saveResults: boolean = false): Promise<void> { public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
if (!this.txSelectionWorker) { if (!this.txSelectionWorker) {
// need to reset the worker // need to reset the worker
await this.$makeBlockTemplates(newMempool, saveResults); await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
return; return;
} }
const start = Date.now(); const start = Date.now();
for (const tx of Object.values(added)) { const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
for (const tx of addedAndChanged) {
this.setUid(tx); this.setUid(tx);
} }
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[]; const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => { const addedStripped: CompactThreadTransaction[] = addedAndChanged.filter(entry => entry.uid != null).map(entry => {
return { return {
uid: entry.uid || 0, uid: entry.uid || 0,
fee: entry.fee, fee: entry.fee + (useAccelerations ? (accelerations[entry.txid] || 0) : 0),
weight: (entry.adjustedVsize * 4), weight: (entry.adjustedVsize * 4),
sigops: entry.sigops, sigops: entry.sigops,
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize, feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
@ -314,14 +321,14 @@ class MempoolBlocks {
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults); this.processBlockTemplates(newMempool, blocks, rates, clusters, accelerations, saveResults);
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`); logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
} catch (e) { } catch (e) {
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
} }
} }
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] { private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, accelerations, saveResults): MempoolBlockWithTransactions[] {
for (const txid of Object.keys(rates)) { for (const txid of Object.keys(rates)) {
if (txid in mempool) { if (txid in mempool) {
mempool[txid].effectiveFeePerVsize = rates[txid]; mempool[txid].effectiveFeePerVsize = rates[txid];
@ -360,6 +367,8 @@ class MempoolBlocks {
}; };
mempoolTx.cpfpChecked = true; mempoolTx.cpfpChecked = true;
mempoolTx.acceleration = accelerations[txid];
// online calculation of stack-of-blocks fee stats // online calculation of stack-of-blocks fee stats
if (hasBlockStack && blockIndex === blocks.length - 1 && feeStatsCalculator) { if (hasBlockStack && blockIndex === blocks.length - 1 && feeStatsCalculator) {
feeStatsCalculator.processNext(mempoolTx); feeStatsCalculator.processNext(mempoolTx);
@ -449,12 +458,18 @@ class MempoolBlocks {
this.nextUid = 1; this.nextUid = 1;
} }
private setUid(tx: MempoolTransactionExtended): number { // use reset=true to overwrite existing uids held by tx objects (required after resetUids)
const uid = this.nextUid; private setUid(tx: MempoolTransactionExtended, reset = false): number {
let uid = reset ? null : this.getUid(tx);
if (uid == null) {
uid = this.nextUid;
this.nextUid++; this.nextUid++;
this.uidMap.set(uid, tx.txid); this.uidMap.set(uid, tx.txid);
tx.uid = uid; tx.uid = uid;
return uid; return uid;
} else {
return uid;
}
} }
private getUid(tx: MempoolTransactionExtended): number | void { private getUid(tx: MempoolTransactionExtended): number | void {

View File

@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import accelerationApi, { Acceleration } from './services/acceleration';
class Mempool { class Mempool {
private inSync: boolean = false; private inSync: boolean = false;
@ -18,9 +19,11 @@ class Mempool {
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[]) => void) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
private accelerations: { [txId: string]: number } = {};
private txPerSecondArray: number[] = []; private txPerSecondArray: number[] = [];
private txPerSecond: number = 0; private txPerSecond: number = 0;
@ -65,12 +68,12 @@ class Mempool {
} }
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn; this.mempoolChangedCallback = fn;
} }
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn; this.$asyncMempoolChangedCallback = fn;
} }
@ -90,10 +93,10 @@ class Mempool {
} }
} }
if (this.mempoolChangedCallback) { if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []); this.mempoolChangedCallback(this.mempoolCache, [], [], []);
} }
if (this.$asyncMempoolChangedCallback) { if (this.$asyncMempoolChangedCallback) {
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []); await this.$asyncMempoolChangedCallback(this.mempoolCache, [], [], []);
} }
this.addToSpendMap(Object.values(this.mempoolCache)); this.addToSpendMap(Object.values(this.mempoolCache));
} }
@ -229,6 +232,11 @@ class Mempool {
} }
} }
const accelerationDelta = await this.$updateAccelerations();
if (accelerationDelta.length) {
hasChange = true;
}
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
@ -241,11 +249,11 @@ class Mempool {
this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length); this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
} }
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.updateTimerProgress(timer, 'running async mempool callback'); this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
this.updateTimerProgress(timer, 'completed async mempool callback'); this.updateTimerProgress(timer, 'completed async mempool callback');
} }
@ -256,6 +264,48 @@ class Mempool {
this.clearTimer(timer); this.clearTimer(timer);
} }
public getAccelerations(): { [txid: string]: number } {
return this.accelerations;
}
public async $updateAccelerations(): Promise<string[]> {
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
return [];
}
try {
const newAccelerations = await accelerationApi.$fetchAccelerations();
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: number } = {};
for (const acceleration of newAccelerations) {
newAccelerationMap[acceleration.txid] = acceleration.feeDelta;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid] !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
this.accelerations = newAccelerationMap;
return changed;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];
}
}
private startTimer() { private startTimer() {
const state: any = { const state: any = {
start: Date.now(), start: Date.now(),

View File

@ -0,0 +1,38 @@
import { query } from '../../utils/axios-query';
import config from '../../config';
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
export interface Acceleration {
txid: string,
feeDelta: number,
}
class AccelerationApi {
public async $fetchAccelerations(): Promise<Acceleration[]> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerations`);
return (response as Acceleration[]) || [];
} else {
return [];
}
}
public async $fetchPools(): Promise<PoolTag[]> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
const response = await query(`${config.MEMPOOL_SERVICES.API}/partners`);
return (response as PoolTag[]) || [];
} else {
return [];
}
}
public async $isAcceleratedBlock(block: BlockExtended): Promise<boolean> {
const pools = await this.$fetchPools();
if (block?.extras?.pool?.id == null) {
return false;
}
return pools.reduce((match, tag) => match || tag.uniqueId === block.extras.pool.id, false);
}
}
export default new AccelerationApi();

View File

@ -21,6 +21,7 @@ import Audit from './audit';
import { deepClone } from '../utils/clone'; import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository'; import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
class WebsocketHandler { class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -144,6 +145,7 @@ class WebsocketHandler {
response['txPosition'] = { response['txPosition'] = {
txid: trackTxid, txid: trackTxid,
position: tx.position, position: tx.position,
accelerated: tx.acceleration || undefined,
}; };
} }
} else { } else {
@ -302,7 +304,7 @@ class WebsocketHandler {
} }
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
@ -310,7 +312,7 @@ class WebsocketHandler {
this.printLogs(); this.printLogs();
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true); await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
} else { } else {
mempoolBlocks.updateMempoolBlocks(newMempool, true); mempoolBlocks.updateMempoolBlocks(newMempool, true);
} }
@ -544,22 +546,27 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT) { if (config.MEMPOOL.AUDIT) {
let projectedBlocks; let projectedBlocks;
let auditMempool = _memPool; let auditMempool = _memPool;
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && await accelerationApi.$isAcceleratedBlock(block);
// template calculation functions have mempool side effects, so calculate audits using // template calculation functions have mempool side effects, so calculate audits using
// a cloned copy of the mempool if we're running a different algorithm for mempool updates // a cloned copy of the mempool if we're running a different algorithm for mempool updates
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL; const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
if (separateAudit) { if (separateAudit) {
auditMempool = deepClone(_memPool); auditMempool = deepClone(_memPool);
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) { if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false); projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
} else { } else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false); projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
} }
} else {
if ((config.MEMPOOL_SERVICES.ACCELERATIONS && !isAccelerated)) {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated);
} else { } else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
} }
}
if (Common.indexingEnabled() && memPool.isInSync()) { if (Common.indexingEnabled() && memPool.isInSync()) {
const { censored, added, fresh, sigop, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const { censored, added, fresh, sigop, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100; const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
@ -587,6 +594,7 @@ class WebsocketHandler {
missingTxs: censored, missingTxs: censored,
freshTxs: fresh, freshTxs: fresh,
sigopTxs: sigop, sigopTxs: sigop,
acceleratedTxs: accelerated,
matchRate: matchRate, matchRate: matchRate,
}); });
@ -613,7 +621,7 @@ class WebsocketHandler {
} }
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$makeBlockTemplates(_memPool, true); await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
} else { } else {
mempoolBlocks.updateMempoolBlocks(_memPool, true); mempoolBlocks.updateMempoolBlocks(_memPool, true);
} }

View File

@ -129,6 +129,10 @@ interface IConfig {
GEOLITE2_ASN: string; GEOLITE2_ASN: string;
GEOIP2_ISP: string; GEOIP2_ISP: string;
}, },
MEMPOOL_SERVICES: {
API: string;
ACCELERATIONS: boolean;
},
} }
const defaults: IConfig = { const defaults: IConfig = {
@ -258,6 +262,10 @@ 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'
}, },
'MEMPOOL_SERVICES': {
'API': '',
'ACCELERATIONS': false,
}
}; };
class Config implements IConfig { class Config implements IConfig {
@ -277,6 +285,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'];
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
constructor() { constructor() {
const configs = this.merge(configFromFile, defaults); const configs = this.merge(configFromFile, defaults);
@ -296,6 +305,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.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View File

@ -34,6 +34,7 @@ export interface BlockAudit {
freshTxs: string[], freshTxs: string[],
sigopTxs: string[], sigopTxs: string[],
addedTxs: string[], addedTxs: string[],
acceleratedTxs: string[],
matchRate: number, matchRate: number,
} }
@ -85,6 +86,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
block: number, block: number,
vsize: number, vsize: number,
}; };
acceleration?: number;
uid?: number; uid?: number;
} }
@ -173,6 +175,7 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
acc?: number;
rate?: number; // effective fee rate rate?: number; // effective fee rate
} }

View File

@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
class BlocksAuditRepositories { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { public async $saveAudit(audit: BlockAudit): Promise<void> {
try { try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, match_rate) await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, accelerated_txs, match_rate)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), audit.matchRate]); JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate]);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`); logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
@ -51,7 +51,7 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size, `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
blocks.weight, blocks.tx_count, blocks.weight, blocks.tx_count,
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, sigop_txs as sigopTxs, match_rate as matchRate transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, sigop_txs as sigopTxs, accelerated_txs as acceleratedTxs, match_rate as matchRate
FROM blocks_audits FROM blocks_audits
JOIN blocks ON blocks.hash = blocks_audits.hash JOIN blocks ON blocks.hash = blocks_audits.hash
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
@ -64,6 +64,7 @@ class BlocksAuditRepositories {
rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].freshTxs = JSON.parse(rows[0].freshTxs); rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
rows[0].transactions = JSON.parse(rows[0].transactions); rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template); rows[0].template = JSON.parse(rows[0].template);

View File

@ -124,5 +124,9 @@
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__", "GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__", "GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__" "GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
},
"MEMPOOL_SERVICES": {
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
} }
} }

View File

@ -126,6 +126,10 @@ __MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"} __MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""} __MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__==${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_ACCELERATIONS__==${MEMPOOL_SERVICES_ACCELERATIONS:=false}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@ -243,5 +247,9 @@ sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-conf
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
# MEMPOOL_SERVICES
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
node /backend/package/index.js node /backend/package/index.js

View File

@ -133,7 +133,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) { if (this.scene) {
this.scene.update(add, remove, change, direction, resetLayout); this.scene.update(add, remove, change, direction, resetLayout);
this.start(); this.start();

View File

@ -156,7 +156,7 @@ export default class BlockScene {
this.updateAll(startTime, 200, direction); this.updateAll(startTime, 200, direction);
} }
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
const startTime = performance.now(); const startTime = performance.now();
const removed = this.removeBatch(remove, startTime, direction); const removed = this.removeBatch(remove, startTime, direction);
@ -181,6 +181,7 @@ export default class BlockScene {
// update effective rates // update effective rates
change.forEach(tx => { change.forEach(tx => {
if (this.txs[tx.txid]) { if (this.txs[tx.txid]) {
this.txs[tx.txid].acc = tx.acc;
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize); this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
this.txs[tx.txid].rate = tx.rate; this.txs[tx.txid].rate = tx.rate;
this.txs[tx.txid].dirty = true; this.txs[tx.txid].dirty = true;

View File

@ -16,6 +16,7 @@ const auditColors = {
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'), added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8F5FF6'),
}; };
// convert from this class's update format to TxSprite's update format // convert from this class's update format to TxSprite's update format
@ -36,8 +37,9 @@ export default class TxView implements TransactionStripped {
vsize: number; vsize: number;
value: number; value: number;
feerate: number; feerate: number;
acc?: number;
rate?: number; rate?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;
@ -60,6 +62,7 @@ export default class TxView implements TransactionStripped {
this.vsize = tx.vsize; this.vsize = tx.vsize;
this.value = tx.value; this.value = tx.value;
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
this.acc = tx.acc;
this.rate = tx.rate; this.rate = tx.rate;
this.status = tx.status; this.status = tx.status;
this.initialised = false; this.initialised = false;
@ -164,6 +167,11 @@ export default class TxView implements TransactionStripped {
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Normal mode // Normal mode
if (!this.scene?.highlightingEnabled) { if (!this.scene?.highlightingEnabled) {
if (this.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor;
}
return feeLevelColor; return feeLevelColor;
} }
// Block audit // Block audit
@ -179,6 +187,8 @@ export default class TxView implements TransactionStripped {
return auditColors.added; return auditColors.added;
case 'selected': case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'accelerated':
return auditColors.accelerated;
case 'found': case 'found':
if (this.context === 'projected') { if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
@ -186,9 +196,13 @@ export default class TxView implements TransactionStripped {
return feeLevelColor; return feeLevelColor;
} }
default: default:
if (this.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor; return feeLevelColor;
} }
} }
}
} }
function hexToColor(hex: string): Color { function hexToColor(hex: string): Color {

View File

@ -29,7 +29,8 @@
</td> </td>
</tr> </tr>
<tr *ngIf="effectiveRate && effectiveRate !== feeRate"> <tr *ngIf="effectiveRate && effectiveRate !== feeRate">
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td> <td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
<td> <td>
{{ effectiveRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> {{ effectiveRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</td> </td>
@ -48,6 +49,7 @@
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td> <td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td> <td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td> <td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>

View File

@ -21,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
vsize = 1; vsize = 1;
feeRate = 0; feeRate = 0;
effectiveRate; effectiveRate;
acceleration;
tooltipPosition: Position = { x: 0, y: 0 }; tooltipPosition: Position = { x: 0, y: 0 };
@ -53,6 +54,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.vsize = tx.vsize || 1; this.vsize = tx.vsize || 1;
this.feeRate = this.fee / this.vsize; this.feeRate = this.fee / this.vsize;
this.effectiveRate = tx.rate; this.effectiveRate = tx.rate;
this.acceleration = tx.acc;
} }
} }
} }

View File

@ -336,6 +336,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isSelected = {}; const isSelected = {};
const isFresh = {}; const isFresh = {};
const isSigop = {}; const isSigop = {};
const isAccelerated = {};
this.numMissing = 0; this.numMissing = 0;
this.numUnexpected = 0; this.numUnexpected = 0;
@ -358,6 +359,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.sigopTxs || []) { for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true; isSigop[txid] = true;
} }
for (const txid of blockAudit.acceleratedTxs || []) {
isAccelerated[txid] = true;
}
// set transaction statuses // set transaction statuses
for (const tx of blockAudit.template) { for (const tx of blockAudit.template) {
tx.context = 'projected'; tx.context = 'projected';
@ -370,6 +374,9 @@ export class BlockComponent implements OnInit, OnDestroy {
isMissing[tx.txid] = true; isMissing[tx.txid] = true;
this.numMissing++; this.numMissing++;
} }
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
} }
for (const [index, tx] of blockAudit.transactions.entries()) { for (const [index, tx] of blockAudit.transactions.entries()) {
tx.context = 'actual'; tx.context = 'actual';
@ -384,6 +391,9 @@ export class BlockComponent implements OnInit, OnDestroy {
isSelected[tx.txid] = true; isSelected[tx.txid] = true;
this.numUnexpected++; this.numUnexpected++;
} }
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
} }
for (const tx of blockAudit.transactions) { for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true; inBlock[tx.txid] = true;

View File

@ -95,7 +95,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
updateBlock(delta: MempoolBlockDelta): void { updateBlock(delta: MempoolBlockDelta): void {
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight); const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) { if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
this.blockGraph.replace(delta.added, direction); this.blockGraph.replace(delta.added, direction);

View File

@ -47,7 +47,7 @@
</div> </div>
</ng-template> </ng-template>
</div> </div>
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }"></div> <div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
</div> </div>
</ng-container> </ng-container>

View File

@ -170,3 +170,33 @@
border-radius: 2px; border-radius: 2px;
z-index: -1; z-index: -1;
} }
.blink{
width:400px;
height:400px;
border-bottom: 35px solid #FFF;
animation: blink 0.2s infinite;
}
@keyframes blink{
0% {
border-bottom: 35px solid green;
}
50% {
border-bottom: 35px solid yellow;
}
100% {
border-bottom: 35px solid orange;
}
}
@-webkit-keyframes blink{
0% {
border-bottom: 35px solid green;
}
50% {
border-bottom: 35px solid yellow;
}
100% {
border-bottom: 35px solid orange;
}
}

View File

@ -26,6 +26,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() minimal: boolean = false; @Input() minimal: boolean = false;
@Input() blockWidth: number = 125; @Input() blockWidth: number = 125;
@Input() containerWidth: number = null;
@Input() count: number = null; @Input() count: number = null;
@Input() spotlight: number = 0; @Input() spotlight: number = 0;
@ -252,7 +253,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2; const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
while (blocks.length < blocksAmount) { while (blocks.length < blocksAmount) {
blocks.push({ blocks.push({
@ -272,7 +273,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2; const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
let blocksAmount; let blocksAmount;
if (this.count) { if (this.count) {
blocksAmount = 8; blocksAmount = 8;

View File

@ -478,7 +478,8 @@
</td> </td>
</tr> </tr>
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate"> <tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td> <td *ngIf="cpfpInfo.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
<td *ngIf="!cpfpInfo.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td> <td>
<div class="effective-fee-container"> <div class="effective-fee-container">
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> {{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>

View File

@ -178,6 +178,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} else { } else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
} }
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
}
this.cpfpInfo = cpfpInfo; this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01)); this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));

View File

@ -19,6 +19,7 @@ export interface Transaction {
ancestors?: Ancestor[]; ancestors?: Ancestor[];
bestDescendant?: BestDescendant | null; bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean; cpfpChecked?: boolean;
acceleration?: number;
deleteAfter?: number; deleteAfter?: number;
_unblinded?: any; _unblinded?: any;
_deduced?: boolean; _deduced?: boolean;

View File

@ -27,6 +27,7 @@ export interface CpfpInfo {
effectiveFeePerVsize?: number; effectiveFeePerVsize?: number;
sigops?: number; sigops?: number;
adjustedVsize?: number; adjustedVsize?: number;
acceleration?: number;
} }
export interface RbfInfo { export interface RbfInfo {
@ -158,7 +159,7 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
} }
interface RbfTransaction extends TransactionStripped { interface RbfTransaction extends TransactionStripped {
@ -168,6 +169,7 @@ interface RbfTransaction extends TransactionStripped {
export interface MempoolPosition { export interface MempoolPosition {
block: number, block: number,
vsize: number, vsize: number,
accelerated?: boolean
} }
export interface RewardStats { export interface RewardStats {

View File

@ -57,7 +57,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionStripped[], added: TransactionStripped[],
removed: string[], removed: string[],
changed?: { txid: string, rate: number | undefined }[]; changed?: { txid: string, rate: number | undefined, acc: number | undefined }[];
} }
export interface MempoolInfo { export interface MempoolInfo {
@ -75,8 +75,9 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
acc?: number; // acceleration delta
rate?: number; // effective fee rate rate?: number; // effective fee rate
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }