Timeline of replacements for RBF-d transactions
This commit is contained in:
@@ -32,7 +32,7 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
@@ -642,8 +642,12 @@ class BitcoinRoutes {
|
||||
|
||||
private async getRbfHistory(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getReplaces(req.params.txId);
|
||||
res.json(result || []);
|
||||
const replacements = rbfCache.getRbfChain(req.params.txId) || [];
|
||||
const replaces = rbfCache.getReplaces(req.params.txId) || null;
|
||||
res.json({
|
||||
replacements,
|
||||
replaces
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ class Mempool {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (this.mempoolCache[rbfTransaction]) {
|
||||
// Store replaced transactions
|
||||
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
|
||||
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction]);
|
||||
// Erase the replaced transactions from the local mempool
|
||||
delete this.mempoolCache[rbfTransaction];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { TransactionExtended } from "../mempool.interfaces";
|
||||
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||
import { Common } from "./common";
|
||||
|
||||
interface RbfTransaction extends TransactionStripped {
|
||||
rbf?: boolean;
|
||||
}
|
||||
|
||||
class RbfCache {
|
||||
private replacedBy: { [txid: string]: string; } = {};
|
||||
private replaces: { [txid: string]: string[] } = {};
|
||||
private rbfChains: { [root: string]: { tx: TransactionStripped, time: number, mined?: boolean }[] } = {}; // sequences of consecutive replacements
|
||||
private chainMap: { [txid: string]: string } = {}; // map of txids to sequence ids
|
||||
private txs: { [txid: string]: TransactionExtended } = {};
|
||||
private expiring: { [txid: string]: Date } = {};
|
||||
|
||||
@@ -10,13 +17,34 @@ class RbfCache {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
public add(replacedTx: TransactionExtended, newTxId: string): void {
|
||||
this.replacedBy[replacedTx.txid] = newTxId;
|
||||
this.txs[replacedTx.txid] = replacedTx;
|
||||
if (!this.replaces[newTxId]) {
|
||||
this.replaces[newTxId] = [];
|
||||
public add(replacedTxExtended: TransactionExtended, newTxExtended: TransactionExtended): void {
|
||||
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
|
||||
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
||||
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
|
||||
this.replacedBy[replacedTx.txid] = newTx.txid;
|
||||
this.txs[replacedTx.txid] = replacedTxExtended;
|
||||
if (!this.replaces[newTx.txid]) {
|
||||
this.replaces[newTx.txid] = [];
|
||||
}
|
||||
this.replaces[newTx.txid].push(replacedTx.txid);
|
||||
|
||||
// maintain rbf chains
|
||||
if (this.chainMap[replacedTx.txid]) {
|
||||
// add to an existing chain
|
||||
const chainRoot = this.chainMap[replacedTx.txid];
|
||||
this.rbfChains[chainRoot].push({ tx: newTx, time: newTxExtended.firstSeen || Date.now() });
|
||||
this.chainMap[newTx.txid] = chainRoot;
|
||||
} else {
|
||||
// start a new chain
|
||||
this.rbfChains[replacedTx.txid] = [
|
||||
{ tx: replacedTx, time: replacedTxExtended.firstSeen || Date.now() },
|
||||
{ tx: newTx, time: newTxExtended.firstSeen || Date.now() },
|
||||
];
|
||||
this.chainMap[replacedTx.txid] = replacedTx.txid;
|
||||
this.chainMap[newTx.txid] = replacedTx.txid;
|
||||
}
|
||||
this.replaces[newTxId].push(replacedTx.txid);
|
||||
}
|
||||
|
||||
public getReplacedBy(txId: string): string | undefined {
|
||||
@@ -31,6 +59,10 @@ class RbfCache {
|
||||
return this.txs[txId];
|
||||
}
|
||||
|
||||
public getRbfChain(txId: string): { tx: TransactionStripped, time: number }[] {
|
||||
return this.rbfChains[this.chainMap[txId]] || [];
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid): void {
|
||||
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
|
||||
@@ -48,14 +80,20 @@ class RbfCache {
|
||||
|
||||
// remove a transaction & all previous versions from the cache
|
||||
private remove(txid): void {
|
||||
// don't remove a transaction while a newer version remains in the mempool
|
||||
if (this.replaces[txid] && !this.replacedBy[txid]) {
|
||||
// don't remove a transaction if a newer version remains in the mempool
|
||||
if (!this.replacedBy[txid]) {
|
||||
const replaces = this.replaces[txid];
|
||||
delete this.replaces[txid];
|
||||
delete this.chainMap[txid];
|
||||
delete this.txs[txid];
|
||||
delete this.expiring[txid];
|
||||
for (const tx of replaces) {
|
||||
// recursively remove prior versions from the cache
|
||||
delete this.replacedBy[tx];
|
||||
delete this.txs[tx];
|
||||
// if this is the root of a chain, remove that too
|
||||
if (this.chainMap[tx] === tx) {
|
||||
delete this.rbfChains[tx];
|
||||
}
|
||||
this.remove(tx);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user