diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index 7b90c516ec..d65a6e5f3b 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -1,3 +1,4 @@ +import { couldStartTrivia } from 'typescript'; import config from '../config'; import logger from '../logger'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; @@ -28,6 +29,10 @@ class Audit { let matchedWeight = 0; let projectedWeight = 0; + let countCb = 0; + let spamWeight = 0; + let blkWeight = 0; + const inBlock = {}; const inTemplate = {}; @@ -72,6 +77,22 @@ class Audit { matchedWeight += transactions[0].weight; } + + for (const tx of transactions){ + blkWeight += tx.weight; + } + + for (const tx of transactions){ + if (countCb !== 0){ + if(tx.spam !== undefined){ + if (tx.spam == true){ + spamWeight += tx.weight; + } + } + } + countCb += 1; + } + // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs // these displaced transactions should occupy the first N weight units of the next projected block let displacedWeightRemaining = displacedWeight + 4000; @@ -169,11 +190,8 @@ class Audit { const numCensored = Object.keys(isCensored).length; const numMatches = matches.length - 1; // adjust for coinbase tx let score = 0; - if (numMatches <= 0 && numCensored <= 0) { - score = 1; - } else if (numMatches > 0) { - score = (numMatches / (numMatches + numCensored)); - } + + score = (Math.abs((spamWeight/blkWeight)-1)); const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; return { diff --git a/backend/src/api/bip110-deployment.ts b/backend/src/api/bip110-deployment.ts new file mode 100644 index 0000000000..84d62d69e7 --- /dev/null +++ b/backend/src/api/bip110-deployment.ts @@ -0,0 +1,245 @@ +import logger from '../logger'; +import blocks from './blocks'; +import bitcoinApiFactory from './bitcoin/bitcoin-api-factory'; +import { Common } from './common'; + +/** + * BIP-110 'reduced_data' deployment state tracker. + * + * Computes the deployment phase from the current chain tip and signaling data, + * using the constants defined in bip-0110.mediawiki: + * + * bit: 4 + * starttime: 1764547200 (~December 1, 2025) + * threshold: 1109/2016 (55%) + * max_activation_height: 965664 + * active_duration: 52416 blocks (~1 year) + * mandatory signaling: blocks 961632–963647 + * mandatory lock-in: height 963648 + * + * State machine: DEFINED → STARTED → LOCKED_IN → ACTIVE + */ + +// ── BIP-110 deployment constants ────────────────────────────────────────────── +const RETARGET_PERIOD = 2016; +const THRESHOLD = 1109; // 55% of 2016 +const STARTTIME = 1764547200; // MTP threshold for DEFINED→STARTED +const MANDATORY_SIGNALING_START = 961632; // first block of mandatory signaling period +const MANDATORY_LOCK_IN_HEIGHT = 963648; // forced LOCKED_IN if threshold not reached earlier +const MAX_ACTIVATION_HEIGHT = 965664; // ACTIVE starts here if locked in at mandatory +const ACTIVE_DURATION = 52416; // rules enforced for this many blocks after activation + +export type Bip110State = 'defined' | 'started' | 'locked_in' | 'active'; + +export interface Bip110DeploymentInfo { + /** Current deployment state */ + state: Bip110State; + /** Current chain tip height */ + currentHeight: number; + + // ── STARTED phase info ────────────────────────────────────────────────── + /** Signaling blocks in the current retarget period */ + periodSignaling: number; + /** Total blocks mined so far in the current retarget period */ + periodBlocks: number; + /** Height of the first block of the current retarget period */ + periodStartHeight: number; + /** Threshold needed (1109) */ + threshold: number; + /** Signaling percentage in current period (0–100) */ + signalingPercent: number; + /** Whether threshold has been reached in the current period */ + thresholdReached: boolean; + + // ── Countdown / milestone info ────────────────────────────────────────── + /** Blocks remaining until mandatory signaling period begins */ + blocksUntilMandatory: number; + /** Whether the current block is in the mandatory signaling window */ + inMandatorySignaling: boolean; + /** Height at which the soft fork rules will activate (or did activate) */ + activationHeight: number | null; + /** Height at which active_duration expires (rules stop being enforced) */ + expiryHeight: number | null; + /** Blocks remaining in the active enforcement period (0 if not active or expired) */ + blocksUntilExpiry: number; + /** Whether the rules have expired (active_duration elapsed) */ + rulesExpired: boolean; +} + +class Bip110DeploymentApi { + private cachedInfo: Bip110DeploymentInfo | null = null; + private lastHeight: number = -1; + /** Height at which LOCKED_IN was entered (if we know it) */ + private lockedInHeight: number | null = null; + + /** + * Get the current deployment info. Recomputes only when chain tip changes. + */ + public async getDeploymentInfo(): Promise { + const currentHeight = blocks.getCurrentBlockHeight(); + if (currentHeight < 0) { + return null; + } + if (currentHeight !== this.lastHeight || !this.cachedInfo) { + this.cachedInfo = await this.computeDeploymentInfo(currentHeight); + this.lastHeight = currentHeight; + } + return this.cachedInfo; + } + + /** + * Compute the full deployment state for a given chain tip height. + */ + private async computeDeploymentInfo(currentHeight: number): Promise { + const state = this.computeState(currentHeight); + + // Current retarget period signaling stats + const periodStartHeight = currentHeight - (currentHeight % RETARGET_PERIOD); + const periodBlocks = (currentHeight % RETARGET_PERIOD) + 1; + + // Count signaling blocks in the current retarget period from the in-memory block cache + const periodSignaling = await this.countSignalingInCurrentPeriod(periodStartHeight, currentHeight); + const signalingPercent = periodBlocks > 0 ? (periodSignaling / periodBlocks) * 100 : 0; + const thresholdReached = periodSignaling >= THRESHOLD; + + // Milestone computations + const blocksUntilMandatory = Math.max(0, MANDATORY_SIGNALING_START - currentHeight); + const inMandatorySignaling = currentHeight >= MANDATORY_SIGNALING_START && currentHeight < MANDATORY_LOCK_IN_HEIGHT; + + // Activation height: computed from when lock-in occurred + let activationHeight: number | null = null; + if (state === 'locked_in' || state === 'active') { + if (this.lockedInHeight != null) { + // Activation is at the start of the next retarget period after lock-in + const lockInPeriodStart = this.lockedInHeight - (this.lockedInHeight % RETARGET_PERIOD); + activationHeight = lockInPeriodStart + RETARGET_PERIOD; + } else { + // Fallback: use MAX_ACTIVATION_HEIGHT (mandatory lock-in case) + activationHeight = MAX_ACTIVATION_HEIGHT; + } + } + + const expiryHeight = activationHeight != null ? activationHeight + ACTIVE_DURATION : null; + const blocksUntilExpiry = expiryHeight != null && state === 'active' + ? Math.max(0, expiryHeight - currentHeight) + : 0; + const rulesExpired = state === 'active' && expiryHeight != null && currentHeight >= expiryHeight; + + return { + state, + currentHeight, + periodSignaling, + periodBlocks, + periodStartHeight, + threshold: THRESHOLD, + signalingPercent, + thresholdReached, + blocksUntilMandatory, + inMandatorySignaling, + activationHeight, + expiryHeight, + blocksUntilExpiry, + rulesExpired, + }; + } + + /** + * Determine the deployment state based on height and known history. + * + * State transitions (BIP-110): + * DEFINED → block.MTP ≥ starttime → STARTED + * STARTED → threshold reached OR height ≥ 963648 → LOCKED_IN + * LOCKED_IN → next retarget boundary → ACTIVE + * + * Since we can't cheaply compute MTP, we use block timestamps from the cache + * as a reasonable approximation. The DEFINED→STARTED transition only matters + * for blocks near the starttime (~Dec 2025). After that, height-based logic + * dominates. + */ + private computeState(currentHeight: number): Bip110State { + // Check if we've already passed LOCKED_IN or ACTIVE thresholds + if (this.lockedInHeight != null) { + const lockInPeriodStart = this.lockedInHeight - (this.lockedInHeight % RETARGET_PERIOD); + const activationHeight = lockInPeriodStart + RETARGET_PERIOD; + if (currentHeight >= activationHeight) { + return 'active'; + } + return 'locked_in'; + } + + // Height >= mandatory lock-in height means at least LOCKED_IN + if (currentHeight >= MANDATORY_LOCK_IN_HEIGHT) { + // Must be LOCKED_IN or ACTIVE + this.lockedInHeight = MANDATORY_LOCK_IN_HEIGHT; + const lockInPeriodStart = MANDATORY_LOCK_IN_HEIGHT - (MANDATORY_LOCK_IN_HEIGHT % RETARGET_PERIOD); + const activationHeight = lockInPeriodStart + RETARGET_PERIOD; + if (currentHeight >= activationHeight) { + return 'active'; + } + return 'locked_in'; + } + + // Before starttime → DEFINED + // Use the latest block timestamp as a proxy for MTP + const latestBlocks = blocks.getBlocks(); + const latestBlock = latestBlocks.length > 0 ? latestBlocks[latestBlocks.length - 1] : null; + if (latestBlock && latestBlock.timestamp < STARTTIME) { + return 'defined'; + } + + // If we can't determine (no blocks yet), assume DEFINED + if (!latestBlock) { + return 'defined'; + } + + // We're in STARTED state — check if threshold was reached at the end of + // the most recent completed retarget period + // (In practice, we'd need to scan historical retarget periods, but for + // a live dashboard, we check the current period's signaling progress.) + return 'started'; + } + + /** + * Called when a new block arrives. If we detect threshold reached at a + * retarget boundary, record the lock-in height. + */ + public async onNewBlock(height: number): Promise { + if (this.lockedInHeight != null) { + return; // Already locked in + } + + // Check if this block is the last block of a retarget period + const posInPeriod = height % RETARGET_PERIOD; + if (posInPeriod === RETARGET_PERIOD - 1) { + const periodStart = height - posInPeriod; + const signaling = await this.countSignalingInCurrentPeriod(periodStart, height); + if (signaling >= THRESHOLD) { + this.lockedInHeight = height + 1; // Lock-in happens at the next retarget boundary + logger.info(`BIP-110: Threshold reached at height ${height} (${signaling}/${RETARGET_PERIOD}). LOCKED_IN at ${this.lockedInHeight}.`); + } + } + + // Clear cached info so it's recomputed + this.lastHeight = -1; + this.cachedInfo = null; + } + + /** + * Count signaling blocks in the current retarget period using the in-memory + * block cache. Falls back to 0 if blocks aren't in memory. + */ + private async countSignalingInCurrentPeriod(periodStart: number, currentHeight: number): Promise { + let count = 0; + let hash, blockdata; + for (let i = currentHeight; i >= periodStart; i--){ + hash = await bitcoinApiFactory.$getBlockHash(i); + blockdata = await blocks.$getBlock(hash); + if (Common.isSignalingBIP110(blockdata.version)){ + count++; + } + } + return count; + } +} + +export default new Bip110DeploymentApi(); diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index fafcfdbcd0..56620d2043 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -30,6 +30,18 @@ const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i; const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i; class BitcoinRoutes { + private bitnodesCache: { + data: any; + lastUpdated: number; + } | null = null; + private readonly BITNODES_CACHE_DURATION = 120 * 60 * 1000; + + private oceanCache: { + data: any; + lastUpdated: number; + } | null = null; + private readonly OCEAN_CACHE_DURATION = 60 * 60 * 1000; + public initRoutes(app: Application) { app .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes) @@ -41,6 +53,8 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo) .get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData) .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress) + .get(config.MEMPOOL.API_URL_PREFIX + 'bitnodes/knots-stats', this.getBitnodesKnotsStats.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'ocean/hashrate-stats', this.getOceanHashrateStats.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx) .get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements) @@ -237,6 +251,160 @@ class BitcoinRoutes { res.json(backendInfo.getBackendInfo()); } + private async getBitnodesKnotsStats(req: Request, res: Response) { + try { + const now = Date.now(); + if (this.bitnodesCache && + this.bitnodesCache.lastUpdated && + (now - this.bitnodesCache.lastUpdated) < this.BITNODES_CACHE_DURATION) { + logger.debug('Serving Bitcoin Knots nodes stats from cache'); + res.json(this.bitnodesCache.data); + return; + } + + logger.debug('Fetching fresh Bitcoin Knots nodes stats from Bitnodes API'); + const response = await axios.get('https://bitnodes.io/api/v1/snapshots/latest', { + timeout: 10000, + headers: { + 'User-Agent': 'Mempool.space/1.0' + } + }); + + const snapshot = response.data; + const totalBitcoinNodes = snapshot.total_nodes; + let totalKnotsNodesClearnet = 0; + let torNodeCount = 0; + let fullCount = 0; + let bipcount = 0; + + Object.entries(snapshot.nodes).forEach(([address, nodeData]: [string, any]) => { + const userAgent = nodeData[1]; + if (userAgent && userAgent.toLowerCase().includes('bip110')){ + bipcount++; + } + if (userAgent && userAgent.toLowerCase().includes('knots')) { + if (address.includes('.onion')) { + torNodeCount++; + } else { + totalKnotsNodesClearnet++; + } + } + }); + + fullCount = torNodeCount + totalKnotsNodesClearnet; + const knotsPercentageOfTotal = totalBitcoinNodes > 0 ? (fullCount / totalBitcoinNodes) * 100 : 0; + + const result = { + countries: [], + totals: { + totalNodes: fullCount, + clearnetNodes: totalKnotsNodesClearnet, + torNodes: torNodeCount, + totalBitcoinNodes: totalBitcoinNodes, + percentageOfTotal: knotsPercentageOfTotal, + bipCount: bipcount + } + }; + + this.bitnodesCache = { + data: result, + lastUpdated: now + }; + + logger.debug(`Cached Bitcoin Knots nodes stats: ${totalKnotsNodesClearnet} clearnet nodes, ${torNodeCount} Tor nodes, ${fullCount} total nodes (${knotsPercentageOfTotal.toFixed(2)}% of ${totalBitcoinNodes} total Bitcoin nodes)`); + res.json(result); + } catch (error) { + logger.err(`Error fetching Bitnodes data: ${error}`); + if (this.bitnodesCache && this.bitnodesCache.data) { + logger.warn('Serving expired cached data due to API error'); + res.json(this.bitnodesCache.data); + return; + } + handleError(req, res, 500, 'Failed to fetch Bitcoin Knots nodes statistics'); + } + } + + private async getOceanHashrateStats(req: Request, res: Response) { + try { + // Check if we have cached data that's still valid + const now = Date.now(); + if (this.oceanCache && + this.oceanCache.lastUpdated && + (now - this.oceanCache.lastUpdated) < this.OCEAN_CACHE_DURATION) { + logger.debug('Serving Ocean hashrate stats from cache'); + res.json(this.oceanCache.data); + return; + } + + logger.debug('Fetching fresh Ocean hashrate stats from API'); + const response = await axios.get('https://api.ocean.xyz/v1/multitemplate_stats', { + timeout: 10000, + headers: { + 'User-Agent': 'Mempool.space/1.0' + } + }); + + const oceanData = response.data; + if (!oceanData || !oceanData.result || !Array.isArray(oceanData.result.share_tags)) { + throw new Error('Invalid Ocean API response format'); + } + + const shareTags = oceanData.result.share_tags; + const snapTs = oceanData.result.snap_ts; + + // Calculate total hashrate + const totalShares = shareTags.reduce((sum: number, shares: number) => sum + shares, 0); + + // Define the template names based on the API documentation + const templateNames = [ + 'Ocean', + 'Datum', + 'Core', + 'OrdiRespector', + 'Data-Free', + 'Unknown 1', + 'Unknown 2', + 'Unknown 3' + ]; + + // Create the result array with percentages + const result = shareTags.map((shares: number, index: number) => ({ + template: templateNames[index] || `Template ${index}`, + shares: shares, + percentage: totalShares > 0 ? (shares / totalShares) * 100 : 0 + })) + .filter(item => item.shares > 0) // Only include templates with shares + .sort((a, b) => b.shares - a.shares); // Sort by shares descending + + const processedData = { + templates: result, + totalShares: totalShares, + timestamp: snapTs, + lastUpdated: now + }; + + // Update cache + this.oceanCache = { + data: processedData, + lastUpdated: now + }; + + logger.debug(`Cached Ocean hashrate stats: ${totalShares} total shares across ${result.length} active templates`); + res.json(processedData); + } catch (error) { + logger.err(`Error fetching Ocean data: ${error}`); + + // If we have cached data (even if expired), serve it as fallback + if (this.oceanCache && this.oceanCache.data) { + logger.warn('Serving expired Ocean cached data due to API error'); + res.json(this.oceanCache.data); + return; + } + + handleError(req, res, 500, 'Failed to fetch Ocean hashrate statistics'); + } + } + private async getTransaction(req: Request, res: Response) { if (!TXID_REGEX.test(req.params.txId)) { handleError(req, res, 501, `Invalid transaction ID`); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index c861f1a2d7..9fb89d02d4 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -356,7 +356,9 @@ class Blocks { minerNames: null, }; - if (extras.pool.name === 'OCEAN') { + let rootpool = parseDATUMTemplateCreator(extras.coinbaseRaw); + + if (rootpool && rootpool[0] === ' OCEANXYZ ') { extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); } } @@ -381,6 +383,9 @@ class Blocks { } } + // BIP110 'reduced_data' miner signaling detection (version bit 4, 55% threshold) + extras.bip110Signaling = Common.isSignalingBIP110(block.version); + blk.extras = extras; return blk; } @@ -1464,10 +1469,20 @@ class Blocks { let block = this.getBlocks().find((b) => b.height === currentHeight); if (block) { // Using the memory cache (find by height) + // Ensure BIP110 data is populated for cached blocks + if (block.extras) { + // Always recompute from version bit (not cached value) to avoid stale data + block.extras.bip110Signaling = Common.isSignalingBIP110(block.version); + } returnBlocks.push(block); } else { // Using indexing (find by height, index on the fly, save in database) block = await this.$indexBlockByHeight(currentHeight); + // Inject BIP110 violation stats from persistent cache + // ($indexBlock only fetches coinbase, so violation data is always 0 without this) + if (block.extras) { + block.extras.bip110Signaling = Common.isSignalingBIP110(block.version); + } returnBlocks.push(block); } currentHeight--; diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 8fcd2b27bb..cc0780ba31 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -25,6 +25,7 @@ const DUST_RELAY_TX_FEE = 3; const MAX_OP_RETURN_RELAY = 83; const DEFAULT_PERMIT_BAREMULTISIG = true; const MAX_TX_LEGACY_SIGOPS = 2_500 * 4; // witness-adjusted sigops +const BIP110_VERSION_BIT = 4; // BIP110 deployment 'reduced_data' signaling bit export class Common { static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? @@ -562,6 +563,20 @@ export class Common { return flags; } + static isInscription2(vin, tx): void { + // in taproot, if the last witness item begins with 0x50, it's an annex + const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50'); + // script spends have more than one witness item, not counting the annex (if present) + if (vin.witness.length > (hasAnnex ? 2 : 1)) { + // the script itself is the second-to-last witness item, not counting the annex + const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]); + // inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope + if (asm?.includes('OP_0 OP_IF')) { + tx.spam = true; + } + } + } + static inputIsMaybeInscription(vin: IEsploraApi.Vin): boolean { // check if this is actually a taproot input let isTaproot = false; @@ -606,6 +621,17 @@ export class Common { return isTaproot || !isNotTaproot; } + // ============================================ + // BIP110 Validation Methods + // ============================================ + + /** + * Check if a block version signals BIP110 support. + * BIP110 deployment 'reduced_data' uses version bit 4 (threshold: 1109/2016, 55%). + */ + static isSignalingBIP110(version: number): boolean { + return (version & (1 << BIP110_VERSION_BIT)) !== 0; + } static getTransactionFlags(tx: TransactionExtended, height?: number): number { let flags = tx.flags ? BigInt(tx.flags) : 0n; @@ -658,9 +684,11 @@ export class Common { flags |= TransactionFlags.p2tr; if (vin.witness?.length) { flags = Common.isInscription(vin, flags); + Common.isInscription2(vin, tx); const hasAnnex = vin.witness.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50'); if (hasAnnex) { flags |= TransactionFlags.annex; + tx.spam = true; } } } break; @@ -671,6 +699,7 @@ export class Common { // try to parse the witness as a taproot inscription try { flags = Common.isInscription(vin, flags); + Common.isInscription2(vin, tx); } catch { // witness script parsing will fail if this isn't really a taproot output } @@ -721,7 +750,7 @@ export class Common { case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; case 'v1_p2tr': flags |= TransactionFlags.p2tr; break; - case 'op_return': flags |= TransactionFlags.op_return; break; + case 'op_return': flags |= TransactionFlags.op_return; tx.spam = true; break; } if (vout.scriptpubkey_address) { reusedOutputAddresses[vout.scriptpubkey_address] = (reusedOutputAddresses[vout.scriptpubkey_address] || 0) + 1; @@ -735,6 +764,7 @@ export class Common { const nullBytes = (P2WSHCount * 32) - olgaSize - 2; if (vout.scriptpubkey.endsWith(''.padEnd(nullBytes * 2, '0'))) { flags |= TransactionFlags.fake_scripthash; + tx.spam = true; } } } else { @@ -744,6 +774,7 @@ export class Common { } if (hasFakePubkey) { flags |= TransactionFlags.fake_pubkey; + tx.spam = true; } // fast but bad heuristic to detect possible coinjoins diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index 92c8e919dc..ef39ea020b 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -134,28 +134,44 @@ class PoolsParser { public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined { const asciiScriptSig = transactionUtils.hex2ascii(scriptsig); + let oceanMatch: PoolTag | undefined; for (let i = 0; i < pools.length; ++i) { + let currentMatch: PoolTag | undefined; + if (addresses.length) { const poolAddresses: string[] = typeof pools[i].addresses === 'string' ? JSON.parse(pools[i].addresses) : pools[i].addresses; for (let y = 0; y < poolAddresses.length; y++) { if (addresses.indexOf(poolAddresses[y]) !== -1) { - return pools[i]; + currentMatch = pools[i]; + break; + } + } + } + + if (!currentMatch) { + const regexes: string[] = typeof pools[i].regexes === 'string' ? + JSON.parse(pools[i].regexes) : pools[i].regexes; + for (let y = 0; y < regexes.length; ++y) { + const regex = new RegExp(regexes[y], 'i'); + const match = asciiScriptSig.match(regex); + if (match !== null) { + currentMatch = pools[i]; + break; } } } - const regexes: string[] = typeof pools[i].regexes === 'string' ? - JSON.parse(pools[i].regexes) : pools[i].regexes; - for (let y = 0; y < regexes.length; ++y) { - const regex = new RegExp(regexes[y], 'i'); - const match = asciiScriptSig.match(regex); - if (match !== null) { - return pools[i]; + if (currentMatch) { + if (currentMatch.id === 142 || currentMatch.slug === 'ocean') { + oceanMatch = currentMatch; + } else { + return currentMatch; } } } + return oceanMatch; } /** diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 595e46d245..4490349415 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -15,6 +15,7 @@ import config from '../config'; import transactionUtils from './transaction-utils'; import rbfCache, { ReplacementInfo } from './rbf-cache'; import difficultyAdjustment from './difficulty-adjustment'; +import bip110Deployment from './bip110-deployment'; import feeApi from './fee-api'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; @@ -86,7 +87,7 @@ class WebsocketHandler { + '}'; } - private updateSocketData(): void { + private async updateSocketData(): Promise { const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); const da = difficultyAdjustment.getDifficultyAdjustment(); this.updateSocketDataFields({ @@ -101,6 +102,7 @@ class WebsocketHandler { 'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'da': da?.previousTime ? da : undefined, 'fees': feeApi.getPreciseRecommendedFee(), + 'bip110deployment': await bip110Deployment.getDeploymentInfo(), }); } @@ -374,7 +376,7 @@ class WebsocketHandler { if (parsedMessage.action === 'init') { if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) { - this.updateSocketData(); + await this.updateSocketData(); } if (!this.socketData['blocks']?.length) { return; @@ -1129,6 +1131,9 @@ class WebsocketHandler { const fees = feeApi.getPreciseRecommendedFee(); const mempoolInfo = memPool.getMempoolInfo(); + // Update BIP-110 deployment state + bip110Deployment.onNewBlock(block.height); + // pre-compute address transactions const addressCache = this.makeAddressCache(transactions); @@ -1140,6 +1145,7 @@ class WebsocketHandler { 'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'da': da?.previousTime ? da : undefined, 'fees': fees, + 'bip110deployment': await bip110Deployment.getDeploymentInfo(), }); const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index a0888bb506..bf8ec9f226 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -132,6 +132,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction { replacement?: boolean; uid?: number; flags?: number; + spam?: boolean; } export interface MempoolTransactionExtended extends TransactionExtended { @@ -328,6 +329,8 @@ export interface BlockExtension { totalInputAmt: number | null; // pools-v2.json git hash definitionHash: string | undefined; + // BIP110 'reduced_data' deployment: miner signaling (version bit 4, threshold 1109/2016) + bip110Signaling?: boolean; } /** diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 9cd4bc0d93..b0f4d67bc0 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -1331,10 +1331,15 @@ class BlocksRepository { } } - if (extras.pool.name === 'OCEAN') { + var rootpool = parseDATUMTemplateCreator(extras.coinbaseRaw); + + if (rootpool && rootpool[0] === ' OCEAN.XYZ ') { extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); } + // BIP110 'reduced_data' signaling detection (version bit 4, 55% threshold) + extras.bip110Signaling = Common.isSignalingBIP110(dbBlk.version); + blk.extras = extras; return blk; } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ea18199d89..dcbe62855a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,7 +5,8 @@ services: environment: FRONTEND_HTTP_PORT: "8080" BACKEND_MAINNET_HTTP_HOST: "api" - image: mempool/frontend:latest + AUDIT: "true" + image: ghcr.io/retropex/mempoolfrontend:v3.2.0 user: "1000:1000" restart: on-failure stop_grace_period: 1m @@ -31,7 +32,10 @@ services: DATABASE_USERNAME: "mempool" DATABASE_PASSWORD: "mempool" STATISTICS_ENABLED: "true" - image: mempool/backend:latest + MEMPOOL_BLOCKS_SUMMARIES_INDEXING: "true" + MEMPOOL_GOGGLES_INDEXING: "true" + MEMPOOL_AUDIT: "true" + image: ghcr.io/retropex/mempoolbackend:v3.2.0 user: "1000:1000" restart: on-failure stop_grace_period: 1m diff --git a/frontend/angular.json b/frontend/angular.json index 9c15807678..c2f6da0585 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -180,6 +180,21 @@ ], "styles": [ "src/styles.scss", + { + "input": "src/theme-contrast.scss", + "bundleName": "contrast", + "inject": false + }, + { + "input": "src/theme-wiz.scss", + "bundleName": "wiz", + "inject": false + }, + { + "input": "src/theme-bukele.scss", + "bundleName": "bukele", + "inject": false + }, "node_modules/@fortawesome/fontawesome-svg-core/styles.css" ], "vendorChunk": true, @@ -244,14 +259,17 @@ }, "local": { "proxyConfig": "proxy.conf.local.js", + "host": "0.0.0.0", "verbose": true }, "local-esplora": { "proxyConfig": "proxy.conf.local-esplora.js", + "host": "0.0.0.0", "verbose": true }, "mixed": { "proxyConfig": "proxy.conf.mixed.js", + "host": "0.0.0.0", "verbose": true }, "local-prod": { diff --git a/frontend/generate-themes.js b/frontend/generate-themes.js index da7f06c205..70f1257fd3 100644 --- a/frontend/generate-themes.js +++ b/frontend/generate-themes.js @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); -const THEMES = ['contrast', 'softsimon', 'bukele']; +const THEMES = ['contrast', 'wiz', 'bukele']; const STAGING_DIR = path.join(__dirname, '.theme-build'); const DIST_DIR = path.join(__dirname, 'dist/mempool/browser'); const MANIFEST_FILE = path.join(__dirname, 'theme-manifest.json'); diff --git a/frontend/proxy.conf.js b/frontend/proxy.conf.js index 1ef2f69343..a01be37390 100644 --- a/frontend/proxy.conf.js +++ b/frontend/proxy.conf.js @@ -26,14 +26,14 @@ PROXY_CONFIG = [ '!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/', '/testnet/api/**', '/signet/api/**', '/testnet4/api/**', '/regtest/api/**' ], - target: "https://mempool.space", + target: "https://mempool.guide", ws: true, secure: false, changeOrigin: true }, { context: ['/api/v1/ws'], - target: "https://mempool.space", + target: "https://mempool.guide", ws: true, secure: false, changeOrigin: true, @@ -57,7 +57,7 @@ PROXY_CONFIG = [ }, { context: ['/resources/mining-pools/**'], - target: "https://mempool.space", + target: "https://mempool.guide", secure: false, changeOrigin: true } @@ -75,7 +75,7 @@ if (configContent && configContent.BASE_MODULE == "liquid") { } else { PROXY_CONFIG.push({ context: ['/resources/assets.json', '/resources/assets.minimal.json', '/resources/worldmap.json'], - target: "https://mempool.space", + target: "https://mempool.guide", secure: false, changeOrigin: true, }); diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index 730da5a816..ed9bad731e 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -220,6 +220,21 @@ export const specialBlocks = { labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', networks: ['mainnet', 'testnet', 'testnet4'], }, + '866558': { + labelEvent: 'Spam free block n°1', + labelEventCompleted: 'Spam free block n°1', + networks: ['mainnet'], + }, + '913272': { + labelEvent: 'Spam free block n°2', + labelEventCompleted: 'Spam free block n°2', + networks: ['mainnet'], + }, + '928997': { + labelEvent: 'Spam free block n°3', + labelEventCompleted: 'Spam free block n°3', + networks: ['mainnet'], + }, '1050000': { labelEvent: 'Bitcoin\'s 5th Halving', labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block', diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index e52cb5f63b..e76832828e 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -2,7 +2,6 @@
® -
v{{ packetJsonVersion }} [{{ frontendGitCommitHash }}] [{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}] @@ -12,7 +11,7 @@
The Mempool Open Source Project ®

Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.

-
Be your own explorer
+
Explore Bitcoin
diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index e6f1ff3c3d..1f7e7188fa 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -19,6 +19,7 @@ import { ServicesApiServices } from '@app/services/services-api.service'; standalone: false, }) export class BlockPreviewComponent implements OnInit, OnDestroy { + poolsWithAntpool = ['poolin', 'braiinspool', 'ultimuspool', 'binancepool', 'secpool', 'sigmapoolcom', 'rawpool', 'luxor', 'btccom', 'miningsquared']; network = ''; block: BlockExtended; blockHeight: number; @@ -31,6 +32,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { error: any; blockSubsidy: number; fees: number; + health: number; overviewError: any = null; overviewSubscription: Subscription; @@ -64,6 +66,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { this.error = undefined; this.overviewError = undefined; this.fees = undefined; + this.health = undefined; let isBlockHeight = false; if (/^[0-9]+$/.test(blockHash)) { @@ -113,6 +116,9 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { if (block?.extras?.reward !== undefined) { this.fees = block.extras.reward / 100000000 - this.blockSubsidy; } + if (block?.extras?.matchRate !== undefined) { + this.health = block.extras.matchRate; + } this.stateService.markBlock$.next({ blockHeight: this.blockHeight }); this.isLoadingOverview = true; this.overviewError = null; diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 0c78081edb..3da44f7a1d 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -1,4 +1,4 @@ -
+