Merge pull request #3838 from mempool/mononaut/dependent-rate-indexing

calculate & index ancestor-dependent effective rates
This commit is contained in:
softsimon 2023-06-14 23:02:25 +02:00 committed by GitHub
commit e8420853e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 21 deletions

View file

@ -1,4 +1,4 @@
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces'; import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats } from '../mempool.interfaces';
import config from '../config'; import config from '../config';
import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net'; import { isIP } from 'net';
@ -374,40 +374,80 @@ export class Common {
} }
static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary { static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary {
const clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[] = []; const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
let cluster: TransactionExtended[] = []; const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
let ancestors: { [txid: string]: boolean } = {}; let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
const txMap = {}; const txMap = {};
// initialize the txMap
for (const tx of transactions) {
txMap[tx.txid] = tx;
}
// reverse pass to identify CPFP clusters
for (let i = transactions.length - 1; i >= 0; i--) { for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i]; const tx = transactions[i];
txMap[tx.txid] = tx;
if (!ancestors[tx.txid]) { if (!ancestors[tx.txid]) {
let totalFee = 0; let totalFee = 0;
let totalVSize = 0; let totalVSize = 0;
cluster.forEach(tx => { clusterTxs.forEach(tx => {
totalFee += tx?.fee || 0; totalFee += tx?.fee || 0;
totalVSize += (tx.weight / 4); totalVSize += (tx.weight / 4);
}); });
const effectiveFeePerVsize = totalFee / totalVSize; const effectiveFeePerVsize = totalFee / totalVSize;
if (cluster.length > 1) { let cluster: CpfpCluster;
clusters.push({ if (clusterTxs.length > 1) {
root: cluster[0].txid, cluster = {
root: clusterTxs[0].txid,
height, height,
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
effectiveFeePerVsize, effectiveFeePerVsize,
}); };
clusters.push(cluster);
} }
cluster.forEach(tx => { clusterTxs.forEach(tx => {
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
if (cluster) {
clusterMap[tx.txid] = cluster;
}
}); });
cluster = []; // reset working vars
clusterTxs = [];
ancestors = {}; ancestors = {};
} }
cluster.push(tx); clusterTxs.push(tx);
tx.vin.forEach(vin => { tx.vin.forEach(vin => {
ancestors[vin.txid] = true; ancestors[vin.txid] = true;
}); });
} }
// forward pass to enforce ancestor rate caps
for (const tx of transactions) {
let minAncestorRate = tx.effectiveFeePerVsize;
for (const vin of tx.vin) {
if (txMap[vin.txid]?.effectiveFeePerVsize) {
minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize);
}
}
// check rounded values to skip cases with almost identical fees
const roundedMinAncestorRate = Math.ceil(minAncestorRate);
const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize);
if (roundedMinAncestorRate < roundedEffectiveFeeRate) {
tx.effectiveFeePerVsize = minAncestorRate;
if (!clusterMap[tx.txid]) {
// add a single-tx cluster to record the dependent rate
const cluster = {
root: tx.txid,
height,
txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }],
effectiveFeePerVsize: minAncestorRate,
};
clusterMap[tx.txid] = cluster;
clusters.push(cluster);
} else {
// update the existing cluster with the dependent rate
clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate;
}
}
}
return { return {
transactions, transactions,
clusters, clusters,

View file

@ -253,9 +253,16 @@ export interface WorkingEffectiveFeeStats extends EffectiveFeeStats {
maxFee: number; maxFee: number;
} }
export interface CpfpCluster {
root: string,
height: number,
txs: Ancestor[],
effectiveFeePerVsize: number,
}
export interface CpfpSummary { export interface CpfpSummary {
transactions: TransactionExtended[]; transactions: TransactionExtended[];
clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]; clusters: CpfpCluster[];
} }
export interface Statistic { export interface Statistic {

View file

@ -1,8 +1,7 @@
import cluster, { Cluster } from 'cluster';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { Ancestor } from '../mempool.interfaces'; import { Ancestor, CpfpCluster } from '../mempool.interfaces';
import transactionRepository from '../repositories/TransactionRepository'; import transactionRepository from '../repositories/TransactionRepository';
class CpfpRepository { class CpfpRepository {
@ -12,7 +11,7 @@ class CpfpRepository {
} }
// skip clusters of transactions with the same fees // skip clusters of transactions with the same fees
const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100; const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100;
const equalFee = txs.reduce((acc, tx) => { const equalFee = txs.length > 1 && txs.reduce((acc, tx) => {
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
}, true); }, true);
if (equalFee) { if (equalFee) {
@ -54,9 +53,9 @@ class CpfpRepository {
const txs: any[] = []; const txs: any[] = [];
for (const cluster of clusters) { for (const cluster of clusters) {
if (cluster.txs?.length > 1) { if (cluster.txs?.length) {
const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100; const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100;
const equalFee = cluster.txs.reduce((acc, tx) => { const equalFee = cluster.txs.length > 1 && cluster.txs.reduce((acc, tx) => {
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
}, true); }, true);
if (!equalFee) { if (!equalFee) {
@ -111,7 +110,7 @@ class CpfpRepository {
} }
} }
public async $getCluster(clusterRoot: string): Promise<Cluster | void> { public async $getCluster(clusterRoot: string): Promise<CpfpCluster | void> {
const [clusterRows]: any = await DB.query( const [clusterRows]: any = await DB.query(
` `
SELECT * SELECT *
@ -121,6 +120,7 @@ class CpfpRepository {
[clusterRoot] [clusterRoot]
); );
const cluster = clusterRows[0]; const cluster = clusterRows[0];
cluster.effectiveFeePerVsize = cluster.fee_rate;
if (cluster?.txs) { if (cluster?.txs) {
cluster.txs = this.unpack(cluster.txs); cluster.txs = this.unpack(cluster.txs);
return cluster; return cluster;

View file

@ -105,6 +105,7 @@ class TransactionRepository {
return { return {
descendants, descendants,
ancestors, ancestors,
effectiveFeePerVsize: cluster.effectiveFeePerVsize,
}; };
} }
} }