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;
|
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