diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index f7aecfca8..a909fc2b6 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -15,7 +15,7 @@ class Audit { 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 or lastBoosted within PROPAGATION_MARGIN - const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement + const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const isCensored = {}; // missing, without excuse const isDisplaced = {}; let displacedWeight = 0; @@ -36,8 +36,9 @@ class Audit { // 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]) { - if (rbfCache.isFullRbf(txid)) { - fullrbf.push(txid); + // allow missing transactions which either belong to a full rbf tree, or conflict with any transaction in the mined block + if (rbfCache.has(txid) && (rbfCache.isFullRbf(txid) || rbfCache.anyInSameTree(txid, (tx) => inBlock[tx.txid]))) { + rbf.push(txid); } else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) { // tx is recent, may have reached the miner too late for inclusion fresh.push(txid); @@ -98,8 +99,8 @@ class Audit { if (inTemplate[tx.txid]) { matches.push(tx.txid); } else { - if (rbfCache.isFullRbf(tx.txid)) { - fullrbf.push(tx.txid); + if (rbfCache.has(tx.txid)) { + rbf.push(tx.txid); } else if (!isDisplaced[tx.txid]) { added.push(tx.txid); } @@ -147,7 +148,7 @@ class Audit { added, fresh, sigop: [], - fullrbf, + fullrbf: rbf, score, similarity, }; diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 367ba1c0e..f28dd0de3 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -100,6 +100,24 @@ class RbfCache { this.dirtyTrees.add(treeId); } + public has(txId: string): boolean { + return this.txs.has(txId); + } + + public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean { + const tree = this.getRbfTree(txId); + if (!tree) { + return false; + } + const txs = this.getTransactionsInTree(tree); + for (const tx of txs) { + if (predicate(tx)) { + return true; + } + } + return false; + } + public getReplacedBy(txId: string): string | undefined { return this.replacedBy.get(txId); } diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index 452bb38f5..1b8c88704 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped { value: number; feerate: number; rate?: number; - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; context?: 'projected' | 'actual'; scene?: BlockScene; @@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped { return auditColors.censored; case 'missing': case 'sigop': - case 'fullrbf': + case 'rbf': return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; case 'fresh': case 'freshcpfp': diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html index 59450326b..c62779b69 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html @@ -53,7 +53,7 @@