2023-05-12 16:31:01 -06:00
|
|
|
import config from "../config";
|
2023-05-04 22:59:34 -04:00
|
|
|
import logger from "../logger";
|
2023-05-29 15:56:29 -04:00
|
|
|
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
2023-03-05 03:02:46 -06:00
|
|
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
2023-08-05 16:08:54 +09:00
|
|
|
import { IEsploraApi } from "./bitcoin/esplora-api.interface";
|
2022-12-13 17:11:37 -06:00
|
|
|
import { Common } from "./common";
|
2023-05-12 16:31:01 -06:00
|
|
|
import redisCache from "./redis-cache";
|
2022-12-13 17:11:37 -06:00
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
export interface RbfTransaction extends TransactionStripped {
|
2022-12-13 17:11:37 -06:00
|
|
|
rbf?: boolean;
|
2022-12-17 09:39:06 -06:00
|
|
|
mined?: boolean;
|
2023-07-14 16:08:57 +09:00
|
|
|
fullRbf?: boolean;
|
2022-12-13 17:11:37 -06:00
|
|
|
}
|
2022-12-09 10:32:58 -06:00
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
export interface RbfTree {
|
2022-12-17 09:39:06 -06:00
|
|
|
tx: RbfTransaction;
|
|
|
|
time: number;
|
|
|
|
interval?: number;
|
|
|
|
mined?: boolean;
|
|
|
|
fullRbf: boolean;
|
|
|
|
replaces: RbfTree[];
|
|
|
|
}
|
2022-12-14 08:49:35 -06:00
|
|
|
|
2023-07-14 16:08:57 +09:00
|
|
|
export interface ReplacementInfo {
|
|
|
|
mined: boolean;
|
|
|
|
fullRbf: boolean;
|
|
|
|
txid: string;
|
|
|
|
oldFee: number;
|
|
|
|
oldVsize: number;
|
|
|
|
newFee: number;
|
|
|
|
newVsize: number;
|
|
|
|
}
|
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
enum CacheOp {
|
|
|
|
Remove = 0,
|
|
|
|
Add = 1,
|
|
|
|
Change = 2,
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CacheEvent {
|
|
|
|
op: CacheOp;
|
|
|
|
type: 'tx' | 'tree' | 'exp';
|
|
|
|
txid: string,
|
|
|
|
value?: any,
|
|
|
|
}
|
|
|
|
|
2024-08-25 22:38:00 +00:00
|
|
|
/**
|
|
|
|
* Singleton for tracking RBF trees
|
|
|
|
*
|
|
|
|
* Maintains a set of RBF trees, where each tree represents a sequence of
|
|
|
|
* consecutive RBF replacements.
|
|
|
|
*
|
|
|
|
* Trees are identified by the txid of the root transaction.
|
|
|
|
*
|
|
|
|
* To maintain consistency, the following invariants must be upheld:
|
|
|
|
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
|
|
|
|
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
|
|
|
|
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
|
|
|
|
* - Existence: X in treeMap => treeMap(X) in rbfTrees
|
|
|
|
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
|
|
|
|
*/
|
|
|
|
|
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
|
2023-05-29 15:56:29 -04:00
|
|
|
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
2023-05-04 22:59:34 -04:00
|
|
|
private expiring: Map<string, number> = new Map();
|
2023-05-12 16:31:01 -06:00
|
|
|
private cacheQueue: CacheEvent[] = [];
|
2022-03-08 14:49:25 +01:00
|
|
|
|
2023-10-16 01:00:18 +00:00
|
|
|
private evictionCount = 0;
|
|
|
|
private staleCount = 0;
|
|
|
|
|
2022-03-08 14:49:25 +01:00
|
|
|
constructor() {
|
2023-05-04 22:59:34 -04:00
|
|
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2024-08-25 22:38:00 +00:00
|
|
|
/**
|
|
|
|
* Low level cache operations
|
|
|
|
*/
|
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
|
|
|
this.txs.set(txid, tx);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private addTree(txid: string, tree: RbfTree): void {
|
|
|
|
this.rbfTrees.set(txid, tree);
|
|
|
|
this.dirtyTrees.add(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'tree', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private addExpiration(txid: string, expiry: number): void {
|
|
|
|
this.expiring.set(txid, expiry);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'exp', txid, value: expiry });
|
|
|
|
}
|
|
|
|
|
|
|
|
private removeTx(txid: string): void {
|
|
|
|
this.txs.delete(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tx', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private removeTree(txid: string): void {
|
|
|
|
this.rbfTrees.delete(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'tree', txid });
|
|
|
|
}
|
|
|
|
|
|
|
|
private removeExpiration(txid: string): void {
|
|
|
|
this.expiring.delete(txid);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
|
|
|
}
|
|
|
|
|
2024-08-25 22:38:00 +00:00
|
|
|
/**
|
|
|
|
* Basic data structure operations
|
|
|
|
* must uphold tree invariants
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2023-05-29 15:56:29 -04:00
|
|
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
2023-05-18 09:51:41 -04:00
|
|
|
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
2022-12-17 09:39:06 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-12-17 10:45:26 +00:00
|
|
|
newTxExtended.replacement = true;
|
|
|
|
|
2022-12-13 17:11:37 -06:00
|
|
|
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
2023-05-11 08:57:12 -06:00
|
|
|
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
|
2022-12-13 17:11:37 -06:00
|
|
|
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
2023-05-12 16:31:01 -06:00
|
|
|
this.addTx(newTx.txid, newTxExtended);
|
2022-12-17 09:39:06 -06:00
|
|
|
|
|
|
|
// maintain rbf trees
|
2023-07-14 16:08:57 +09:00
|
|
|
let txFullRbf = false;
|
|
|
|
let treeFullRbf = false;
|
2022-12-17 09:39:06 -06:00
|
|
|
const replacedTrees: RbfTree[] = [];
|
|
|
|
for (const replacedTxExtended of replaced) {
|
|
|
|
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
|
|
|
|
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
2023-07-14 16:08:57 +09:00
|
|
|
if (!replacedTx.rbf) {
|
|
|
|
txFullRbf = true;
|
|
|
|
}
|
2024-08-25 22:38:00 +00:00
|
|
|
if (this.replacedBy.has(replacedTx.txid)) {
|
|
|
|
// should never happen
|
|
|
|
continue;
|
|
|
|
}
|
2022-12-17 09:39:06 -06:00
|
|
|
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);
|
2023-05-12 16:31:01 -06:00
|
|
|
this.removeTree(treeId);
|
2022-12-17 09:39:06 -06:00
|
|
|
if (tree) {
|
|
|
|
tree.interval = newTime - tree?.time;
|
|
|
|
replacedTrees.push(tree);
|
2023-07-14 16:08:57 +09:00
|
|
|
treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf;
|
2022-12-17 09:39:06 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2023-05-11 08:57:12 -06:00
|
|
|
const replacedTime = replacedTxExtended.firstSeen || (Date.now() / 1000);
|
2022-12-17 09:39:06 -06:00
|
|
|
replacedTrees.push({
|
|
|
|
tx: replacedTx,
|
|
|
|
time: replacedTime,
|
|
|
|
interval: newTime - replacedTime,
|
|
|
|
fullRbf: !replacedTx.rbf,
|
|
|
|
replaces: [],
|
|
|
|
});
|
2023-07-14 16:08:57 +09:00
|
|
|
treeFullRbf = treeFullRbf || !replacedTx.rbf;
|
2023-05-12 16:31:01 -06:00
|
|
|
this.addTx(replacedTx.txid, replacedTxExtended);
|
2022-12-17 09:39:06 -06:00
|
|
|
}
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
2023-07-14 16:08:57 +09:00
|
|
|
newTx.fullRbf = txFullRbf;
|
2022-12-17 09:39:06 -06:00
|
|
|
const newTree = {
|
|
|
|
tx: newTx,
|
2023-05-11 08:57:12 -06:00
|
|
|
time: newTime,
|
2023-07-14 16:08:57 +09:00
|
|
|
fullRbf: treeFullRbf,
|
2022-12-17 09:39:06 -06:00
|
|
|
replaces: replacedTrees
|
|
|
|
};
|
2024-08-25 22:38:00 +00:00
|
|
|
this.addTree(newTree.tx.txid, newTree);
|
|
|
|
this.updateTreeMap(newTree.tx.txid, newTree);
|
2022-12-17 09:39:06 -06:00
|
|
|
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2024-08-25 22:38:00 +00:00
|
|
|
public mined(txid): void {
|
|
|
|
if (!this.txs.has(txid)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.evict(txid);
|
|
|
|
}
|
|
|
|
|
|
|
|
// flag a transaction as removed from the mempool
|
|
|
|
public evict(txid: string, fast: boolean = false): void {
|
|
|
|
this.evictionCount++;
|
|
|
|
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
|
|
|
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
|
|
|
this.addExpiration(txid, expiryTime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read-only public interface
|
|
|
|
*/
|
|
|
|
|
2023-07-25 14:00:17 +09:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-12-09 14:35:51 -06:00
|
|
|
public getReplacedBy(txId: string): string | undefined {
|
2022-12-14 08:49:35 -06:00
|
|
|
return this.replacedBy.get(txId);
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
|
|
|
|
2022-12-09 14:35:51 -06:00
|
|
|
public getReplaces(txId: string): string[] | undefined {
|
2022-12-14 08:49:35 -06:00
|
|
|
return this.replaces.get(txId);
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
|
|
|
|
2023-05-29 15:56:29 -04:00
|
|
|
public getTx(txId: string): MempoolTransactionExtended | 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-13 17:11:37 -06:00
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-06-19 18:14:09 -04:00
|
|
|
// is the transaction involved in a full rbf replacement?
|
|
|
|
public isFullRbf(txid: string): boolean {
|
|
|
|
const treeId = this.treeMap.get(txid);
|
|
|
|
if (!treeId) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const tree = this.rbfTrees.get(treeId);
|
|
|
|
if (!tree) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return tree?.fullRbf;
|
|
|
|
}
|
|
|
|
|
2024-08-25 22:38:00 +00:00
|
|
|
/**
|
|
|
|
* Cache maintenance & utility functions
|
|
|
|
*/
|
|
|
|
|
2022-03-08 14:49:25 +01:00
|
|
|
private cleanup(): void {
|
2023-05-04 22:59:34 -04:00
|
|
|
const now = Date.now();
|
|
|
|
for (const txid of this.expiring.keys()) {
|
|
|
|
if ((this.expiring.get(txid) || 0) < now) {
|
2023-05-12 16:31:01 -06:00
|
|
|
this.removeExpiration(txid);
|
2022-12-09 14:35:51 -06:00
|
|
|
this.remove(txid);
|
2022-12-09 10:32:58 -06:00
|
|
|
}
|
|
|
|
}
|
2023-10-16 01:00:18 +00:00
|
|
|
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire (${this.evictionCount} newly expired)`);
|
|
|
|
this.evictionCount = 0;
|
2022-12-09 14:35:51 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// remove a transaction & all previous versions from the cache
|
|
|
|
private remove(txid): void {
|
2022-12-13 17:11:37 -06:00
|
|
|
// 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)) {
|
2023-10-16 01:00:18 +00:00
|
|
|
const root = this.treeMap.get(txid);
|
2022-12-14 08:49:35 -06:00
|
|
|
const replaces = this.replaces.get(txid);
|
|
|
|
this.replaces.delete(txid);
|
2022-12-17 09:39:06 -06:00
|
|
|
this.treeMap.delete(txid);
|
2023-05-12 16:31:01 -06:00
|
|
|
this.removeTx(txid);
|
|
|
|
this.removeExpiration(txid);
|
2023-10-16 01:00:18 +00:00
|
|
|
if (root === txid) {
|
|
|
|
this.removeTree(txid);
|
|
|
|
}
|
2022-12-14 08:49:35 -06:00
|
|
|
for (const tx of (replaces || [])) {
|
2022-12-09 14:35:51 -06:00
|
|
|
// recursively remove prior versions from the cache
|
2022-12-14 08:49:35 -06:00
|
|
|
this.replacedBy.delete(tx);
|
2022-12-09 14:35:51 -06:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2023-03-05 03:02:46 -06:00
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
public async updateCache(): Promise<void> {
|
|
|
|
if (!config.REDIS.ENABLED) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Update the Redis cache by replaying queued events
|
|
|
|
for (const e of this.cacheQueue) {
|
|
|
|
if (e.op === CacheOp.Add || e.op === CacheOp.Change) {
|
|
|
|
let value = e.value;
|
|
|
|
switch(e.type) {
|
|
|
|
case 'tx': {
|
|
|
|
value = this.txs.get(e.txid);
|
|
|
|
} break;
|
|
|
|
case 'tree': {
|
|
|
|
const tree = this.rbfTrees.get(e.txid);
|
|
|
|
value = tree ? this.exportTree(tree) : null;
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
if (value != null) {
|
|
|
|
await redisCache.$setRbfEntry(e.type, e.txid, value);
|
|
|
|
}
|
|
|
|
} else if (e.op === CacheOp.Remove) {
|
|
|
|
await redisCache.$removeRbfEntry(e.type, e.txid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.cacheQueue = [];
|
|
|
|
}
|
|
|
|
|
2023-03-05 03:02:46 -06:00
|
|
|
public dump(): any {
|
|
|
|
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
|
|
|
|
|
|
|
|
return {
|
|
|
|
txs: Array.from(this.txs.entries()),
|
|
|
|
trees,
|
|
|
|
expiring: Array.from(this.expiring.entries()),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-12-17 10:45:26 +00:00
|
|
|
public async load({ txs, trees, expiring, mempool }): Promise<void> {
|
2023-11-11 05:52:37 +00:00
|
|
|
try {
|
|
|
|
txs.forEach(txEntry => {
|
|
|
|
this.txs.set(txEntry.value.txid, txEntry.value);
|
|
|
|
});
|
|
|
|
this.staleCount = 0;
|
2024-08-25 22:38:00 +00:00
|
|
|
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
|
|
|
|
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
|
|
|
if (tree) {
|
|
|
|
this.addTree(tree.tx.txid, tree);
|
|
|
|
this.updateTreeMap(tree.tx.txid, tree);
|
|
|
|
if (tree.mined) {
|
|
|
|
this.evict(tree.tx.txid);
|
|
|
|
}
|
|
|
|
}
|
2023-05-04 22:59:34 -04:00
|
|
|
}
|
2023-11-11 05:52:37 +00:00
|
|
|
expiring.forEach(expiringEntry => {
|
|
|
|
if (this.txs.has(expiringEntry.key)) {
|
|
|
|
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.staleCount = 0;
|
2023-08-05 16:08:54 +09:00
|
|
|
await this.checkTrees();
|
2023-11-12 06:19:46 +00:00
|
|
|
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
2023-11-11 05:52:37 +00:00
|
|
|
this.cleanup();
|
2023-11-12 06:19:46 +00:00
|
|
|
|
2023-11-11 05:52:37 +00:00
|
|
|
} catch (e) {
|
|
|
|
logger.err('failed to restore RBF cache: ' + (e instanceof Error ? e.message : e));
|
|
|
|
}
|
2023-03-05 03:02:46 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
exportTree(tree: RbfTree, deflated: any = null) {
|
|
|
|
if (!deflated) {
|
|
|
|
deflated = {
|
|
|
|
root: tree.tx.txid,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
deflated[tree.tx.txid] = {
|
|
|
|
tx: tree.tx.txid,
|
|
|
|
txMined: tree.tx.mined,
|
|
|
|
time: tree.time,
|
|
|
|
interval: tree.interval,
|
|
|
|
mined: tree.mined,
|
|
|
|
fullRbf: tree.fullRbf,
|
|
|
|
replaces: tree.replaces.map(child => child.tx.txid),
|
|
|
|
};
|
|
|
|
tree.replaces.forEach(child => {
|
|
|
|
this.exportTree(child, deflated);
|
|
|
|
});
|
|
|
|
return deflated;
|
|
|
|
}
|
|
|
|
|
2023-12-17 10:45:26 +00:00
|
|
|
async importTree(mempool, root, txid, deflated, txs: Map<string, MempoolTransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
|
2023-03-05 03:02:46 -06:00
|
|
|
const treeInfo = deflated[txid];
|
|
|
|
const replaces: RbfTree[] = [];
|
|
|
|
|
2023-10-16 01:00:18 +00:00
|
|
|
// if the root tx is unknown, remove this tree and return early
|
|
|
|
if (root === txid && !txs.has(txid)) {
|
|
|
|
this.staleCount++;
|
|
|
|
this.removeTree(deflated.key);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-08-25 22:38:00 +00:00
|
|
|
// if this tx is already in the cache, return early
|
|
|
|
if (this.treeMap.has(txid)) {
|
|
|
|
this.removeTree(deflated.key);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-03-05 03:02:46 -06:00
|
|
|
// recursively reconstruct child trees
|
|
|
|
for (const childId of treeInfo.replaces) {
|
2023-12-17 10:45:26 +00:00
|
|
|
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
2023-03-05 03:02:46 -06:00
|
|
|
if (replaced) {
|
|
|
|
this.replacedBy.set(replaced.tx.txid, txid);
|
2023-12-17 10:45:26 +00:00
|
|
|
if (mempool[replaced.tx.txid]) {
|
|
|
|
mempool[replaced.tx.txid].replacement = true;
|
|
|
|
}
|
2023-03-05 03:02:46 -06:00
|
|
|
replaces.push(replaced);
|
|
|
|
if (replaced.mined) {
|
|
|
|
mined = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.replaces.set(txid, replaces.map(t => t.tx.txid));
|
|
|
|
|
|
|
|
const tx = txs.get(txid);
|
|
|
|
if (!tx) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const strippedTx = Common.stripTransaction(tx) as RbfTransaction;
|
|
|
|
strippedTx.rbf = tx.vin.some((v) => v.sequence < 0xfffffffe);
|
|
|
|
strippedTx.mined = treeInfo.txMined;
|
|
|
|
const tree = {
|
|
|
|
tx: strippedTx,
|
|
|
|
time: treeInfo.time,
|
|
|
|
interval: treeInfo.interval,
|
|
|
|
mined: mined,
|
|
|
|
fullRbf: treeInfo.fullRbf,
|
|
|
|
replaces,
|
|
|
|
};
|
|
|
|
return tree;
|
|
|
|
}
|
2023-07-14 16:08:57 +09:00
|
|
|
|
2023-08-05 16:08:54 +09:00
|
|
|
private async checkTrees(): Promise<void> {
|
|
|
|
const found: { [txid: string]: boolean } = {};
|
|
|
|
const txids = Array.from(this.txs.values()).map(tx => tx.txid).filter(txid => {
|
|
|
|
return !this.expiring.has(txid) && !this.getRbfTree(txid)?.mined;
|
|
|
|
});
|
|
|
|
|
|
|
|
const processTxs = (txs: IEsploraApi.Transaction[]): void => {
|
|
|
|
for (const tx of txs) {
|
|
|
|
found[tx.txid] = true;
|
|
|
|
if (tx.status?.confirmed) {
|
|
|
|
const tree = this.getRbfTree(tx.txid);
|
|
|
|
if (tree) {
|
|
|
|
this.setTreeMined(tree, tx.txid);
|
|
|
|
tree.mined = true;
|
|
|
|
this.evict(tx.txid, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
2023-11-17 07:06:44 +00:00
|
|
|
let processedCount = 0;
|
2023-11-15 06:58:00 +00:00
|
|
|
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
|
2023-08-05 16:08:54 +09:00
|
|
|
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
|
|
|
|
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
|
2023-11-17 07:06:44 +00:00
|
|
|
processedCount += slice.length;
|
2023-08-05 16:08:54 +09:00
|
|
|
try {
|
|
|
|
const txs = await bitcoinApi.$getRawTransactions(slice);
|
|
|
|
processTxs(txs);
|
2023-11-17 07:06:44 +00:00
|
|
|
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
|
2023-08-05 16:08:54 +09:00
|
|
|
} catch (err) {
|
2023-11-12 09:23:37 +00:00
|
|
|
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
|
2023-08-05 16:08:54 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const txs: IEsploraApi.Transaction[] = [];
|
|
|
|
for (const txid of txids) {
|
|
|
|
try {
|
|
|
|
const tx = await bitcoinApi.$getRawTransaction(txid, true, false);
|
|
|
|
txs.push(tx);
|
|
|
|
} catch (err) {
|
|
|
|
// some 404s are expected, so continue quietly
|
|
|
|
}
|
|
|
|
}
|
|
|
|
processTxs(txs);
|
|
|
|
}
|
|
|
|
|
2024-08-25 22:38:00 +00:00
|
|
|
// evict missing transactions
|
2023-08-05 16:08:54 +09:00
|
|
|
for (const txid of txids) {
|
|
|
|
if (!found[txid]) {
|
|
|
|
this.evict(txid, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-14 16:08:57 +09:00
|
|
|
public getLatestRbfSummary(): ReplacementInfo[] {
|
|
|
|
const rbfList = this.getRbfTrees(false);
|
|
|
|
return rbfList.slice(0, 6).map(rbfTree => {
|
|
|
|
let oldFee = 0;
|
|
|
|
let oldVsize = 0;
|
|
|
|
for (const replaced of rbfTree.replaces) {
|
|
|
|
oldFee += replaced.tx.fee;
|
|
|
|
oldVsize += replaced.tx.vsize;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
txid: rbfTree.tx.txid,
|
|
|
|
mined: !!rbfTree.tx.mined,
|
|
|
|
fullRbf: !!rbfTree.tx.fullRbf,
|
|
|
|
oldFee,
|
|
|
|
oldVsize,
|
|
|
|
newFee: rbfTree.tx.fee,
|
|
|
|
newVsize: rbfTree.tx.vsize,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
2022-03-08 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export default new RbfCache();
|