mirror of
https://github.com/mempool/mempool.git
synced 2025-04-22 14:34:47 +02:00
improve audit analysis and scoring
This commit is contained in:
parent
39afa4cda1
commit
832ccdac46
114
backend/src/api/audit.ts
Normal file
114
backend/src/api/audit.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
class Audit {
|
||||
auditBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[],
|
||||
projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended },
|
||||
): { censored: string[], added: string[], score: number } {
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
let displacedWeight = 0;
|
||||
|
||||
const inBlock = {};
|
||||
const inTemplate = {};
|
||||
|
||||
const now = Math.round((Date.now() / 1000));
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = tx;
|
||||
}
|
||||
// coinbase is always expected
|
||||
if (transactions[0]) {
|
||||
inTemplate[transactions[0].txid] = true;
|
||||
}
|
||||
// look for transactions that were expected in the template, but missing from the mined block
|
||||
for (const txid of projectedBlocks[0].transactionIds) {
|
||||
if (!inBlock[txid]) {
|
||||
// tx is recent, may have reached the miner too late for inclusion
|
||||
if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
||||
fresh.push(txid);
|
||||
} else {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
displacedWeight += mempool[txid].weight;
|
||||
}
|
||||
inTemplate[txid] = true;
|
||||
}
|
||||
|
||||
displacedWeight += (4000 - transactions[0].weight);
|
||||
|
||||
logger.warn(`${fresh.length} fresh, ${Object.keys(isCensored).length} possibly censored, ${displacedWeight} displaced weight`);
|
||||
|
||||
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
||||
// these displaced transactions should occupy the first N weight units of the next projected block
|
||||
let displacedWeightRemaining = displacedWeight;
|
||||
let index = 0;
|
||||
let lastFeeRate = Infinity;
|
||||
let failures = 0;
|
||||
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) {
|
||||
const txid = projectedBlocks[1].transactionIds[index];
|
||||
const fits = (mempool[txid].weight - displacedWeightRemaining) < 4000;
|
||||
const feeMatches = mempool[txid].effectiveFeePerVsize >= lastFeeRate;
|
||||
if (fits || feeMatches) {
|
||||
isDisplaced[txid] = true;
|
||||
if (fits) {
|
||||
lastFeeRate = Math.min(lastFeeRate, mempool[txid].effectiveFeePerVsize);
|
||||
}
|
||||
if (mempool[txid].firstSeen == null || (now - (mempool[txid]?.firstSeen || 0)) > PROPAGATION_MARGIN) {
|
||||
displacedWeightRemaining -= mempool[txid].weight;
|
||||
}
|
||||
failures = 0;
|
||||
} else {
|
||||
failures++;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
// mark unexpected transactions in the mined block as 'added'
|
||||
let overflowWeight = 0;
|
||||
for (const tx of transactions) {
|
||||
if (inTemplate[tx.txid]) {
|
||||
matches.push(tx.txid);
|
||||
} else {
|
||||
if (!isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
}
|
||||
overflowWeight += tx.weight;
|
||||
}
|
||||
}
|
||||
|
||||
// transactions missing from near the end of our template are probably not being censored
|
||||
let overflowWeightRemaining = overflowWeight;
|
||||
let lastOverflowRate = 1.00;
|
||||
index = projectedBlocks[0].transactionIds.length - 1;
|
||||
while (index >= 0) {
|
||||
const txid = projectedBlocks[0].transactionIds[index];
|
||||
if (overflowWeightRemaining > 0) {
|
||||
if (isCensored[txid]) {
|
||||
delete isCensored[txid];
|
||||
}
|
||||
lastOverflowRate = mempool[txid].effectiveFeePerVsize;
|
||||
} else if (Math.floor(mempool[txid].effectiveFeePerVsize * 100) <= Math.ceil(lastOverflowRate * 100)) { // tolerance of 0.01 sat/vb
|
||||
if (isCensored[txid]) {
|
||||
delete isCensored[txid];
|
||||
}
|
||||
}
|
||||
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
|
||||
index--;
|
||||
}
|
||||
|
||||
const numCensored = Object.keys(isCensored).length;
|
||||
const score = matches.length > 0 ? (matches.length / (matches.length + numCensored)) : 0;
|
||||
|
||||
return {
|
||||
censored: Object.keys(isCensored),
|
||||
added,
|
||||
score
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new Audit();
|
@ -18,6 +18,7 @@ import difficultyAdjustment from './difficulty-adjustment';
|
||||
import feeApi from './fee-api';
|
||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||
import Audit from './audit';
|
||||
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@ -405,7 +406,7 @@ class WebsocketHandler {
|
||||
});
|
||||
}
|
||||
|
||||
handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) {
|
||||
handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]): void {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
@ -414,30 +415,19 @@ class WebsocketHandler {
|
||||
let mBlockDeltas: undefined | MempoolBlockDelta[];
|
||||
let matchRate = 0;
|
||||
const _memPool = memPool.getMempool();
|
||||
const projectedBlocks = mempoolBlocks.makeBlockTemplates(cloneMempool(_memPool), 1, 1);
|
||||
const mempoolCopy = cloneMempool(_memPool);
|
||||
|
||||
const projectedBlocks = mempoolBlocks.makeBlockTemplates(mempoolCopy, 2, 2);
|
||||
|
||||
if (projectedBlocks[0]) {
|
||||
const matches: string[] = [];
|
||||
const added: string[] = [];
|
||||
const missing: string[] = [];
|
||||
const { censored, added, score } = Audit.auditBlock(block, txIds, transactions, projectedBlocks, mempoolCopy);
|
||||
matchRate = Math.round(score * 100 * 100) / 100;
|
||||
|
||||
// Update mempool to remove transactions included in the new block
|
||||
for (const txId of txIds) {
|
||||
if (projectedBlocks[0].transactionIds.indexOf(txId) > -1) {
|
||||
matches.push(txId);
|
||||
} else {
|
||||
added.push(txId);
|
||||
}
|
||||
delete _memPool[txId];
|
||||
}
|
||||
|
||||
for (const txId of projectedBlocks[0].transactionIds) {
|
||||
if (matches.includes(txId) || added.includes(txId)) {
|
||||
continue;
|
||||
}
|
||||
missing.push(txId);
|
||||
}
|
||||
|
||||
matchRate = Math.round((Math.max(0, matches.length - missing.length - added.length) / txIds.length * 100) * 100) / 100;
|
||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
||||
mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
@ -464,7 +454,7 @@ class WebsocketHandler {
|
||||
height: block.height,
|
||||
hash: block.id,
|
||||
addedTxs: added,
|
||||
missingTxs: missing,
|
||||
missingTxs: censored,
|
||||
matchRate: matchRate,
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user