Persist RBF cache to disk
This commit is contained in:
		
							parent
							
								
									6fb4adc27d
								
							
						
					
					
						commit
						7e9cfa0858
					
				| @ -7,14 +7,17 @@ import logger from '../logger'; | ||||
| import config from '../config'; | ||||
| import { TransactionExtended } from '../mempool.interfaces'; | ||||
| import { Common } from './common'; | ||||
| import rbfCache from './rbf-cache'; | ||||
| 
 | ||||
| class DiskCache { | ||||
|   private cacheSchemaVersion = 3; | ||||
|   private rbfCacheSchemaVersion = 1; | ||||
| 
 | ||||
|   private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json'; | ||||
|   private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json'; | ||||
|   private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json'; | ||||
|   private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json'; | ||||
|   private static RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/rbfcache.json'; | ||||
|   private static CHUNK_FILES = 25; | ||||
|   private isWritingCache = false; | ||||
| 
 | ||||
| @ -100,6 +103,20 @@ class DiskCache { | ||||
|       logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e)); | ||||
|       this.isWritingCache = false; | ||||
|     } | ||||
|     try { | ||||
|       logger.debug('Writing rbf data to disk cache (async)...'); | ||||
|       this.isWritingCache = true; | ||||
| 
 | ||||
|       await fsPromises.writeFile(DiskCache.RBF_FILE_NAME, JSON.stringify({ | ||||
|         rbfCacheSchemaVersion: this.rbfCacheSchemaVersion, | ||||
|         rbf: rbfCache.dump(), | ||||
|       }), { flag: 'w' }); | ||||
|       logger.debug('Rbf data saved to disk cache'); | ||||
|       this.isWritingCache = false; | ||||
|     } catch (e) { | ||||
|       logger.warn('Error writing rbf data to cache file: ' + (e instanceof Error ? e.message : e)); | ||||
|       this.isWritingCache = false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   wipeCache(): void { | ||||
| @ -124,6 +141,18 @@ class DiskCache { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   wipeRbfCache() { | ||||
|     logger.notice(`Wipping nodejs backend cache/rbfcache.json file`); | ||||
| 
 | ||||
|     try { | ||||
|       fs.unlinkSync(DiskCache.RBF_FILE_NAME); | ||||
|     } catch (e: any) { | ||||
|       if (e?.code !== 'ENOENT') { | ||||
|         logger.err(`Cannot wipe cache file ${DiskCache.RBF_FILE_NAME}. Exception ${JSON.stringify(e)}`); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async $loadMempoolCache(): Promise<void> { | ||||
|     if (!fs.existsSync(DiskCache.FILE_NAME)) { | ||||
|       return; | ||||
| @ -174,6 +203,23 @@ class DiskCache { | ||||
|     } catch (e) { | ||||
|       logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       let rbfData: any = {}; | ||||
|       const rbfCacheData = fs.readFileSync(DiskCache.RBF_FILE_NAME, 'utf8'); | ||||
|       if (rbfCacheData) { | ||||
|         logger.info('Restoring rbf data from disk cache'); | ||||
|         rbfData = JSON.parse(rbfCacheData); | ||||
|         if (rbfData.rbfCacheSchemaVersion === undefined || rbfData.rbfCacheSchemaVersion !== this.rbfCacheSchemaVersion) { | ||||
|           logger.notice('Rbf disk cache contains an outdated schema version. Clearing it and skipping the cache loading.'); | ||||
|           return this.wipeRbfCache(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       rbfCache.load(rbfData.rbf); | ||||
|     } catch (e) { | ||||
|       logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { runInNewContext } from "vm"; | ||||
| import { TransactionExtended, TransactionStripped } from "../mempool.interfaces"; | ||||
| import bitcoinApi from './bitcoin/bitcoin-api-factory'; | ||||
| import { Common } from "./common"; | ||||
| 
 | ||||
| interface RbfTransaction extends TransactionStripped { | ||||
| @ -218,6 +218,104 @@ class RbfCache { | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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()), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   public async load({ txs, trees, expiring }): Promise<void> { | ||||
|     txs.forEach(txEntry => { | ||||
|       this.txs.set(txEntry[0], txEntry[1]); | ||||
|     }); | ||||
|     for (const deflatedTree of trees) { | ||||
|       await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); | ||||
|     } | ||||
|     expiring.forEach(expiringEntry => { | ||||
|       this.expiring.set(expiringEntry[0], expiringEntry[1]); | ||||
|     }); | ||||
|     this.cleanup(); | ||||
|   } | ||||
| 
 | ||||
|   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; | ||||
|   } | ||||
| 
 | ||||
|   async importTree(root, txid, deflated, txs: Map<string, TransactionExtended>, mined: boolean = false): Promise<RbfTree | void> { | ||||
|     const treeInfo = deflated[txid]; | ||||
|     const replaces: RbfTree[] = []; | ||||
| 
 | ||||
|     // check if any transactions in this tree have already been confirmed
 | ||||
|     mined = mined || treeInfo.mined; | ||||
|     if (!mined) { | ||||
|       try { | ||||
|         const apiTx = await bitcoinApi.$getRawTransaction(txid); | ||||
|         if (apiTx?.status?.confirmed) { | ||||
|           mined = true; | ||||
|           this.evict(txid); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         // most transactions do not exist
 | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // recursively reconstruct child trees
 | ||||
|     for (const childId of treeInfo.replaces) { | ||||
|       const replaced = await this.importTree(root, childId, deflated, txs, mined); | ||||
|       if (replaced) { | ||||
|         this.replacedBy.set(replaced.tx.txid, txid); | ||||
|         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, | ||||
|     }; | ||||
|     this.treeMap.set(txid, root); | ||||
|     if (root === txid) { | ||||
|       this.rbfTrees.set(root, tree); | ||||
|       this.dirtyTrees.add(root); | ||||
|     } | ||||
|     return tree; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default new RbfCache(); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user