Improve stability of mempool tx position arrow

This commit is contained in:
Mononaut 2023-04-21 08:40:21 +09:00
parent a5b764fb66
commit 3b8bcc4da5
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
9 changed files with 128 additions and 33 deletions

View File

@ -252,9 +252,17 @@ class MempoolBlocks {
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
// update this thread's mempool with the results
blocks.forEach(block => {
blocks.forEach((block, blockIndex) => {
let runningVsize = 0;
block.forEach(tx => {
if (tx.txid && tx.txid in mempool) {
// save position in projected blocks
mempool[tx.txid].position = {
block: blockIndex,
vsize: runningVsize + (mempool[tx.txid].vsize / 2),
};
runningVsize += mempool[tx.txid].vsize;
if (tx.effectiveFeePerVsize != null) {
mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
}

View File

@ -2,7 +2,6 @@ import config from '../config';
import logger from '../logger';
import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
import { PairingHeap } from '../utils/pairing-heap';
import { Common } from './common';
import { parentPort } from 'worker_threads';
let mempool: { [txid: string]: ThreadTransaction } = {};
@ -72,7 +71,14 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
}
// Sort by descending ancestor score
mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
mempoolArray.sort((a, b) => {
if (b.score === a.score) {
// tie-break by lexicographic txid order for stability
return a.txid < b.txid ? -1 : 1;
} else {
return (b.score || 0) - (a.score || 0);
}
});
// Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score)
@ -80,7 +86,14 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
let blockWeight = 4000;
let blockSize = 0;
let transactions: AuditTransaction[] = [];
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
if (a.score === b.score) {
// tie-break by lexicographic txid order for stability
return a.txid > b.txid;
} else {
return (a.score || 0) > (b.score || 0);
}
});
let overflow: AuditTransaction[] = [];
let failures = 0;
let top = 0;

View File

@ -66,9 +66,10 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-tx']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx'];
const trackTxid = client['track-tx'];
// Client is telling the transaction wasn't found
if (parsedMessage['watch-mempool']) {
const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']);
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid);
if (rbfCacheTxid) {
response['txReplaced'] = {
txid: rbfCacheTxid,
@ -76,7 +77,7 @@ class WebsocketHandler {
client['track-tx'] = null;
} else {
// It might have appeared before we had the time to start watching for it
const tx = memPool.getMempool()[client['track-tx']];
const tx = memPool.getMempool()[trackTxid];
if (tx) {
if (config.MEMPOOL.BACKEND === 'esplora') {
response['tx'] = tx;
@ -100,6 +101,13 @@ class WebsocketHandler {
}
}
}
const tx = memPool.getMempool()[trackTxid];
if (tx && tx.position) {
response['txPosition'] = {
txid: trackTxid,
position: tx.position,
};
}
} else {
client['track-tx'] = null;
}
@ -401,9 +409,10 @@ class WebsocketHandler {
}
if (client['track-tx']) {
const trackTxid = client['track-tx'];
const outspends: object = {};
newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
if (vin.txid === client['track-tx']) {
if (vin.txid === trackTxid) {
outspends[vin.vout] = {
vin: i,
txid: tx.txid,
@ -426,6 +435,14 @@ class WebsocketHandler {
if (rbfChange) {
response['rbfInfo'] = rbfChanges.trees[rbfChange];
}
const mempoolTx = newMempool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = {
txid: trackTxid,
position: mempoolTx.position,
};
}
}
if (client['track-mempool-block'] >= 0) {
@ -556,8 +573,19 @@ class WebsocketHandler {
response['mempool-blocks'] = mBlocks;
}
if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
if (client['track-tx']) {
const trackTxid = client['track-tx'];
if (txIds.indexOf(trackTxid) > -1) {
response['txConfirmed'] = true;
} else {
const mempoolTx = _memPool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = {
txid: trackTxid,
position: mempoolTx.position,
};
}
}
}
if (client['track-address']) {

View File

@ -80,6 +80,10 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
deleteAfter?: number;
position?: {
block: number,
vsize: number,
};
}
export interface AuditTransaction {

View File

@ -8,7 +8,7 @@ import { feeLevels, mempoolFeeColors } from '../../app.constants';
import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Location } from '@angular/common';
import { DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface';
import { animate, style, transition, trigger } from '@angular/animations';
@Component({
@ -58,6 +58,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
transition = 'background 2s, right 2s, transform 1s';
markIndex: number;
txPosition: MempoolPosition;
txFeePerVSize: number;
resetTransitionTimeout: number;
@ -156,6 +157,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
if (state.mempoolBlockIndex !== undefined) {
this.markIndex = state.mempoolBlockIndex;
}
if (state.mempoolPosition) {
this.txPosition = state.mempoolPosition;
}
if (state.txFeePerVSize) {
this.txFeePerVSize = state.txFeePerVSize;
}
@ -302,7 +306,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
}
calculateTransactionPosition() {
if ((!this.txFeePerVSize && (this.markIndex === undefined || this.markIndex === -1)) || !this.mempoolBlocks) {
if ((!this.txPosition && !this.txFeePerVSize && (this.markIndex === undefined || this.markIndex === -1)) || !this.mempoolBlocks) {
this.arrowVisible = false;
return;
} else if (this.markIndex > -1) {
@ -320,6 +324,15 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
this.arrowVisible = true;
if (this.txPosition) {
if (this.txPosition.block >= this.mempoolBlocks.length) {
this.rightPosition = ((this.mempoolBlocks.length - 1) * (this.blockWidth + this.blockPadding)) + this.blockWidth;
} else {
const positionInBlock = Math.min(1, this.txPosition.vsize / this.stateService.blockVSize) * this.blockWidth;
const positionOfBlock = this.txPosition.block * (this.blockWidth + this.blockPadding);
this.rightPosition = positionOfBlock + positionInBlock;
}
} else {
let found = false;
for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) {
const block = this.mempoolBlocks[txInBlockIndex];
@ -349,6 +362,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
}
}
}
}
mountEmptyBlocks() {
const emptyBlocks = [];

View File

@ -19,7 +19,7 @@ import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, CpfpInfo, RbfTree } from '../../interfaces/node-api.interface';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Price, PriceService } from '../../services/price.service';
@ -35,6 +35,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
tx: Transaction;
txId: string;
txInBlockIndex: number;
mempoolPosition: MempoolPosition;
isLoadingTx = true;
error: any = undefined;
errorUnblinded: any = undefined;
@ -47,6 +48,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchCachedTxSubscription: Subscription;
txReplacedSubscription: Subscription;
txRbfInfoSubscription: Subscription;
mempoolPositionSubscription: Subscription;
blocksSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
@ -174,6 +176,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (!this.tx?.status?.confirmed) {
this.stateService.markBlock$.next({
txFeePerVSize: this.tx.effectiveFeePerVsize,
mempoolPosition: this.mempoolPosition,
});
}
this.cpfpInfo = cpfpInfo;
@ -231,6 +234,19 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
});
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
this.mempoolPosition = txPosition.position;
if (this.tx && !this.tx.status.confirmed) {
this.stateService.markBlock$.next({
mempoolPosition: this.mempoolPosition
});
}
} else {
this.mempoolPosition = null;
}
});
this.subscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
@ -342,6 +358,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (tx.cpfpChecked) {
this.stateService.markBlock$.next({
txFeePerVSize: tx.effectiveFeePerVsize,
mempoolPosition: this.mempoolPosition,
});
this.cpfpInfo = {
ancestors: tx.ancestors,
@ -569,6 +586,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.flowPrefSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.mempoolPositionSubscription.unsubscribe();
this.leaveTransaction();
}
}

View File

@ -162,6 +162,10 @@ interface RbfTransaction extends TransactionStripped {
rbf?: boolean;
mined?: boolean,
}
export interface MempoolPosition {
block: number,
vsize: number,
}
export interface RewardStats {
startBlock: number;

View File

@ -2,7 +2,7 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { map, shareReplay } from 'rxjs/operators';
@ -12,6 +12,7 @@ interface MarkBlockState {
blockHeight?: number;
mempoolBlockIndex?: number;
txFeePerVSize?: number;
mempoolPosition?: MempoolPosition;
}
export interface ILoadingIndicators { [name: string]: number; }
@ -105,6 +106,7 @@ export class StateService {
utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
mempoolTransactions$ = new Subject<Transaction>();
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>();
blockTransactions$ = new Subject<Transaction>();
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
vbytesPerSecond$ = new ReplaySubject<number>(1);

View File

@ -249,6 +249,10 @@ export class WebsocketService {
this.stateService.mempoolTransactions$.next(response.tx);
}
if (response['txPosition']) {
this.stateService.mempoolTxPosition$.next(response['txPosition']);
}
if (response.block) {
if (response.block.height > this.stateService.latestBlockHeight) {
this.stateService.updateChainTip(response.block.height);