mempool/backend/src/api/rbf-cache.ts

224 lines
6.8 KiB
TypeScript
Raw Normal View History

2022-12-17 09:39:06 -06:00
import { runInNewContext } from "vm";
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
import { Common } from "./common";
interface RbfTransaction extends TransactionStripped {
rbf?: boolean;
2022-12-17 09:39:06 -06:00
mined?: boolean;
}
2022-12-17 09:39:06 -06:00
interface RbfTree {
tx: RbfTransaction;
time: number;
interval?: number;
mined?: boolean;
fullRbf: boolean;
replaces: RbfTree[];
}
2022-12-14 08:49:35 -06:00
2022-03-08 14:49:25 +01:00
class RbfCache {
2022-12-14 08:49:35 -06:00
private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map();
2022-12-17 09:39:06 -06:00
private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements
private dirtyTrees: Set<string> = new Set();
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
2022-12-14 08:49:35 -06:00
private txs: Map<string, TransactionExtended> = new Map();
private expiring: Map<string, Date> = new Map();
2022-03-08 14:49:25 +01:00
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
}
2022-12-17 09:39:06 -06:00
public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void {
if (!newTxExtended || !replaced?.length) {
return;
}
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
2022-12-17 09:39:06 -06:00
const newTime = newTxExtended.firstSeen || Date.now();
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
2022-12-14 08:49:35 -06:00
this.txs.set(newTx.txid, newTxExtended);
2022-12-17 09:39:06 -06:00
// maintain rbf trees
let fullRbf = false;
const replacedTrees: RbfTree[] = [];
for (const replacedTxExtended of replaced) {
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
if (treeId) {
const tree = this.rbfTrees.get(treeId);
this.rbfTrees.delete(treeId);
if (tree) {
tree.interval = newTime - tree?.time;
replacedTrees.push(tree);
fullRbf = fullRbf || tree.fullRbf;
}
}
} else {
const replacedTime = replacedTxExtended.firstSeen || Date.now();
replacedTrees.push({
tx: replacedTx,
time: replacedTime,
interval: newTime - replacedTime,
fullRbf: !replacedTx.rbf,
replaces: [],
});
fullRbf = fullRbf || !replacedTx.rbf;
this.txs.set(replacedTx.txid, replacedTxExtended);
}
}
2022-12-17 09:39:06 -06:00
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTxExtended.firstSeen || Date.now(),
fullRbf,
replaces: replacedTrees
};
this.rbfTrees.set(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
this.dirtyTrees.add(treeId);
2022-03-08 14:49:25 +01:00
}
public getReplacedBy(txId: string): string | undefined {
2022-12-14 08:49:35 -06:00
return this.replacedBy.get(txId);
}
public getReplaces(txId: string): string[] | undefined {
2022-12-14 08:49:35 -06:00
return this.replaces.get(txId);
}
public getTx(txId: string): TransactionExtended | undefined {
2022-12-14 08:49:35 -06:00
return this.txs.get(txId);
2022-03-08 14:49:25 +01:00
}
2022-12-17 09:39:06 -06:00
public getRbfTree(txId: string): RbfTree | void {
return this.rbfTrees.get(this.treeMap.get(txId) || '');
}
2022-12-17 09:39:06 -06:00
// get a paginated list of RbfTrees
2022-12-14 16:51:53 -06:00
// ordered by most recent replacement time
2022-12-17 09:39:06 -06:00
public getRbfTrees(onlyFullRbf: boolean, after?: string): RbfTree[] {
2022-12-14 16:51:53 -06:00
const limit = 25;
2022-12-17 09:39:06 -06:00
const trees: RbfTree[] = [];
2022-12-14 16:51:53 -06:00
const used = new Set<string>();
const replacements: string[][] = Array.from(this.replacedBy).reverse();
2022-12-17 09:39:06 -06:00
const afterTree = after ? this.treeMap.get(after) : null;
let ready = !afterTree;
for (let i = 0; i < replacements.length && trees.length <= limit - 1; i++) {
2022-12-14 16:51:53 -06:00
const txid = replacements[i][1];
2022-12-17 09:39:06 -06:00
const treeId = this.treeMap.get(txid) || '';
if (treeId === afterTree) {
2022-12-14 16:51:53 -06:00
ready = true;
} else if (ready) {
2022-12-17 09:39:06 -06:00
if (!used.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
used.add(treeId);
if (tree && (!onlyFullRbf || tree.fullRbf)) {
trees.push(tree);
2022-12-14 16:51:53 -06:00
}
}
}
}
2022-12-17 09:39:06 -06:00
return trees;
2022-12-14 16:51:53 -06:00
}
2022-12-17 09:39:06 -06:00
// get map of rbf trees that have been updated since the last call
public getRbfChanges(): { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} {
const changes: { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} = {
trees: {},
2022-12-14 08:49:35 -06:00
map: {},
};
2022-12-17 09:39:06 -06:00
this.dirtyTrees.forEach(id => {
const tree = this.rbfTrees.get(id);
if (tree) {
changes.trees[id] = tree;
this.getTransactionsInTree(tree).forEach(tx => {
changes.map[tx.txid] = id;
2022-12-14 08:49:35 -06:00
});
}
});
2022-12-17 09:39:06 -06:00
this.dirtyTrees = new Set();
2022-12-14 08:49:35 -06:00
return changes;
}
2022-12-14 16:51:53 -06:00
public mined(txid): void {
2022-12-17 09:39:06 -06:00
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
2022-12-14 16:51:53 -06:00
}
}
this.evict(txid);
}
2022-12-14 08:49:35 -06:00
// flag a transaction as removed from the mempool
public evict(txid): void {
2022-12-14 08:49:35 -06:00
this.expiring.set(txid, new Date(Date.now() + 1000 * 86400)); // 24 hours
}
2022-03-08 14:49:25 +01:00
private cleanup(): void {
const currentDate = new Date();
for (const txid in this.expiring) {
2022-12-14 08:49:35 -06:00
if ((this.expiring.get(txid) || 0) < currentDate) {
this.expiring.delete(txid);
this.remove(txid);
}
}
}
// remove a transaction & all previous versions from the cache
private remove(txid): void {
// don't remove a transaction if a newer version remains in the mempool
2022-12-14 08:49:35 -06:00
if (!this.replacedBy.has(txid)) {
const replaces = this.replaces.get(txid);
this.replaces.delete(txid);
2022-12-17 09:39:06 -06:00
this.treeMap.delete(txid);
2022-12-14 08:49:35 -06:00
this.txs.delete(txid);
this.expiring.delete(txid);
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache
2022-12-14 08:49:35 -06:00
this.replacedBy.delete(tx);
2022-12-17 09:39:06 -06:00
// if this is the id of a tree, remove that too
if (this.treeMap.get(tx) === tx) {
this.rbfTrees.delete(tx);
}
this.remove(tx);
2022-03-08 14:49:25 +01:00
}
}
}
2022-12-17 09:39:06 -06:00
private updateTreeMap(newId: string, tree: RbfTree): void {
this.treeMap.set(tree.tx.txid, newId);
tree.replaces.forEach(subtree => {
this.updateTreeMap(newId, subtree);
});
}
private getTransactionsInTree(tree: RbfTree, txs: RbfTransaction[] = []): RbfTransaction[] {
txs.push(tree.tx);
tree.replaces.forEach(subtree => {
this.getTransactionsInTree(subtree, txs);
});
return txs;
}
private setTreeMined(tree: RbfTree, txid: string): void {
if (tree.tx.txid === txid) {
tree.tx.mined = true;
} else {
tree.replaces.forEach(subtree => {
this.setTreeMined(subtree, txid);
});
}
}
2022-03-08 14:49:25 +01:00
}
export default new RbfCache();