Multi-pool ETA calculation
This commit is contained in:
parent
6277813414
commit
833418514e
170
frontend/src/app/services/eta.service.ts
Normal file
170
frontend/src/app/services/eta.service.ts
Normal file
@ -0,0 +1,170 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* **Define parameters**
|
||||
- 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
|
||||
**Overall expected confirmation time**
|
||||
- $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
|
||||
*/
|
||||
const pools: { [id: number]: SinglePoolStats } = {};
|
||||
for (const pool of miningStats.pools) {
|
||||
pools[pool.poolUniqueId] = pool;
|
||||
}
|
||||
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
|
||||
const positions = [unacceleratedPosition, ...accelerationPositions];
|
||||
const max = unacceleratedPosition.block; // by definition, assuming no negative fee deltas or out of band txs
|
||||
|
||||
let tailProb = 0;
|
||||
let Q = 0;
|
||||
for (let i = 0; i < max; i++) {
|
||||
// find H_i
|
||||
const H = accelerationPositions.reduce((total, pos) => total + (pos.block <= i ? pools[pos.poolId].lastEstimatedHashrate : 0), 0) / miningStats.lastEstimatedHashrate;
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -442,4 +442,18 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number {
|
||||
if (accelerated) {
|
||||
let ancestorVsize = tx.weight / 4;
|
||||
let ancestorFee = tx.fee;
|
||||
for (const ancestor of tx.ancestors || []) {
|
||||
ancestorVsize += (ancestor.weight / 4);
|
||||
ancestorFee += ancestor.fee;
|
||||
}
|
||||
return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize));
|
||||
} else {
|
||||
return tx.effectiveFeePerVsize;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user