mirror of
https://github.com/mempool/mempool.git
synced 2025-02-22 14:22:44 +01:00
Merge pull request #4063 from mempool/mononaut/simple-redis
Simple Redis
This commit is contained in:
commit
36a26fc2ce
16 changed files with 752 additions and 151 deletions
|
@ -8,6 +8,7 @@
|
|||
"API_URL_PREFIX": "/api/v1/",
|
||||
"POLL_RATE_MS": 2000,
|
||||
"CACHE_DIR": "./cache",
|
||||
"CACHE_ENABLED": true,
|
||||
"CLEAR_PROTECTION_MINUTES": 20,
|
||||
"RECOMMENDED_FEE_PERCENTILE": 50,
|
||||
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||
|
|
158
backend/package-lock.json
generated
158
backend/package-lock.json
generated
|
@ -19,6 +19,7 @@
|
|||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.5.2",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.13.0"
|
||||
|
@ -1555,6 +1556,64 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
|
||||
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
|
||||
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/@redis/graph": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
|
||||
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/json": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
|
||||
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/search": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
|
||||
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/time-series": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
|
||||
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
|
@ -2718,6 +2777,14 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
|
@ -3678,6 +3745,14 @@
|
|||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/generic-pool": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
|
||||
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
@ -6577,6 +6652,19 @@
|
|||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
|
||||
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
|
||||
"dependencies": {
|
||||
"@redis/bloom": "1.2.0",
|
||||
"@redis/client": "1.5.7",
|
||||
"@redis/graph": "1.1.0",
|
||||
"@redis/json": "1.0.4",
|
||||
"@redis/search": "1.1.2",
|
||||
"@redis/time-series": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
@ -8704,6 +8792,53 @@
|
|||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@redis/bloom": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
|
||||
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/client": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz",
|
||||
"integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==",
|
||||
"requires": {
|
||||
"cluster-key-slot": "1.1.2",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@redis/graph": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
|
||||
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/json": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
|
||||
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/search": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
|
||||
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@redis/time-series": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
|
||||
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
|
||||
"requires": {}
|
||||
},
|
||||
"@sinclair/typebox": {
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
|
@ -9604,6 +9739,11 @@
|
|||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
|
@ -10332,6 +10472,11 @@
|
|||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"generic-pool": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
|
||||
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="
|
||||
},
|
||||
"gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
@ -12454,6 +12599,19 @@
|
|||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"redis": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz",
|
||||
"integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==",
|
||||
"requires": {
|
||||
"@redis/bloom": "1.2.0",
|
||||
"@redis/client": "1.5.7",
|
||||
"@redis/graph": "1.1.0",
|
||||
"@redis/json": "1.0.4",
|
||||
"@redis/search": "1.1.2",
|
||||
"@redis/time-series": "1.0.4"
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
|
@ -47,13 +47,14 @@
|
|||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.5.2",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.21.3",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||
"POLL_RATE_MS": 3,
|
||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||
"CACHE_ENABLED": true,
|
||||
"CLEAR_PROTECTION_MINUTES": 4,
|
||||
"RECOMMENDED_FEE_PERCENTILE": 5,
|
||||
"BLOCK_WEIGHT_UNITS": 6,
|
||||
|
@ -127,5 +128,9 @@
|
|||
"AUDIT": false,
|
||||
"AUDIT_START_HEIGHT": 774000,
|
||||
"SERVERS": []
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": false,
|
||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('Mempool Backend Config', () => {
|
|||
AUTOMATIC_BLOCK_REINDEXING: false,
|
||||
POLL_RATE_MS: 2000,
|
||||
CACHE_DIR: './cache',
|
||||
CACHE_ENABLED: true,
|
||||
CLEAR_PROTECTION_MINUTES: 20,
|
||||
RECOMMENDED_FEE_PERCENTILE: 50,
|
||||
BLOCK_WEIGHT_UNITS: 4000000,
|
||||
|
@ -127,6 +128,11 @@ describe('Mempool Backend Config', () => {
|
|||
AUDIT_START_HEIGHT: 774000,
|
||||
SERVERS: []
|
||||
});
|
||||
|
||||
expect(config.REDIS).toStrictEqual({
|
||||
ENABLED: false,
|
||||
UNIX_SOCKET_PATH: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -160,6 +166,8 @@ describe('Mempool Backend Config', () => {
|
|||
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
|
||||
|
||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||
|
||||
expect(config.REDIS).toStrictEqual(fixture.REDIS);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { IEsploraApi } from './esplora-api.interface';
|
|||
import blocks from '../blocks';
|
||||
import mempool from '../mempool';
|
||||
import { TransactionExtended } from '../../mempool.interfaces';
|
||||
import transactionUtils from '../transaction-utils';
|
||||
|
||||
class BitcoinApi implements AbstractBitcoinApi {
|
||||
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
|
||||
|
@ -63,9 +64,16 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
$getTransactionHex(txId: string): Promise<string> {
|
||||
return this.$getRawTransaction(txId, true)
|
||||
.then((tx) => tx.hex || '');
|
||||
async $getTransactionHex(txId: string): Promise<string> {
|
||||
const txInMempool = mempool.getMempool()[txId];
|
||||
if (txInMempool && txInMempool.hex) {
|
||||
return txInMempool.hex;
|
||||
}
|
||||
|
||||
return this.bitcoindClient.getRawTransaction(txId, true)
|
||||
.then((transaction: IBitcoinApi.Transaction) => {
|
||||
return transaction.hex;
|
||||
});
|
||||
}
|
||||
|
||||
$getBlockHeightTip(): Promise<number> {
|
||||
|
@ -209,7 +217,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
scriptpubkey: vout.scriptPubKey.hex,
|
||||
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
|
||||
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
|
||||
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
|
||||
scriptpubkey_asm: vout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
|
||||
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
|
||||
};
|
||||
});
|
||||
|
@ -219,7 +227,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
is_coinbase: !!vin.coinbase,
|
||||
prevout: null,
|
||||
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
|
||||
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
|
||||
scriptsig_asm: vin.scriptSig && transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) || '',
|
||||
sequence: vin.sequence,
|
||||
txid: vin.txid || '',
|
||||
vout: vin.vout || 0,
|
||||
|
@ -291,7 +299,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
}
|
||||
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
|
||||
vin.prevout = innerTx.vout[vin.vout];
|
||||
this.addInnerScriptsToVin(vin);
|
||||
transactionUtils.addInnerScriptsToVin(vin);
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
@ -330,7 +338,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
}
|
||||
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
|
||||
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
|
||||
this.addInnerScriptsToVin(transaction.vin[i]);
|
||||
transactionUtils.addInnerScriptsToVin(transaction.vin[i]);
|
||||
totalIn += innerTx.vout[transaction.vin[i].vout].value;
|
||||
}
|
||||
if (lazyPrevouts && transaction.vin.length > 12) {
|
||||
|
@ -342,122 +350,6 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||
return transaction;
|
||||
}
|
||||
|
||||
private convertScriptSigAsm(hex: string): string {
|
||||
const buf = Buffer.from(hex, 'hex');
|
||||
|
||||
const b: string[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < buf.length) {
|
||||
const op = buf[i];
|
||||
if (op >= 0x01 && op <= 0x4e) {
|
||||
i++;
|
||||
let push: number;
|
||||
if (op === 0x4c) {
|
||||
push = buf.readUInt8(i);
|
||||
b.push('OP_PUSHDATA1');
|
||||
i += 1;
|
||||
} else if (op === 0x4d) {
|
||||
push = buf.readUInt16LE(i);
|
||||
b.push('OP_PUSHDATA2');
|
||||
i += 2;
|
||||
} else if (op === 0x4e) {
|
||||
push = buf.readUInt32LE(i);
|
||||
b.push('OP_PUSHDATA4');
|
||||
i += 4;
|
||||
} else {
|
||||
push = op;
|
||||
b.push('OP_PUSHBYTES_' + push);
|
||||
}
|
||||
|
||||
const data = buf.slice(i, i + push);
|
||||
if (data.length !== push) {
|
||||
break;
|
||||
}
|
||||
|
||||
b.push(data.toString('hex'));
|
||||
i += data.length;
|
||||
} else {
|
||||
if (op === 0x00) {
|
||||
b.push('OP_0');
|
||||
} else if (op === 0x4f) {
|
||||
b.push('OP_PUSHNUM_NEG1');
|
||||
} else if (op === 0xb1) {
|
||||
b.push('OP_CLTV');
|
||||
} else if (op === 0xb2) {
|
||||
b.push('OP_CSV');
|
||||
} else if (op === 0xba) {
|
||||
b.push('OP_CHECKSIGADD');
|
||||
} else {
|
||||
const opcode = bitcoinjs.script.toASM([ op ]);
|
||||
if (opcode && op < 0xfd) {
|
||||
if (/^OP_(\d+)$/.test(opcode)) {
|
||||
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
|
||||
} else {
|
||||
b.push(opcode);
|
||||
}
|
||||
} else {
|
||||
b.push('OP_RETURN_' + op);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return b.join(' ');
|
||||
}
|
||||
|
||||
private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
|
||||
if (!vin.prevout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
||||
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
||||
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
|
||||
if (vin.witness && vin.witness.length > 2) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
|
||||
const witnessScript = this.witnessToP2TRScript(vin.witness);
|
||||
if (witnessScript !== null) {
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function must only be called when we know the witness we are parsing
|
||||
* is a taproot witness.
|
||||
* @param witness An array of hex strings that represents the witness stack of
|
||||
* the input.
|
||||
* @returns null if the witness is not a script spend, and the hex string of
|
||||
* the script item if it is a script spend.
|
||||
*/
|
||||
private witnessToP2TRScript(witness: string[]): string | null {
|
||||
if (witness.length < 2) return null;
|
||||
// Note: see BIP341 for parsing details of witness stack
|
||||
|
||||
// If there are at least two witness elements, and the first byte of the
|
||||
// last element is 0x50, this last element is called annex a and
|
||||
// is removed from the witness stack.
|
||||
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
|
||||
// If there are at least two witness elements left, script path spending is used.
|
||||
// Call the second-to-last stack element s, the script.
|
||||
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
|
||||
if (hasAnnex && witness.length < 3) return null;
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BitcoinApi;
|
||||
|
|
|
@ -26,6 +26,8 @@ import PricesRepository from '../repositories/PricesRepository';
|
|||
import priceUpdater from '../tasks/price-updater';
|
||||
import chainTips from './chain-tips';
|
||||
import websocketHandler from './websocket-handler';
|
||||
import redisCache from './redis-cache';
|
||||
import rbfCache from './rbf-cache';
|
||||
|
||||
class Blocks {
|
||||
private blocks: BlockExtended[] = [];
|
||||
|
@ -804,10 +806,18 @@ class Blocks {
|
|||
if (this.newBlockCallbacks.length) {
|
||||
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
||||
}
|
||||
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
|
||||
if (config.MEMPOOL.CACHE_ENABLED && !memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
|
||||
diskCache.$saveCacheToDisk();
|
||||
}
|
||||
|
||||
// Update Redis cache
|
||||
if (config.REDIS.ENABLED) {
|
||||
await redisCache.$updateBlocks(this.blocks);
|
||||
await redisCache.$updateBlockSummaries(this.blockSummaries);
|
||||
await redisCache.$removeTransactions(txIds);
|
||||
await rbfCache.updateCache();
|
||||
}
|
||||
|
||||
handledBlocks++;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class DiskCache {
|
|||
};
|
||||
|
||||
constructor() {
|
||||
if (!cluster.isPrimary) {
|
||||
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
process.on('SIGINT', (e) => {
|
||||
|
@ -39,7 +39,7 @@ class DiskCache {
|
|||
}
|
||||
|
||||
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
|
||||
if (!cluster.isPrimary) {
|
||||
if (!cluster.isPrimary || !config.MEMPOOL.CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (this.isWritingCache) {
|
||||
|
@ -175,10 +175,11 @@ class DiskCache {
|
|||
}
|
||||
|
||||
async $loadMempoolCache(): Promise<void> {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
if (!config.MEMPOOL.CACHE_ENABLED || !fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const start = Date.now();
|
||||
let data: any = {};
|
||||
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
||||
if (cacheData) {
|
||||
|
@ -220,6 +221,8 @@ class DiskCache {
|
|||
}
|
||||
}
|
||||
|
||||
logger.info(`Loaded mempool from disk cache in ${Date.now() - start} ms`);
|
||||
|
||||
await memPool.$setMempool(data.mempool);
|
||||
if (!this.ignoreBlocksCache) {
|
||||
blocks.setBlocks(data.blocks);
|
||||
|
|
|
@ -9,7 +9,7 @@ import loadingIndicators from './loading-indicators';
|
|||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||
import redisCache from './redis-cache';
|
||||
|
||||
class Mempool {
|
||||
private inSync: boolean = false;
|
||||
|
@ -86,6 +86,10 @@ class Mempool {
|
|||
public async $setMempool(mempoolData: { [txId: string]: MempoolTransactionExtended }) {
|
||||
this.mempoolCache = mempoolData;
|
||||
let count = 0;
|
||||
const redisTimer = Date.now();
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
logger.debug(`Migrating ${Object.keys(this.mempoolCache).length} transactions from disk cache to Redis cache`);
|
||||
}
|
||||
for (const txid of Object.keys(this.mempoolCache)) {
|
||||
if (!this.mempoolCache[txid].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) {
|
||||
this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]);
|
||||
|
@ -94,6 +98,13 @@ class Mempool {
|
|||
this.mempoolCache[txid].order = transactionUtils.txidToOrdering(txid);
|
||||
}
|
||||
count++;
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
await redisCache.$addTransaction(this.mempoolCache[txid]);
|
||||
}
|
||||
}
|
||||
if (config.MEMPOOL.CACHE_ENABLED && config.REDIS.ENABLED) {
|
||||
await redisCache.$flushTransactions();
|
||||
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
|
||||
}
|
||||
if (this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||
|
@ -138,8 +149,8 @@ class Mempool {
|
|||
logger.err('failed to fetch bulk mempool transactions from esplora');
|
||||
}
|
||||
}
|
||||
return newTransactions;
|
||||
logger.info(`Done inserting loaded mempool transactions into local cache`);
|
||||
return newTransactions;
|
||||
}
|
||||
|
||||
public async $updateMemPoolInfo() {
|
||||
|
@ -208,6 +219,11 @@ class Mempool {
|
|||
logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`);
|
||||
try {
|
||||
newTransactions = await this.$reloadMempool(transactions.length);
|
||||
if (config.REDIS.ENABLED) {
|
||||
for (const tx of newTransactions) {
|
||||
await redisCache.$addTransaction(tx);
|
||||
}
|
||||
}
|
||||
loaded = true;
|
||||
} catch (e) {
|
||||
logger.err('failed to load mempool in bulk from esplora, falling back to fetching individual transactions');
|
||||
|
@ -230,6 +246,10 @@ class Mempool {
|
|||
}
|
||||
hasChange = true;
|
||||
newTransactions.push(transaction);
|
||||
|
||||
if (config.REDIS.ENABLED) {
|
||||
await redisCache.$addTransaction(transaction);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
|
||||
this.missingTxCount++;
|
||||
|
@ -318,6 +338,13 @@ class Mempool {
|
|||
loadingIndicators.setProgress('mempool', 100);
|
||||
}
|
||||
|
||||
// Update Redis cache
|
||||
if (config.REDIS.ENABLED) {
|
||||
await redisCache.$flushTransactions();
|
||||
await redisCache.$removeTransactions(deletedTransactions.map(tx => tx.txid));
|
||||
await rbfCache.updateCache();
|
||||
}
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import config from "../config";
|
||||
import logger from "../logger";
|
||||
import { MempoolTransactionExtended, TransactionStripped } from "../mempool.interfaces";
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { Common } from "./common";
|
||||
import redisCache from "./redis-cache";
|
||||
|
||||
interface RbfTransaction extends TransactionStripped {
|
||||
export interface RbfTransaction extends TransactionStripped {
|
||||
rbf?: boolean;
|
||||
mined?: boolean;
|
||||
fullRbf?: boolean;
|
||||
}
|
||||
|
||||
interface RbfTree {
|
||||
export interface RbfTree {
|
||||
tx: RbfTransaction;
|
||||
time: number;
|
||||
interval?: number;
|
||||
|
@ -28,6 +30,19 @@ export interface ReplacementInfo {
|
|||
newVsize: number;
|
||||
}
|
||||
|
||||
enum CacheOp {
|
||||
Remove = 0,
|
||||
Add = 1,
|
||||
Change = 2,
|
||||
}
|
||||
|
||||
interface CacheEvent {
|
||||
op: CacheOp;
|
||||
type: 'tx' | 'tree' | 'exp';
|
||||
txid: string,
|
||||
value?: any,
|
||||
}
|
||||
|
||||
class RbfCache {
|
||||
private replacedBy: Map<string, string> = new Map();
|
||||
private replaces: Map<string, string[]> = new Map();
|
||||
|
@ -36,11 +51,43 @@ class RbfCache {
|
|||
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
|
||||
private txs: Map<string, MempoolTransactionExtended> = new Map();
|
||||
private expiring: Map<string, number> = new Map();
|
||||
private cacheQueue: CacheEvent[] = [];
|
||||
|
||||
constructor() {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||
return;
|
||||
|
@ -49,7 +96,7 @@ class RbfCache {
|
|||
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
|
||||
const newTime = newTxExtended.firstSeen || (Date.now() / 1000);
|
||||
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
|
||||
this.txs.set(newTx.txid, newTxExtended);
|
||||
this.addTx(newTx.txid, newTxExtended);
|
||||
|
||||
// maintain rbf trees
|
||||
let txFullRbf = false;
|
||||
|
@ -66,7 +113,7 @@ class RbfCache {
|
|||
const treeId = this.treeMap.get(replacedTx.txid);
|
||||
if (treeId) {
|
||||
const tree = this.rbfTrees.get(treeId);
|
||||
this.rbfTrees.delete(treeId);
|
||||
this.removeTree(treeId);
|
||||
if (tree) {
|
||||
tree.interval = newTime - tree?.time;
|
||||
replacedTrees.push(tree);
|
||||
|
@ -83,7 +130,7 @@ class RbfCache {
|
|||
replaces: [],
|
||||
});
|
||||
treeFullRbf = treeFullRbf || !replacedTx.rbf;
|
||||
this.txs.set(replacedTx.txid, replacedTxExtended);
|
||||
this.addTx(replacedTx.txid, replacedTxExtended);
|
||||
}
|
||||
}
|
||||
newTx.fullRbf = txFullRbf;
|
||||
|
@ -94,10 +141,9 @@ class RbfCache {
|
|||
fullRbf: treeFullRbf,
|
||||
replaces: replacedTrees
|
||||
};
|
||||
this.rbfTrees.set(treeId, newTree);
|
||||
this.addTree(treeId, newTree);
|
||||
this.updateTreeMap(treeId, newTree);
|
||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||
this.dirtyTrees.add(treeId);
|
||||
}
|
||||
|
||||
public has(txId: string): boolean {
|
||||
|
@ -191,6 +237,7 @@ class RbfCache {
|
|||
this.setTreeMined(tree, txid);
|
||||
tree.mined = true;
|
||||
this.dirtyTrees.add(treeId);
|
||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||
}
|
||||
}
|
||||
this.evict(txid);
|
||||
|
@ -199,7 +246,8 @@ class RbfCache {
|
|||
// flag a transaction as removed from the mempool
|
||||
public evict(txid: string, fast: boolean = false): void {
|
||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||
this.expiring.set(txid, fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400)); // 24 hours
|
||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||
this.addExpiration(txid, expiryTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,11 +268,11 @@ class RbfCache {
|
|||
const now = Date.now();
|
||||
for (const txid of this.expiring.keys()) {
|
||||
if ((this.expiring.get(txid) || 0) < now) {
|
||||
this.expiring.delete(txid);
|
||||
this.removeExpiration(txid);
|
||||
this.remove(txid);
|
||||
}
|
||||
}
|
||||
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.expiring.size} due to expire`);
|
||||
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.rbfTrees.size} trees, ${this.expiring.size} due to expire`);
|
||||
}
|
||||
|
||||
// remove a transaction & all previous versions from the cache
|
||||
|
@ -234,14 +282,14 @@ class RbfCache {
|
|||
const replaces = this.replaces.get(txid);
|
||||
this.replaces.delete(txid);
|
||||
this.treeMap.delete(txid);
|
||||
this.txs.delete(txid);
|
||||
this.expiring.delete(txid);
|
||||
this.removeTx(txid);
|
||||
this.removeExpiration(txid);
|
||||
for (const tx of (replaces || [])) {
|
||||
// recursively remove prior versions from the cache
|
||||
this.replacedBy.delete(tx);
|
||||
// if this is the id of a tree, remove that too
|
||||
if (this.treeMap.get(tx) === tx) {
|
||||
this.rbfTrees.delete(tx);
|
||||
this.removeTree(tx);
|
||||
}
|
||||
this.remove(tx);
|
||||
}
|
||||
|
@ -273,6 +321,33 @@ class RbfCache {
|
|||
}
|
||||
}
|
||||
|
||||
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 = [];
|
||||
}
|
||||
|
||||
public dump(): any {
|
||||
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
|
||||
|
||||
|
@ -285,14 +360,14 @@ class RbfCache {
|
|||
|
||||
public async load({ txs, trees, expiring }): Promise<void> {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry[0], txEntry[1]);
|
||||
this.txs.set(txEntry.key, txEntry.value);
|
||||
});
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry[0])) {
|
||||
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
||||
}
|
||||
});
|
||||
this.cleanup();
|
||||
|
@ -378,8 +453,7 @@ class RbfCache {
|
|||
};
|
||||
this.treeMap.set(txid, root);
|
||||
if (root === txid) {
|
||||
this.rbfTrees.set(root, tree);
|
||||
this.dirtyTrees.add(root);
|
||||
this.addTree(root, tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
|
275
backend/src/api/redis-cache.ts
Normal file
275
backend/src/api/redis-cache.ts
Normal file
|
@ -0,0 +1,275 @@
|
|||
import { createClient } from 'redis';
|
||||
import memPool from './mempool';
|
||||
import blocks from './blocks';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
import { BlockExtended, BlockSummary, MempoolTransactionExtended } from '../mempool.interfaces';
|
||||
import rbfCache from './rbf-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
|
||||
enum NetworkDB {
|
||||
mainnet = 0,
|
||||
testnet,
|
||||
signet,
|
||||
liquid,
|
||||
liquidtestnet,
|
||||
}
|
||||
|
||||
class RedisCache {
|
||||
private client;
|
||||
private connected = false;
|
||||
private schemaVersion = 1;
|
||||
|
||||
private cacheQueue: MempoolTransactionExtended[] = [];
|
||||
private txFlushLimit: number = 10000;
|
||||
|
||||
constructor() {
|
||||
if (config.REDIS.ENABLED) {
|
||||
const redisConfig = {
|
||||
socket: {
|
||||
path: config.REDIS.UNIX_SOCKET_PATH
|
||||
},
|
||||
database: NetworkDB[config.MEMPOOL.NETWORK],
|
||||
};
|
||||
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) {
|
||||
return this.client.connect().then(async () => {
|
||||
this.connected = true;
|
||||
logger.info(`Redis client connected`);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $updateBlocks(blocks: BlockExtended[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set('blocks', JSON.stringify(blocks));
|
||||
logger.debug(`Saved latest blocks to Redis cache`);
|
||||
} 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();
|
||||
await this.client.set('block-summaries', JSON.stringify(summaries));
|
||||
logger.debug(`Saved latest block summaries to Redis cache`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to update block summaries in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $addTransaction(tx: MempoolTransactionExtended) {
|
||||
this.cacheQueue.push(tx);
|
||||
if (this.cacheQueue.length >= this.txFlushLimit) {
|
||||
await this.$flushTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
async $flushTransactions() {
|
||||
const success = await this.$addTransactions(this.cacheQueue);
|
||||
if (success) {
|
||||
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
this.cacheQueue = [];
|
||||
} else {
|
||||
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
||||
if (!newTransactions.length) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const msetData = newTransactions.map(tx => {
|
||||
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;
|
||||
}
|
||||
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
|
||||
});
|
||||
await this.client.MSET(msetData);
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async $removeTransactions(transactions: string[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
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}`));
|
||||
logger.info(`Deleted ${transactions.length} transactions from the Redis cache`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
async $getBlocks(): Promise<BlockExtended[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const json = await this.client.get('blocks');
|
||||
return JSON.parse(json);
|
||||
} 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();
|
||||
const json = await this.client.get('block-summaries');
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
|
||||
const start = Date.now();
|
||||
const mempool = {};
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
|
||||
for (const tx of mempoolList) {
|
||||
mempool[tx.key] = tx.value;
|
||||
}
|
||||
logger.info(`Loaded mempool from Redis cache in ${Date.now() - start} ms`);
|
||||
return mempool || {};
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async $getRbfEntries(type: string): Promise<any[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
|
||||
return rbfEntries;
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
this.inflateLoadedTxs(loadedMempool);
|
||||
// Load rbf data
|
||||
const rbfTxs = await this.$getRbfEntries('tx');
|
||||
const rbfTrees = await this.$getRbfEntries('tree');
|
||||
const rbfExpirations = await this.$getRbfEntries('exp');
|
||||
|
||||
// Set loaded data
|
||||
blocks.setBlocks(loadedBlocks || []);
|
||||
blocks.setBlockSummaries(loadedBlockSummaries || []);
|
||||
await memPool.$setMempool(loadedMempool);
|
||||
await rbfCache.load({
|
||||
txs: rbfTxs,
|
||||
trees: rbfTrees.map(loadedTree => loadedTree.value),
|
||||
expiring: rbfExpirations,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export default new RedisCache();
|
|
@ -188,6 +188,122 @@ class TransactionUtils {
|
|||
16
|
||||
);
|
||||
}
|
||||
|
||||
public addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
|
||||
if (!vin.prevout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
||||
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
||||
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
|
||||
if (vin.witness && vin.witness.length > 2) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
|
||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) {
|
||||
const witnessScript = this.witnessToP2TRScript(vin.witness);
|
||||
if (witnessScript !== null) {
|
||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public convertScriptSigAsm(hex: string): string {
|
||||
const buf = Buffer.from(hex, 'hex');
|
||||
|
||||
const b: string[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < buf.length) {
|
||||
const op = buf[i];
|
||||
if (op >= 0x01 && op <= 0x4e) {
|
||||
i++;
|
||||
let push: number;
|
||||
if (op === 0x4c) {
|
||||
push = buf.readUInt8(i);
|
||||
b.push('OP_PUSHDATA1');
|
||||
i += 1;
|
||||
} else if (op === 0x4d) {
|
||||
push = buf.readUInt16LE(i);
|
||||
b.push('OP_PUSHDATA2');
|
||||
i += 2;
|
||||
} else if (op === 0x4e) {
|
||||
push = buf.readUInt32LE(i);
|
||||
b.push('OP_PUSHDATA4');
|
||||
i += 4;
|
||||
} else {
|
||||
push = op;
|
||||
b.push('OP_PUSHBYTES_' + push);
|
||||
}
|
||||
|
||||
const data = buf.slice(i, i + push);
|
||||
if (data.length !== push) {
|
||||
break;
|
||||
}
|
||||
|
||||
b.push(data.toString('hex'));
|
||||
i += data.length;
|
||||
} else {
|
||||
if (op === 0x00) {
|
||||
b.push('OP_0');
|
||||
} else if (op === 0x4f) {
|
||||
b.push('OP_PUSHNUM_NEG1');
|
||||
} else if (op === 0xb1) {
|
||||
b.push('OP_CLTV');
|
||||
} else if (op === 0xb2) {
|
||||
b.push('OP_CSV');
|
||||
} else if (op === 0xba) {
|
||||
b.push('OP_CHECKSIGADD');
|
||||
} else {
|
||||
const opcode = bitcoinjs.script.toASM([ op ]);
|
||||
if (opcode && op < 0xfd) {
|
||||
if (/^OP_(\d+)$/.test(opcode)) {
|
||||
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
|
||||
} else {
|
||||
b.push(opcode);
|
||||
}
|
||||
} else {
|
||||
b.push('OP_RETURN_' + op);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return b.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function must only be called when we know the witness we are parsing
|
||||
* is a taproot witness.
|
||||
* @param witness An array of hex strings that represents the witness stack of
|
||||
* the input.
|
||||
* @returns null if the witness is not a script spend, and the hex string of
|
||||
* the script item if it is a script spend.
|
||||
*/
|
||||
public witnessToP2TRScript(witness: string[]): string | null {
|
||||
if (witness.length < 2) return null;
|
||||
// Note: see BIP341 for parsing details of witness stack
|
||||
|
||||
// If there are at least two witness elements, and the first byte of the
|
||||
// last element is 0x50, this last element is called annex a and
|
||||
// is removed from the witness stack.
|
||||
const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50';
|
||||
// If there are at least two witness elements left, script path spending is used.
|
||||
// Call the second-to-last stack element s, the script.
|
||||
// (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack)
|
||||
if (hasAnnex && witness.length < 3) return null;
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
}
|
||||
|
||||
export default new TransactionUtils();
|
||||
|
|
|
@ -12,6 +12,7 @@ interface IConfig {
|
|||
API_URL_PREFIX: string;
|
||||
POLL_RATE_MS: number;
|
||||
CACHE_DIR: string;
|
||||
CACHE_ENABLED: boolean;
|
||||
CLEAR_PROTECTION_MINUTES: number;
|
||||
RECOMMENDED_FEE_PERCENTILE: number;
|
||||
BLOCK_WEIGHT_UNITS: number;
|
||||
|
@ -137,7 +138,11 @@ interface IConfig {
|
|||
AUDIT: boolean;
|
||||
AUDIT_START_HEIGHT: number;
|
||||
SERVERS: string[];
|
||||
}
|
||||
},
|
||||
REDIS: {
|
||||
ENABLED: boolean;
|
||||
UNIX_SOCKET_PATH: string;
|
||||
},
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
|
@ -150,6 +155,7 @@ const defaults: IConfig = {
|
|||
'API_URL_PREFIX': '/api/v1/',
|
||||
'POLL_RATE_MS': 2000,
|
||||
'CACHE_DIR': './cache',
|
||||
'CACHE_ENABLED': true,
|
||||
'CLEAR_PROTECTION_MINUTES': 20,
|
||||
'RECOMMENDED_FEE_PERCENTILE': 50,
|
||||
'BLOCK_WEIGHT_UNITS': 4000000,
|
||||
|
@ -275,7 +281,11 @@ const defaults: IConfig = {
|
|||
'AUDIT': false,
|
||||
'AUDIT_START_HEIGHT': 774000,
|
||||
'SERVERS': [],
|
||||
}
|
||||
},
|
||||
'REDIS': {
|
||||
'ENABLED': false,
|
||||
'UNIX_SOCKET_PATH': '',
|
||||
},
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
|
@ -296,6 +306,7 @@ class Config implements IConfig {
|
|||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||
MAXMIND: IConfig['MAXMIND'];
|
||||
REPLICATION: IConfig['REPLICATION'];
|
||||
REDIS: IConfig['REDIS'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFromFile, defaults);
|
||||
|
@ -316,6 +327,7 @@ class Config implements IConfig {
|
|||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||
this.MAXMIND = configs.MAXMIND;
|
||||
this.REPLICATION = configs.REPLICATION;
|
||||
this.REDIS = configs.REDIS;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
|
|
|
@ -41,6 +41,7 @@ import chainTips from './api/chain-tips';
|
|||
import { AxiosError } from 'axios';
|
||||
import v8 from 'v8';
|
||||
import { formatBytes, getBytesUnit } from './utils/format';
|
||||
import redisCache from './api/redis-cache';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
|
@ -122,7 +123,11 @@ class Server {
|
|||
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
||||
await syncAssets.syncAssets$();
|
||||
if (config.MEMPOOL.ENABLED) {
|
||||
if (config.MEMPOOL.CACHE_ENABLED) {
|
||||
await diskCache.$loadMempoolCache();
|
||||
} else if (config.REDIS.ENABLED) {
|
||||
await redisCache.$loadCache();
|
||||
}
|
||||
}
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||
"CACHE_ENABLED": __MEMPOOL_CACHE_ENABLED__,
|
||||
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
|
||||
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
|
||||
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
|
||||
|
@ -133,5 +134,9 @@
|
|||
"AUDIT": __REPLICATION_AUDIT__,
|
||||
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||
"SERVERS": __REPLICATION_SERVERS__
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": __REDIS_ENABLED__,
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
|||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
||||
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
||||
__MEMPOOL_CACHE_ENABLED__=${MEMPOOL_CACHE_ENABLED:=true}
|
||||
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
|
||||
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
|
||||
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
|
||||
|
@ -136,6 +137,9 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
|||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||
|
||||
# REDIS
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
|
@ -147,6 +151,7 @@ sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g"
|
|||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CACHE_ENABLED__!${__MEMPOOL_CACHE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
|
||||
|
@ -262,4 +267,8 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
|||
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||
|
||||
# REDIS
|
||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
|
Loading…
Add table
Reference in a new issue