From d1ed7f4429d5899c568be91bc7d6abd047c48bbe Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 27 Jan 2026 00:54:56 +0000 Subject: [PATCH] [liquid] use bitcoin esplora for peg parsing queries --- backend/mempool-config.sample.json | 3 + backend/src/api/bitcoin/esplora-second-api.ts | 89 +++++++++++++++++++ backend/src/api/liquid/elements-parser.ts | 38 +++++--- backend/src/config.ts | 12 +++ production/mempool-config.liquid.json | 3 + production/mempool-config.liquidtestnet.json | 3 + 6 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 backend/src/api/bitcoin/esplora-second-api.ts diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index c2715153b..923e23d71 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -64,6 +64,9 @@ "FALLBACK": [], "MAX_BEHIND_TIP": 2 }, + "SECOND_ESPLORA": { + "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet" + }, "SECOND_CORE_RPC": { "HOST": "127.0.0.1", "PORT": 8332, diff --git a/backend/src/api/bitcoin/esplora-second-api.ts b/backend/src/api/bitcoin/esplora-second-api.ts new file mode 100644 index 000000000..069eec5e4 --- /dev/null +++ b/backend/src/api/bitcoin/esplora-second-api.ts @@ -0,0 +1,89 @@ +import config from '../../config'; +import axios from 'axios'; +import http from 'http'; +import { IEsploraApi } from './esplora-api.interface'; +import logger from '../../logger'; + +class EsploraSecondApi { + private requestConnection = axios.create({ + httpAgent: new http.Agent({ keepAlive: true }) + }); + + private async $query(method: 'get' | 'post', path: string, data?: any, responseType = 'json'): Promise { + let axiosConfig: any; + let url: string; + + if (config.SECOND_ESPLORA.UNIX_SOCKET_PATH) { + axiosConfig = { socketPath: config.SECOND_ESPLORA.UNIX_SOCKET_PATH, timeout: config.SECOND_ESPLORA.REQUEST_TIMEOUT, responseType }; + url = 'http://api' + path; + } else { + axiosConfig = { timeout: config.SECOND_ESPLORA.REQUEST_TIMEOUT, responseType }; + url = config.SECOND_ESPLORA.REST_API_URL + path; + } + + try { + const response = method === 'post' + ? await this.requestConnection.post(url, data, axiosConfig) + : await this.requestConnection.get(url, axiosConfig); + return response.data; + } catch (e: any) { + logger.warn(`Second esplora request failed: ${url}`); + logger.warn(e instanceof Error ? e.message : e); + throw e; + } + } + + async $getRawTransaction(txId: string): Promise { + return this.$query('get', '/tx/' + txId); + } + + async $getTransactionHex(txId: string): Promise { + return this.$query('get', '/tx/' + txId + '/hex'); + } + + async $getBlockHeightTip(): Promise { + return this.$query('get', '/blocks/tip/height'); + } + + async $getBlockHashTip(): Promise { + return this.$query('get', '/blocks/tip/hash'); + } + + async $getBlockHash(height: number): Promise { + return this.$query('get', '/block-height/' + height); + } + + async $getBlock(hash: string): Promise { + return this.$query('get', '/block/' + hash); + } + + async $getBlockHeader(hash: string): Promise { + return this.$query('get', '/block/' + hash + '/header'); + } + + async $getTxIdsForBlock(hash: string): Promise { + return this.$query('get', '/block/' + hash + '/txids'); + } + + async $getTxsForBlock(hash: string): Promise { + return this.$query('get', '/internal/block/' + hash + '/txs'); + } + + async $getAddress(address: string): Promise { + return this.$query('get', '/address/' + address); + } + + async $getAddressUtxos(address: string): Promise { + return this.$query('get', '/address/' + address + '/utxo'); + } + + async $getOutspend(txId: string, vout: number): Promise { + return this.$query('get', '/tx/' + txId + '/outspend/' + vout); + } + + async $getOutspends(txId: string): Promise { + return this.$query('get', '/tx/' + txId + '/outspends'); + } +} + +export default new EsploraSecondApi(); diff --git a/backend/src/api/liquid/elements-parser.ts b/backend/src/api/liquid/elements-parser.ts index 05717c132..71e1600b0 100644 --- a/backend/src/api/liquid/elements-parser.ts +++ b/backend/src/api/liquid/elements-parser.ts @@ -1,9 +1,11 @@ import { IBitcoinApi } from '../bitcoin/bitcoin-api.interface'; import bitcoinClient from '../bitcoin/bitcoin-client'; import bitcoinSecondClient from '../bitcoin/bitcoin-second-client'; +import bitcoinEsploraApi from '../bitcoin/esplora-second-api'; import { Common } from '../common'; import DB from '../../database'; import logger from '../../logger'; +import { IEsploraApi } from '../bitcoin/esplora-api.interface'; const federationChangeAddresses = ['bc1qxvay4an52gcghxq5lavact7r6qe9l4laedsazz8fj2ee2cy47tlqff4aj4', '3EiAcrzq1cELXScc98KeCswGWZaPGceT1d', '3G6neksSBMp51kHJ2if8SeDUrzT8iVETWT', 'bc1qwnevjp8nsq7adu3hxlvdvslrf242q4vuavfg0y929jp2zntp3vgq7cq6z2']; const auditBlockOffsetWithTip = 1; // Wait for 1 block confirmation before processing the block in the audit process to reduce the risk of reorgs @@ -52,12 +54,18 @@ class ElementsParser { } protected async $parsePegIn(input: IBitcoinApi.Vin, vindex: number, txid: string, block: IBitcoinApi.Block) { - const bitcoinTx: IBitcoinApi.Transaction = await bitcoinSecondClient.getRawTransaction(input.txid, true); - const bitcoinBlock: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(bitcoinTx.blockhash); - const prevout = bitcoinTx.vout[input.vout || 0]; - const outputAddress = prevout.scriptPubKey.address || (prevout.scriptPubKey.addresses && prevout.scriptPubKey.addresses[0]) || ''; - await this.$savePegToDatabase(block.height, block.time, prevout.value * 100000000, txid, vindex, - outputAddress, bitcoinTx.txid, prevout.n, bitcoinBlock.height, bitcoinBlock.time, 1); + if (!input.txid || input.vout == null) { + return; + } + const bitcoinTx: IEsploraApi.Transaction = await bitcoinEsploraApi.$getRawTransaction(input.txid); + if (!bitcoinTx.status.block_hash) { + return; + } + const bitcoinBlock: IEsploraApi.Block = await bitcoinEsploraApi.$getBlock(bitcoinTx.status.block_hash); + const prevout = bitcoinTx.vout[input.vout]; + const outputAddress = prevout.scriptpubkey_address || ''; + await this.$savePegToDatabase(bitcoinBlock.height, bitcoinBlock.timestamp, prevout.value * 100000000, txid, vindex, + outputAddress, bitcoinTx.txid, input.vout, bitcoinBlock.height, bitcoinBlock.timestamp, 1); } protected async $parseOutputs(tx: IBitcoinApi.Transaction, block: IBitcoinApi.Block) { @@ -184,7 +192,7 @@ class ElementsParser { } // The slow way: parse the block to look for the spending tx - const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit); + const blockHash: string = await bitcoinEsploraApi.$getBlockHash(auditProgress.lastBlockAudit); const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2); await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses); @@ -217,8 +225,12 @@ class ElementsParser { const unspentAsTip: any[] = []; for (const utxo of utxos) { - const result = await bitcoinSecondClient.getTxOut(utxo.txid, utxo.txindex, false); - result ? unspentAsTip.push(utxo) : spentAsTip.push(utxo); + const outspend = await bitcoinEsploraApi.$getOutspend(utxo.txid, utxo.txindex); + if (outspend?.spent) { + spentAsTip.push(utxo); + } else { + unspentAsTip.push(utxo); + } } return {spentAsTip, unspentAsTip}; @@ -323,10 +335,10 @@ class ElementsParser { // Get the bitcoin block where the audit process was last updated protected async $getAuditProgress(): Promise { const lastblockaudit = await this.$getLastBlockAudit(); - const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState(); + const bitcoinHeight = await bitcoinEsploraApi.$getBlockHeightTip(); return { lastBlockAudit: lastblockaudit, - confirmedTip: bitcoinBlocksToSync.bitcoinBlocks - auditBlockOffsetWithTip, + confirmedTip: bitcoinHeight - auditBlockOffsetWithTip, }; } @@ -393,7 +405,7 @@ class ElementsParser { public async $getCurrentLbtcSupply(): Promise { const [rows] = await DB.query(`SELECT SUM(amount) AS LBTC_supply FROM elements_pegs;`); const lastblockupdate = await this.$getLatestBlockHeightFromDatabase(); - const hash = await bitcoinClient.getBlockHash(lastblockupdate); + const hash = await bitcoinEsploraApi.$getBlockHash(lastblockupdate); return { amount: rows[0]['LBTC_supply'], lastBlockUpdate: lastblockupdate, @@ -405,7 +417,7 @@ class ElementsParser { public async $getCurrentFederationReserves(): Promise { const [rows] = await DB.query(`SELECT SUM(amount) AS total_balance FROM federation_txos WHERE unspent = 1 AND expiredAt = 0;`); const lastblockaudit = await this.$getLastBlockAudit(); - const hash = await bitcoinSecondClient.getBlockHash(lastblockaudit); + const hash = await bitcoinEsploraApi.$getBlockHash(lastblockaudit); return { amount: rows[0]['total_balance'], lastBlockUpdate: lastblockaudit, diff --git a/backend/src/config.ts b/backend/src/config.ts index f7f8b371b..f8d475e69 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -54,6 +54,11 @@ interface IConfig { FALLBACK: string[]; MAX_BEHIND_TIP: number; }; + SECOND_ESPLORA: { + REST_API_URL: string; + UNIX_SOCKET_PATH: string | void | null; + REQUEST_TIMEOUT: number; + }; LIGHTNING: { ENABLED: boolean; BACKEND: 'lnd' | 'cln' | 'ldk'; @@ -225,6 +230,11 @@ const defaults: IConfig = { 'FALLBACK': [], 'MAX_BEHIND_TIP': 2, }, + 'SECOND_ESPLORA': { + 'REST_API_URL': 'http://127.0.0.1:3000', + 'UNIX_SOCKET_PATH': null, + 'REQUEST_TIMEOUT': 10000, + }, 'ELECTRUM': { 'HOST': '127.0.0.1', 'PORT': 3306, @@ -347,6 +357,7 @@ const defaults: IConfig = { class Config implements IConfig { MEMPOOL: IConfig['MEMPOOL']; ESPLORA: IConfig['ESPLORA']; + SECOND_ESPLORA: IConfig['SECOND_ESPLORA']; ELECTRUM: IConfig['ELECTRUM']; CORE_RPC: IConfig['CORE_RPC']; SECOND_CORE_RPC: IConfig['SECOND_CORE_RPC']; @@ -370,6 +381,7 @@ class Config implements IConfig { const configs = this.merge(configFromFile, defaults); this.MEMPOOL = configs.MEMPOOL; this.ESPLORA = configs.ESPLORA; + this.SECOND_ESPLORA = configs.SECOND_ESPLORA; this.ELECTRUM = configs.ELECTRUM; this.CORE_RPC = configs.CORE_RPC; this.SECOND_CORE_RPC = configs.SECOND_CORE_RPC; diff --git a/production/mempool-config.liquid.json b/production/mempool-config.liquid.json index 316f99305..f85cd69d6 100644 --- a/production/mempool-config.liquid.json +++ b/production/mempool-config.liquid.json @@ -70,6 +70,9 @@ "http://node206.tk7.mempool.space:3001" ] }, + "SECOND_ESPLORA": { + "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet" + }, "DATABASE": { "ENABLED": true, "HOST": "127.0.0.1", diff --git a/production/mempool-config.liquidtestnet.json b/production/mempool-config.liquidtestnet.json index e42bbadd6..93ea09cbe 100644 --- a/production/mempool-config.liquidtestnet.json +++ b/production/mempool-config.liquidtestnet.json @@ -70,6 +70,9 @@ "http://node206.tk7.mempool.space:3004" ] }, + "SECOND_ESPLORA": { + "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet" + }, "DATABASE": { "ENABLED": true, "HOST": "127.0.0.1",