mempool/frontend/src/app/services/eta.service.ts

184 lines
7.0 KiB
TypeScript

import { Injectable } from '@angular/core';
import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../interfaces/node-api.interface';
import { StateService } from './state.service';
import { MempoolBlock } from '../interfaces/websocket.interface';
import { Transaction } from '../interfaces/electrs.interface';
import { MiningStats } from './mining.service';
import { getUnacceleratedFeeRate } from '../shared/transaction.utils';
export interface ETA {
now: number, // time at which calculation performed
time: number, // absolute time expected (in unix epoch ms)
wait: number, // expected wait time in ms
blocks: number, // expected number of blocks (rounded up to next integer)
}
@Injectable({
providedIn: 'root'
})
export class EtaService {
constructor(
private stateService: StateService,
) { }
mempoolPositionFromFees(feerate: number, mempoolBlocks: MempoolBlock[]): MempoolPosition {
for (let txInBlockIndex = 0; txInBlockIndex < mempoolBlocks.length; txInBlockIndex++) {
const block = mempoolBlocks[txInBlockIndex];
for (let i = 0; i < block.feeRange.length - 1; i++) {
if (feerate < block.feeRange[i + 1] && feerate >= block.feeRange[i]) {
const feeRangeIndex = i;
const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
const txFee = feerate - block.feeRange[i];
const max = block.feeRange[i + 1] - block.feeRange[i];
const blockLocation = txFee / max;
const chunkPositionOffset = blockLocation * feeRangeChunkSize;
const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
const blockedFilledPercentage = (block.blockVSize > this.stateService.blockVSize ? this.stateService.blockVSize : block.blockVSize) / this.stateService.blockVSize;
return {
block: txInBlockIndex,
vsize: (1 - feePosition) * blockedFilledPercentage * this.stateService.blockVSize,
}
}
}
if (feerate >= block.feeRange[block.feeRange.length - 1]) {
// at the very front of this block
return {
block: txInBlockIndex,
vsize: 0,
}
}
}
// at the very back of the last block
return {
block: mempoolBlocks.length - 1,
vsize: mempoolBlocks[mempoolBlocks.length - 1].blockVSize,
}
}
calculateETA(
network: string,
tx: Transaction,
mempoolBlocks: MempoolBlock[],
position: { txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] },
da: DifficultyAdjustment,
miningStats: MiningStats,
isAccelerated: boolean,
accelerationPositions: AccelerationPosition[],
): ETA | null {
// return this.calculateETA(tx, this.accelerationPositions, position, mempoolBlocks, da, isAccelerated)
if (!tx || !mempoolBlocks) {
return null;
}
const now = Date.now();
// use known projected position, or fall back to feerate-based estimate
const mempoolPosition = position?.position ?? this.mempoolPositionFromFees(tx.effectiveFeePerVsize || tx.feePerVsize, mempoolBlocks);
if (!mempoolPosition) {
return null;
}
// Liquid block time is always 60 seconds
if (network === 'liquid' || network === 'liquidtestnet') {
return {
now,
time: now + (60_000 * (mempoolPosition.block + 1)),
wait: (60_000 * (mempoolPosition.block + 1)),
blocks: mempoolPosition.block + 1,
}
}
// difficulty adjustment estimate is required to know avg block time on non-Liquid networks
if (!da) {
return null;
}
if (!isAccelerated) {
const blocks = mempoolPosition.block + 1;
const wait = da.adjustedTimeAvg * (mempoolPosition.block + 1);
return {
now,
time: wait + now + da.timeOffset,
wait,
blocks,
}
} else {
// accelerated transactions
// mining stats are required for pool hashrate weightings
if (!miningStats) {
return null;
}
// acceleration positions are required
if (!accelerationPositions) {
return null;
}
const pools: { [id: number]: SinglePoolStats } = {};
for (const pool of miningStats.pools) {
pools[pool.poolUniqueId] = pool;
}
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
let totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0);
const shares = [
{
block: unacceleratedPosition.block,
hashrateShare: (1 - (totalAcceleratedHashrate / miningStats.lastEstimatedHashrate)),
},
...accelerationPositions.map(pos => ({
block: pos.block,
hashrateShare: ((pools[pos.poolId].lastEstimatedHashrate) / miningStats.lastEstimatedHashrate)
}))
];
return this.calculateETAFromShares(shares, da);
}
}
/**
*
- Let $\{C_i\}$ be the set of pools.
- $P(C_i)$ is the probability that a random block belongs to pool $C_i$.
- $N(C_i)$ is the number of blocks that need to be mined before a block by pool $C_i$ contains the given transaction.
- $H(n)$ is the proportion of hashrate for which the transaction is in mempool block ≤ $n$
- $S(n)$ is the probability of the transaction being mined in block $n$
- by definition, $S(max) = 1$ , where $max$ is the maximum depth of the transaction in any mempool, and therefore $S(n>max) = 0$
- $Q$ is the expected number of blocks before the transaction is confirmed
- $E$ is the expected time before the transaction is confirmed
- $S(i) = H(i) \times (1 - \sum_{j=0}^{i-1} S(j))$
- the probability of mining a block including the transaction at this depth, multiplied by the probability that it hasn't already been mined at an earlier depth.
- $Q = \sum_{i=0}^{max} S(i) \times (i+1)$
- number of blocks, weighted by the probability that the block includes the transaction
- $E = Q \times T$
- expected number of blocks, multiplied by the avg time per block
*/
calculateETAFromShares(shares: { block: number, hashrateShare: number }[], da: DifficultyAdjustment, now: number = Date.now()): ETA {
const max = shares.reduce((max, share) => Math.max(max, share.block), 0);
let tailProb = 0;
let Q = 0;
for (let i = 0; i < max; i++) {
// find H_i
const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0);
// find S_i
let S = H * (1 - tailProb);
// accumulate sum (S_i x i)
Q += (S * (i + 1));
// accumulate sum (S_j)
tailProb += S;
}
// at max depth, the transaction is guaranteed to be mined in the next block if it hasn't already
Q += (1-tailProb);
const eta = da.timeAvg * Q; // T x Q
return {
now,
time: eta + now + da.timeOffset,
wait: eta,
blocks: Math.ceil(eta / da.adjustedTimeAvg),
}
}
}