2023-05-12 16:29:45 -06:00
|
|
|
import { createClient } from 'redis';
|
|
|
|
import memPool from './mempool';
|
|
|
|
import blocks from './blocks';
|
|
|
|
import logger from '../logger';
|
|
|
|
import config from '../config';
|
2023-06-16 18:55:22 -04:00
|
|
|
import { BlockExtended, BlockSummary, MempoolTransactionExtended } from '../mempool.interfaces';
|
2023-05-12 16:31:01 -06:00
|
|
|
import rbfCache from './rbf-cache';
|
2023-07-25 16:35:21 +09:00
|
|
|
import transactionUtils from './transaction-utils';
|
2023-05-12 16:29:45 -06:00
|
|
|
|
2023-06-16 19:00:52 -04:00
|
|
|
enum NetworkDB {
|
|
|
|
mainnet = 0,
|
|
|
|
testnet,
|
|
|
|
signet,
|
|
|
|
liquid,
|
|
|
|
liquidtestnet,
|
|
|
|
}
|
|
|
|
|
2023-05-12 16:29:45 -06:00
|
|
|
class RedisCache {
|
|
|
|
private client;
|
|
|
|
private connected = false;
|
2023-06-16 19:00:52 -04:00
|
|
|
private schemaVersion = 1;
|
2023-05-12 16:29:45 -06:00
|
|
|
|
2023-06-16 18:56:34 -04:00
|
|
|
private cacheQueue: MempoolTransactionExtended[] = [];
|
2023-07-25 16:35:21 +09:00
|
|
|
private txFlushLimit: number = 10000;
|
2023-06-16 18:56:34 -04:00
|
|
|
|
2023-05-12 16:29:45 -06:00
|
|
|
constructor() {
|
|
|
|
if (config.REDIS.ENABLED) {
|
|
|
|
const redisConfig = {
|
|
|
|
socket: {
|
|
|
|
path: config.REDIS.UNIX_SOCKET_PATH
|
2023-06-16 19:00:52 -04:00
|
|
|
},
|
|
|
|
database: NetworkDB[config.MEMPOOL.NETWORK],
|
2023-05-12 16:29:45 -06:00
|
|
|
};
|
|
|
|
this.client = createClient(redisConfig);
|
|
|
|
this.client.on('error', (e) => {
|
|
|
|
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
|
|
|
});
|
|
|
|
this.$ensureConnected();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async $ensureConnected(): Promise<void> {
|
|
|
|
if (!this.connected && config.REDIS.ENABLED) {
|
2023-06-16 19:00:52 -04:00
|
|
|
return this.client.connect().then(async () => {
|
2023-05-12 16:29:45 -06:00
|
|
|
this.connected = true;
|
|
|
|
logger.info(`Redis client connected`);
|
2023-06-16 19:00:52 -04:00
|
|
|
const version = await this.client.get('schema_version');
|
|
|
|
if (version !== this.schemaVersion) {
|
|
|
|
// schema changed
|
|
|
|
// perform migrations or flush DB if necessary
|
|
|
|
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
|
|
|
|
await this.client.set('schema_version', this.schemaVersion);
|
|
|
|
}
|
2023-05-12 16:29:45 -06:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async $updateBlocks(blocks: BlockExtended[]) {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
await this.client.set('blocks', JSON.stringify(blocks));
|
2023-07-31 12:19:28 +09:00
|
|
|
logger.debug(`Saved latest blocks to Redis cache`);
|
2023-05-12 16:29:45 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async $updateBlockSummaries(summaries: BlockSummary[]) {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
await this.client.set('block-summaries', JSON.stringify(summaries));
|
2023-07-31 12:19:28 +09:00
|
|
|
logger.debug(`Saved latest block summaries to Redis cache`);
|
2023-05-12 16:29:45 -06:00
|
|
|
} catch (e) {
|
2023-07-31 12:19:28 +09:00
|
|
|
logger.warn(`Failed to update block summaries in Redis cache: ${e instanceof Error ? e.message : e}`);
|
2023-05-12 16:29:45 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-16 18:56:34 -04:00
|
|
|
async $addTransaction(tx: MempoolTransactionExtended) {
|
|
|
|
this.cacheQueue.push(tx);
|
2023-07-25 16:35:21 +09:00
|
|
|
if (this.cacheQueue.length >= this.txFlushLimit) {
|
2023-06-16 18:56:34 -04:00
|
|
|
await this.$flushTransactions();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async $flushTransactions() {
|
2023-06-16 19:00:52 -04:00
|
|
|
const success = await this.$addTransactions(this.cacheQueue);
|
|
|
|
if (success) {
|
2023-07-31 12:19:28 +09:00
|
|
|
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
|
2023-06-16 19:00:52 -04:00
|
|
|
this.cacheQueue = [];
|
2023-07-25 16:35:21 +09:00
|
|
|
} else {
|
2023-07-31 12:19:28 +09:00
|
|
|
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
|
2023-06-16 19:00:52 -04:00
|
|
|
}
|
2023-06-16 18:56:34 -04:00
|
|
|
}
|
|
|
|
|
2023-07-25 16:35:21 +09:00
|
|
|
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
2023-07-30 16:01:03 +09:00
|
|
|
if (!newTransactions.length) {
|
|
|
|
return true;
|
|
|
|
}
|
2023-05-12 16:29:45 -06:00
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
const msetData = newTransactions.map(tx => {
|
2023-07-25 16:35:21 +09:00
|
|
|
const minified: any = { ...tx };
|
|
|
|
delete minified.hex;
|
|
|
|
for (const vin of minified.vin) {
|
|
|
|
delete vin.inner_redeemscript_asm;
|
|
|
|
delete vin.inner_witnessscript_asm;
|
|
|
|
delete vin.scriptsig_asm;
|
|
|
|
}
|
|
|
|
for (const vout of minified.vout) {
|
|
|
|
delete vout.scriptpubkey_asm;
|
|
|
|
}
|
2023-07-30 16:01:03 +09:00
|
|
|
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
|
|
|
|
});
|
|
|
|
await this.client.MSET(msetData);
|
2023-06-16 19:00:52 -04:00
|
|
|
return true;
|
2023-05-12 16:29:45 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
|
2023-06-16 19:00:52 -04:00
|
|
|
return false;
|
2023-05-12 16:29:45 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async $removeTransactions(transactions: string[]) {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
for (let i = 0; i < Math.ceil(transactions.length / 1000); i++) {
|
|
|
|
await this.client.del(transactions.slice(i * 1000, (i + 1) * 1000).map(txid => `mempool:tx:${txid}`));
|
2023-07-31 12:19:28 +09:00
|
|
|
logger.info(`Deleted ${transactions.length} transactions from the Redis cache`);
|
2023-07-30 16:01:03 +09:00
|
|
|
}
|
2023-05-12 16:29:45 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
|
2023-05-12 16:31:01 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async $removeRbfEntry(type: string, txid: string): Promise<void> {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
|
|
|
await this.client.del(`rbf:${type}:${txid}`);
|
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-12 16:29:45 -06:00
|
|
|
async $getBlocks(): Promise<BlockExtended[]> {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
const json = await this.client.get('blocks');
|
|
|
|
return JSON.parse(json);
|
2023-05-12 16:29:45 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async $getBlockSummaries(): Promise<BlockSummary[]> {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
const json = await this.client.get('block-summaries');
|
|
|
|
return JSON.parse(json);
|
2023-05-12 16:29:45 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-16 18:55:22 -04:00
|
|
|
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
|
2023-07-13 09:25:05 +09:00
|
|
|
const start = Date.now();
|
2023-07-30 16:01:03 +09:00
|
|
|
const mempool = {};
|
2023-05-12 16:29:45 -06:00
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
|
|
|
|
for (const tx of mempoolList) {
|
|
|
|
mempool[tx.key] = tx.value;
|
2023-05-12 16:29:45 -06:00
|
|
|
}
|
2023-07-30 16:01:03 +09:00
|
|
|
logger.info(`Loaded mempool from Redis cache in ${Date.now() - start} ms`);
|
2023-07-13 09:25:05 +09:00
|
|
|
return mempool || {};
|
2023-05-12 16:29:45 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
}
|
2023-07-13 09:25:05 +09:00
|
|
|
return {};
|
2023-05-12 16:29:45 -06:00
|
|
|
}
|
|
|
|
|
2023-05-12 16:31:01 -06:00
|
|
|
async $getRbfEntries(type: string): Promise<any[]> {
|
|
|
|
try {
|
|
|
|
await this.$ensureConnected();
|
2023-07-30 16:01:03 +09:00
|
|
|
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
|
|
|
|
return rbfEntries;
|
2023-05-12 16:31:01 -06:00
|
|
|
} catch (e) {
|
|
|
|
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-12 16:29:45 -06:00
|
|
|
async $loadCache() {
|
|
|
|
logger.info('Restoring mempool and blocks data from Redis cache');
|
|
|
|
// Load block data
|
|
|
|
const loadedBlocks = await this.$getBlocks();
|
|
|
|
const loadedBlockSummaries = await this.$getBlockSummaries();
|
|
|
|
// Load mempool
|
|
|
|
const loadedMempool = await this.$getMempool();
|
2023-07-25 16:35:21 +09:00
|
|
|
this.inflateLoadedTxs(loadedMempool);
|
2023-05-12 16:31:01 -06:00
|
|
|
// Load rbf data
|
|
|
|
const rbfTxs = await this.$getRbfEntries('tx');
|
|
|
|
const rbfTrees = await this.$getRbfEntries('tree');
|
|
|
|
const rbfExpirations = await this.$getRbfEntries('exp');
|
2023-05-12 16:29:45 -06:00
|
|
|
|
|
|
|
// Set loaded data
|
|
|
|
blocks.setBlocks(loadedBlocks || []);
|
|
|
|
blocks.setBlockSummaries(loadedBlockSummaries || []);
|
|
|
|
await memPool.$setMempool(loadedMempool);
|
2023-05-12 16:31:01 -06:00
|
|
|
await rbfCache.load({
|
|
|
|
txs: rbfTxs,
|
2023-07-30 16:01:03 +09:00
|
|
|
trees: rbfTrees.map(loadedTree => loadedTree.value),
|
2023-05-12 16:31:01 -06:00
|
|
|
expiring: rbfExpirations,
|
|
|
|
});
|
2023-05-12 16:29:45 -06:00
|
|
|
}
|
|
|
|
|
2023-07-25 16:35:21 +09:00
|
|
|
private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
|
|
|
|
for (const tx of Object.values(mempool)) {
|
|
|
|
for (const vin of tx.vin) {
|
|
|
|
if (vin.scriptsig) {
|
|
|
|
vin.scriptsig_asm = transactionUtils.convertScriptSigAsm(vin.scriptsig);
|
|
|
|
transactionUtils.addInnerScriptsToVin(vin);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const vout of tx.vout) {
|
|
|
|
if (vout.scriptpubkey) {
|
|
|
|
vout.scriptpubkey_asm = transactionUtils.convertScriptSigAsm(vout.scriptpubkey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-30 16:01:03 +09:00
|
|
|
private async scanKeys<T>(pattern): Promise<{ key: string, value: T }[]> {
|
|
|
|
logger.info(`loading Redis entries for ${pattern}`);
|
|
|
|
let keys: string[] = [];
|
|
|
|
const result: { key: string, value: T }[] = [];
|
|
|
|
const patternLength = pattern.length - 1;
|
|
|
|
let count = 0;
|
|
|
|
const processValues = async (keys): Promise<void> => {
|
|
|
|
const values = await this.client.MGET(keys);
|
|
|
|
for (let i = 0; i < values.length; i++) {
|
|
|
|
if (values[i]) {
|
|
|
|
result.push({ key: keys[i].slice(patternLength), value: JSON.parse(values[i]) });
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
logger.info(`loaded ${count} entries from Redis cache`);
|
|
|
|
};
|
|
|
|
for await (const key of this.client.scanIterator({
|
|
|
|
MATCH: pattern,
|
|
|
|
COUNT: 100
|
|
|
|
})) {
|
|
|
|
keys.push(key);
|
|
|
|
if (keys.length >= 10000) {
|
|
|
|
await processValues(keys);
|
|
|
|
keys = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (keys.length) {
|
|
|
|
await processValues(keys);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2023-05-12 16:29:45 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export default new RedisCache();
|