diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 47ec6898a..00fe95cc5 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -50,7 +50,8 @@ "ESPLORA": { "REST_API_URL": "http://127.0.0.1:3000", "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet", - "RETRY_UNIX_SOCKET_AFTER": 30000 + "RETRY_UNIX_SOCKET_AFTER": 30000, + "FALLBACK": [] }, "SECOND_CORE_RPC": { "HOST": "127.0.0.1", diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index 658b1a6c2..1b6c8d411 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -51,7 +51,8 @@ "ESPLORA": { "REST_API_URL": "__ESPLORA_REST_API_URL__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", - "RETRY_UNIX_SOCKET_AFTER": 888 + "RETRY_UNIX_SOCKET_AFTER": 888, + "FALLBACK": [] }, "SECOND_CORE_RPC": { "HOST": "__SECOND_CORE_RPC_HOST__", diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index 23ad0e4a6..655898917 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -52,7 +52,12 @@ describe('Mempool Backend Config', () => { expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); - expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 }); + expect(config.ESPLORA).toStrictEqual({ + REST_API_URL: 'http://127.0.0.1:3000', + UNIX_SOCKET_PATH: null, + RETRY_UNIX_SOCKET_AFTER: 30000, + FALLBACK: [], + }); expect(config.CORE_RPC).toStrictEqual({ HOST: '127.0.0.1', diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index f14c5525d..c44653a3d 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -23,6 +23,8 @@ export interface AbstractBitcoinApi { $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; $getBatchedOutspends(txId: string[]): Promise; + + startHealthChecks(): void; } export interface BitcoinRpcCredentials { host: string; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index b315ed0f7..807baae2e 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -355,6 +355,7 @@ class BitcoinApi implements AbstractBitcoinApi { return transaction; } + public startHealthChecks(): void {}; } export default BitcoinApi; diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 77c6d80fc..a44720d83 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -1,135 +1,260 @@ import config from '../../config'; -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosResponse } from 'axios'; import http from 'http'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; -const axiosConnection = axios.create({ - httpAgent: new http.Agent({ keepAlive: true, }) -}); +interface FailoverHost { + host: string, + rtts: number[], + rtt: number + failures: number, + socket?: boolean, + outOfSync?: boolean, + unreachable?: boolean, + preferred?: boolean, +} -class ElectrsApi implements AbstractBitcoinApi { - private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? { - socketPath: config.ESPLORA.UNIX_SOCKET_PATH, - timeout: 10000, - } : { - timeout: 10000, - }; - private axiosConfigTcpSocketOnly: AxiosRequestConfig = { - timeout: 10000, - }; - - unixSocketRetryTimeout; - activeAxiosConfig; +class FailoverRouter { + activeHost: FailoverHost; + fallbackHost: FailoverHost; + hosts: FailoverHost[]; + multihost: boolean; + pollInterval: number = 60000; + pollTimer: NodeJS.Timeout | null = null; + pollConnection = axios.create(); + requestConnection = axios.create({ + httpAgent: new http.Agent({ keepAlive: true }) + }); constructor() { - this.activeAxiosConfig = this.axiosConfigWithUnixSocket; + // setup list of hosts + this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { + return { + host: domain, + rtts: [], + rtt: Infinity, + failures: 0, + }; + }); + this.activeHost = { + host: config.ESPLORA.UNIX_SOCKET_PATH || config.ESPLORA.REST_API_URL, + rtts: [], + rtt: 0, + failures: 0, + socket: !!config.ESPLORA.UNIX_SOCKET_PATH, + preferred: true, + }; + this.fallbackHost = this.activeHost; + this.hosts.unshift(this.activeHost); + this.multihost = this.hosts.length > 1; } - fallbackToTcpSocket() { - if (!this.unixSocketRetryTimeout) { - logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`); - // Retry the unix socket after a few seconds - this.unixSocketRetryTimeout = setTimeout(() => { - logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`); - this.activeAxiosConfig = this.axiosConfigWithUnixSocket; - this.unixSocketRetryTimeout = undefined; - }, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER); + public startHealthChecks(): void { + // use axios interceptors to measure request rtt + this.pollConnection.interceptors.request.use((config) => { + config['meta'] = { startTime: Date.now() }; + return config; + }); + this.pollConnection.interceptors.response.use((response) => { + response.config['meta'].rtt = Date.now() - response.config['meta'].startTime; + return response; + }); + + if (this.multihost) { + this.pollHosts(); + } + } + + // start polling hosts to measure availability & rtt + private async pollHosts(): Promise { + if (this.pollTimer) { + clearTimeout(this.pollTimer); } - // Use the TCP socket (reach a different esplora instance through nginx) - this.activeAxiosConfig = this.axiosConfigTcpSocketOnly; + const results = await Promise.allSettled(this.hosts.map(async (host) => { + if (host.socket) { + return this.pollConnection.get('/blocks/tip/height', { socketPath: host.host, timeout: 2000 }); + } else { + return this.pollConnection.get(host.host + '/blocks/tip/height', { timeout: 2000 }); + } + })); + const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0); + + // update rtts & sync status + for (let i = 0; i < results.length; i++) { + const host = this.hosts[i]; + const result = results[i].status === 'fulfilled' ? (results[i] as PromiseFulfilledResult>).value : null; + if (result) { + const height = result.data; + const rtt = result.config['meta'].rtt; + host.rtts.unshift(rtt); + host.rtts.slice(0, 5); + host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length; + if (height == null || isNaN(height) || (maxHeight - height > 2)) { + host.outOfSync = true; + } else { + host.outOfSync = false; + } + host.unreachable = false; + } else { + host.unreachable = true; + } + } + + this.sortHosts(); + + logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`); + + // switch if the current host is out of sync or significantly slower than the next best alternative + if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) { + if (this.activeHost.unreachable) { + logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`); + } else if (this.activeHost.outOfSync) { + logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`); + } else { + logger.debug(`${this.activeHost.host} is no longer the best esplora host`); + } + this.electHost(); + } + + this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval); } - $queryWrapper(url, responseType = 'json'): Promise { - return axiosConnection.get(url, { ...this.activeAxiosConfig, responseType: responseType }) - .then((response) => response.data) + // sort hosts by connection quality, and update default fallback + private sortHosts(): void { + // sort by connection quality + this.hosts.sort((a, b) => { + if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) { + if (a.preferred === b.preferred) { + // lower rtt is best + return a.rtt - b.rtt; + } else { // unless we have a preferred host + return a.preferred ? -1 : 1; + } + } else { // or the host is out of sync + return (a.unreachable || a.outOfSync) ? 1 : -1; + } + }); + if (this.hosts.length > 1 && this.hosts[0] === this.activeHost) { + this.fallbackHost = this.hosts[1]; + } else { + this.fallbackHost = this.hosts[0]; + } + } + + // depose the active host and choose the next best replacement + private electHost(): void { + this.activeHost.outOfSync = true; + this.activeHost.failures = 0; + this.sortHosts(); + this.activeHost = this.hosts[0]; + logger.warn(`Switching esplora host to ${this.activeHost.host}`); + } + + private addFailure(host: FailoverHost): FailoverHost { + host.failures++; + if (host.failures > 5 && this.multihost) { + logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`); + this.electHost(); + return this.activeHost; + } else { + return this.fallbackHost; + } + } + + private async $query(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise { + let axiosConfig; + let url; + if (host.socket) { + axiosConfig = { socketPath: host.host, timeout: 10000, responseType }; + url = path; + } else { + axiosConfig = { timeout: 10000, responseType }; + url = host.host + path; + } + return (method === 'post' + ? this.requestConnection.post(url, data, axiosConfig) + : this.requestConnection.get(url, axiosConfig) + ).then((response) => { host.failures = Math.max(0, host.failures - 1); return response.data; }) .catch((e) => { - if (e?.code === 'ECONNREFUSED') { - this.fallbackToTcpSocket(); + let fallbackHost = this.fallbackHost; + if (e?.response?.status !== 404) { + logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`); + fallbackHost = this.addFailure(host); + } + if (retry && e?.code === 'ECONNREFUSED' && this.multihost) { // Retry immediately - return axiosConnection.get(url, this.activeAxiosConfig) - .then((response) => response.data) - .catch((e) => { - logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`); - throw e; - }); + return this.$query(method, path, data, responseType, fallbackHost, false); } else { throw e; } }); } - $postWrapper(url, body, responseType = 'json', params: any = undefined): Promise { - return axiosConnection.post(url, body, { ...this.activeAxiosConfig, responseType: responseType, params }) - .then((response) => response.data) - .catch((e) => { - if (e?.code === 'ECONNREFUSED') { - this.fallbackToTcpSocket(); - // Retry immediately - return axiosConnection.post(url, body, this.activeAxiosConfig) - .then((response) => response.data) - .catch((e) => { - logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`); - throw e; - }); - } else { - throw e; - } - }); + public async $get(path, responseType = 'json'): Promise { + return this.$query('get', path, null, responseType); } + public async $post(path, data: any, responseType = 'json'): Promise { + return this.$query('post', path, data, responseType); + } +} + +class ElectrsApi implements AbstractBitcoinApi { + private failoverRouter = new FailoverRouter(); + $getRawMempool(): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/mempool/txids'); + return this.failoverRouter.$get('/mempool/txids'); } $getRawTransaction(txId: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId); + return this.failoverRouter.$get('/tx/' + txId); } async $getMempoolTransactions(txids: string[]): Promise { - return this.$postWrapper(config.ESPLORA.REST_API_URL + '/mempool/txs', txids, 'json'); + return this.failoverRouter.$post('/mempool/txs', txids, 'json'); } async $getAllMempoolTransactions(lastSeenTxid?: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : '')); + return this.failoverRouter.$get('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : '')); } $getTransactionHex(txId: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex'); + return this.failoverRouter.$get('/tx/' + txId + '/hex'); } $getBlockHeightTip(): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/blocks/tip/height'); + return this.failoverRouter.$get('/blocks/tip/height'); } $getBlockHashTip(): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/blocks/tip/hash'); + return this.failoverRouter.$get('/blocks/tip/hash'); } $getTxIdsForBlock(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids'); + return this.failoverRouter.$get('/block/' + hash + '/txids'); } $getTxsForBlock(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs'); + return this.failoverRouter.$get('/block/' + hash + '/txs'); } $getBlockHash(height: number): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block-height/' + height); + return this.failoverRouter.$get('/block-height/' + height); } $getBlockHeader(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header'); + return this.failoverRouter.$get('/block/' + hash + '/header'); } $getBlock(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash); + return this.failoverRouter.$get('/block/' + hash); } $getRawBlock(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer') + return this.failoverRouter.$get('/block/' + hash + '/raw', 'arraybuffer') .then((response) => { return Buffer.from(response.data); }); } @@ -158,11 +283,11 @@ class ElectrsApi implements AbstractBitcoinApi { } $getOutspend(txId: string, vout: number): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout); + return this.failoverRouter.$get('/tx/' + txId + '/outspend/' + vout); } $getOutspends(txId: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends'); + return this.failoverRouter.$get('/tx/' + txId + '/outspends'); } async $getBatchedOutspends(txId: string[]): Promise { @@ -173,6 +298,10 @@ class ElectrsApi implements AbstractBitcoinApi { } return outspends; } + + public startHealthChecks(): void { + this.failoverRouter.startHealthChecks(); + } } export default ElectrsApi; diff --git a/backend/src/config.ts b/backend/src/config.ts index 982e17b34..ed320d957 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -44,6 +44,7 @@ interface IConfig { REST_API_URL: string; UNIX_SOCKET_PATH: string | void | null; RETRY_UNIX_SOCKET_AFTER: number; + FALLBACK: string[]; }; LIGHTNING: { ENABLED: boolean; @@ -188,6 +189,7 @@ const defaults: IConfig = { 'REST_API_URL': 'http://127.0.0.1:3000', 'UNIX_SOCKET_PATH': null, 'RETRY_UNIX_SOCKET_AFTER': 30000, + 'FALLBACK': [], }, 'ELECTRUM': { 'HOST': '127.0.0.1', diff --git a/backend/src/index.ts b/backend/src/index.ts index adb3f2e02..9d0fa07f5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -91,6 +91,10 @@ class Server { async startServer(worker = false): Promise { logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); + if (config.MEMPOOL.BACKEND === 'esplora') { + bitcoinApi.startHealthChecks(); + } + if (config.DATABASE.ENABLED) { await DB.checkDbConnection(); try { diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 70ff0d283..e283d1171 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -51,7 +51,8 @@ "ESPLORA": { "REST_API_URL": "__ESPLORA_REST_API_URL__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", - "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__ + "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__, + "FALLBACK": __ESPLORA_FALLBACK__, }, "SECOND_CORE_RPC": { "HOST": "__SECOND_CORE_RPC_HOST__", diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index 6b67711b6..81a8c2a57 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -22,7 +22,7 @@ footer .row.main .branding { } footer .row.main .branding > p { - margin-bottom: 25px; + margin-bottom: 45px; } footer .row.main .branding .btn { diff --git a/production/mempool-config.liquid.json b/production/mempool-config.liquid.json index 29223fa07..d67d7b794 100644 --- a/production/mempool-config.liquid.json +++ b/production/mempool-config.liquid.json @@ -23,8 +23,27 @@ "PASSWORD": "__BITCOIN_RPC_PASS__" }, "ESPLORA": { - "REST_API_URL": "http://127.0.0.1:5001", - "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet" + "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet", + "FALLBACK": [ + "http://node201.fmt.mempool.space:3001", + "http://node202.fmt.mempool.space:3001", + "http://node203.fmt.mempool.space:3001", + "http://node204.fmt.mempool.space:3001", + "http://node205.fmt.mempool.space:3001", + "http://node206.fmt.mempool.space:3001", + "http://node201.fra.mempool.space:3001", + "http://node202.fra.mempool.space:3001", + "http://node203.fra.mempool.space:3001", + "http://node204.fra.mempool.space:3001", + "http://node205.fra.mempool.space:3001", + "http://node206.fra.mempool.space:3001", + "http://node201.tk7.mempool.space:3001", + "http://node202.tk7.mempool.space:3001", + "http://node203.tk7.mempool.space:3001", + "http://node204.tk7.mempool.space:3001", + "http://node205.tk7.mempool.space:3001", + "http://node206.tk7.mempool.space:3001" + ] }, "DATABASE": { "ENABLED": true, diff --git a/production/mempool-config.liquidtestnet.json b/production/mempool-config.liquidtestnet.json index 82b41e07f..3a76b4c86 100644 --- a/production/mempool-config.liquidtestnet.json +++ b/production/mempool-config.liquidtestnet.json @@ -23,8 +23,27 @@ "PASSWORD": "__BITCOIN_RPC_PASS__" }, "ESPLORA": { - "REST_API_URL": "http://127.0.0.1:5004", - "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet" + "UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet", + "FALLBACK": [ + "http://node201.fmt.mempool.space:3004", + "http://node202.fmt.mempool.space:3004", + "http://node203.fmt.mempool.space:3004", + "http://node204.fmt.mempool.space:3004", + "http://node205.fmt.mempool.space:3004", + "http://node206.fmt.mempool.space:3004", + "http://node201.fra.mempool.space:3004", + "http://node202.fra.mempool.space:3004", + "http://node203.fra.mempool.space:3004", + "http://node204.fra.mempool.space:3004", + "http://node205.fra.mempool.space:3004", + "http://node206.fra.mempool.space:3004", + "http://node201.tk7.mempool.space:3004", + "http://node202.tk7.mempool.space:3004", + "http://node203.tk7.mempool.space:3004", + "http://node204.tk7.mempool.space:3004", + "http://node205.tk7.mempool.space:3004", + "http://node206.tk7.mempool.space:3004" + ] }, "DATABASE": { "ENABLED": true, diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index f54635415..d4222bd05 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -35,8 +35,27 @@ "PASSWORD": "__BITCOIN_RPC_PASS__" }, "ESPLORA": { - "REST_API_URL": "http://127.0.0.1:5000", - "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet" + "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet", + "FALLBACK": [ + "http://node201.fmt.mempool.space:3000", + "http://node202.fmt.mempool.space:3000", + "http://node203.fmt.mempool.space:3000", + "http://node204.fmt.mempool.space:3000", + "http://node205.fmt.mempool.space:3000", + "http://node206.fmt.mempool.space:3000", + "http://node201.fra.mempool.space:3000", + "http://node202.fra.mempool.space:3000", + "http://node203.fra.mempool.space:3000", + "http://node204.fra.mempool.space:3000", + "http://node205.fra.mempool.space:3000", + "http://node206.fra.mempool.space:3000", + "http://node201.tk7.mempool.space:3000", + "http://node202.tk7.mempool.space:3000", + "http://node203.tk7.mempool.space:3000", + "http://node204.tk7.mempool.space:3000", + "http://node205.tk7.mempool.space:3000", + "http://node206.tk7.mempool.space:3000" + ] }, "DATABASE": { "ENABLED": true, diff --git a/production/mempool-config.signet.json b/production/mempool-config.signet.json index 957b36101..38d59c0e9 100644 --- a/production/mempool-config.signet.json +++ b/production/mempool-config.signet.json @@ -25,8 +25,27 @@ "PASSWORD": "__BITCOIN_RPC_PASS__" }, "ESPLORA": { - "REST_API_URL": "http://127.0.0.1:5003", - "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet" + "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet", + "FALLBACK": [ + "http://node201.fmt.mempool.space:3003", + "http://node202.fmt.mempool.space:3003", + "http://node203.fmt.mempool.space:3003", + "http://node204.fmt.mempool.space:3003", + "http://node205.fmt.mempool.space:3003", + "http://node206.fmt.mempool.space:3003", + "http://node201.fra.mempool.space:3003", + "http://node202.fra.mempool.space:3003", + "http://node203.fra.mempool.space:3003", + "http://node204.fra.mempool.space:3003", + "http://node205.fra.mempool.space:3003", + "http://node206.fra.mempool.space:3003", + "http://node201.tk7.mempool.space:3003", + "http://node202.tk7.mempool.space:3003", + "http://node203.tk7.mempool.space:3003", + "http://node204.tk7.mempool.space:3003", + "http://node205.tk7.mempool.space:3003", + "http://node206.tk7.mempool.space:3003" + ] }, "DATABASE": { "ENABLED": true, diff --git a/production/mempool-config.testnet.json b/production/mempool-config.testnet.json index 8943e987f..c5bdfc8d7 100644 --- a/production/mempool-config.testnet.json +++ b/production/mempool-config.testnet.json @@ -25,8 +25,27 @@ "PASSWORD": "__BITCOIN_RPC_PASS__" }, "ESPLORA": { - "REST_API_URL": "http://127.0.0.1:5002", - "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet" + "UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet", + "FALLBACK": [ + "http://node201.fmt.mempool.space:3002", + "http://node202.fmt.mempool.space:3002", + "http://node203.fmt.mempool.space:3002", + "http://node204.fmt.mempool.space:3002", + "http://node205.fmt.mempool.space:3002", + "http://node206.fmt.mempool.space:3002", + "http://node201.fra.mempool.space:3002", + "http://node202.fra.mempool.space:3002", + "http://node203.fra.mempool.space:3002", + "http://node204.fra.mempool.space:3002", + "http://node205.fra.mempool.space:3002", + "http://node206.fra.mempool.space:3002", + "http://node201.tk7.mempool.space:3002", + "http://node202.tk7.mempool.space:3002", + "http://node203.tk7.mempool.space:3002", + "http://node204.tk7.mempool.space:3002", + "http://node205.tk7.mempool.space:3002", + "http://node206.tk7.mempool.space:3002" + ] }, "DATABASE": { "ENABLED": true,