Catch RBF replacements across mempool update boundaries
This commit is contained in:
parent
185eae00e9
commit
e362003746
@ -80,8 +80,8 @@ export class Common {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
|
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||||
|
|
||||||
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
||||||
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
||||||
@ -96,7 +96,7 @@ export class Common {
|
|||||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||||
});
|
});
|
||||||
if (foundMatches?.length) {
|
if (foundMatches?.length) {
|
||||||
matches[addedTx.txid] = [...new Set(foundMatches)];
|
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -124,7 +124,7 @@ export class Common {
|
|||||||
foundMatches.add(deletedTx);
|
foundMatches.add(deletedTx);
|
||||||
}
|
}
|
||||||
if (foundMatches.size) {
|
if (foundMatches.size) {
|
||||||
matches[addedTx.txid] = [...foundMatches];
|
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,17 +139,17 @@ export class Common {
|
|||||||
const replaced: Set<MempoolTransactionExtended> = new Set();
|
const replaced: Set<MempoolTransactionExtended> = new Set();
|
||||||
for (let i = 0; i < tx.vin.length; i++) {
|
for (let i = 0; i < tx.vin.length; i++) {
|
||||||
const vin = tx.vin[i];
|
const vin = tx.vin[i];
|
||||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
const key = `${vin.txid}:${vin.vout}`;
|
||||||
|
const match = spendMap.get(key);
|
||||||
if (match && match.txid !== tx.txid) {
|
if (match && match.txid !== tx.txid) {
|
||||||
replaced.add(match);
|
replaced.add(match);
|
||||||
// remove this tx from the spendMap
|
// remove this tx from the spendMap
|
||||||
// prevents the same tx being replaced more than once
|
// prevents the same tx being replaced more than once
|
||||||
for (const replacedVin of match.vin) {
|
for (const replacedVin of match.vin) {
|
||||||
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||||
spendMap.delete(key);
|
spendMap.delete(replacedKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const key = `${vin.txid}:${vin.vout}`;
|
|
||||||
spendMap.delete(key);
|
spendMap.delete(key);
|
||||||
}
|
}
|
||||||
if (replaced.size) {
|
if (replaced.size) {
|
||||||
|
@ -19,12 +19,13 @@ class Mempool {
|
|||||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||||
|
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
|
||||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
|
||||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||||
|
|
||||||
private accelerations: { [txId: string]: Acceleration } = {};
|
private accelerations: { [txId: string]: Acceleration } = {};
|
||||||
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
||||||
@ -74,12 +75,12 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
|
||||||
this.mempoolChangedCallback = fn;
|
this.mempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates) => Promise<void>): void {
|
candidates?: GbtCandidates) => Promise<void>): void {
|
||||||
this.$asyncMempoolChangedCallback = fn;
|
this.$asyncMempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
@ -362,12 +363,15 @@ class Mempool {
|
|||||||
|
|
||||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||||
|
|
||||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
this.recentlyDeleted.unshift(deletedTransactions);
|
||||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
|
||||||
|
|
||||||
|
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
|
||||||
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
|
||||||
}
|
}
|
||||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
|
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
|
||||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
|
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
|
||||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,16 +545,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
|
||||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
|
||||||
// Store replaced transactions
|
|
||||||
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
for (const rbfTransaction in rbfTransactions) {
|
||||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||||
// Store replaced transactions
|
// Store replaced transactions
|
||||||
|
@ -520,8 +520,17 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param newMempool
|
||||||
|
* @param mempoolSize
|
||||||
|
* @param newTransactions array of transactions added this mempool update.
|
||||||
|
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
|
||||||
|
* @param accelerationDelta
|
||||||
|
* @param candidates
|
||||||
|
*/
|
||||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates): Promise<void> {
|
candidates?: GbtCandidates): Promise<void> {
|
||||||
if (!this.webSocketServers.length) {
|
if (!this.webSocketServers.length) {
|
||||||
throw new Error('No WebSocket.Server have been set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
@ -529,6 +538,8 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
|
|
||||||
|
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
|
||||||
|
|
||||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||||
let added = newTransactions;
|
let added = newTransactions;
|
||||||
let removed = deletedTransactions;
|
let removed = deletedTransactions;
|
||||||
@ -547,7 +558,7 @@ class WebsocketHandler {
|
|||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
const accelerations = memPool.getAccelerations();
|
const accelerations = memPool.getAccelerations();
|
||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
@ -578,7 +589,7 @@ class WebsocketHandler {
|
|||||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||||
for (const tx of newTransactions) {
|
for (const tx of newTransactions) {
|
||||||
if (rbfTransactions[tx.txid]) {
|
if (rbfTransactions[tx.txid]) {
|
||||||
for (const replaced of rbfTransactions[tx.txid]) {
|
for (const replaced of rbfTransactions[tx.txid].replaced) {
|
||||||
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -947,7 +958,7 @@ class WebsocketHandler {
|
|||||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||||
|
|
||||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
memPool.removeFromSpendMap(transactions);
|
memPool.removeFromSpendMap(transactions);
|
||||||
|
|
||||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user