feat: coinbase bip 54 badge in block details

This commit is contained in:
rodribp 2026-07-21 04:22:13 -06:00
parent 29afce710d
commit 04aad09305
No known key found for this signature in database
GPG key ID: DBB99FAE51E608F0
7 changed files with 126 additions and 5 deletions

View file

@ -51,6 +51,7 @@ class Blocks {
private quarterEpochBlockTime: number | null = null;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private classifyingBlocks: boolean = false;
private updatingBlocksMissingCoinbaseBip54: boolean = false;
private oldestCoreLogTimestamp: number | undefined | null = undefined;
private mainLoopTimeout: number = 120000;
@ -319,6 +320,12 @@ class Blocks {
extras.coinbaseSignatureAscii = null;
}
const seq = transactions[0].vin[0].sequence;
if (block.timestamp >= 1771507776) { // First block to include coinbase bip54 on mainnet
extras.coinbaseBip54 = (block.height > 0 && transactions[0].locktime === block.height - 1 && typeof seq === 'number' && seq !== 0xffffffff);
}
const header = await bitcoinClient.getBlockHeader(block.id, false);
extras.header = header;
@ -931,6 +938,60 @@ class Blocks {
this.classifyingBlocks = false;
}
/** @asyncSafe */
public async $updateBlocksMissingCoinbaseBip54(): Promise<void> {
if (this.updatingBlocksMissingCoinbaseBip54) {
return;
}
this.updatingBlocksMissingCoinbaseBip54 = true;
if (!Common.indexingEnabled()) {
return;
}
const blocksMissingCoinbaseBip54 = await BlocksRepository.$getBlocksMissingCoinbaseBip54();
if (!blocksMissingCoinbaseBip54.length) {
this.updatingBlocksMissingCoinbaseBip54 = false;
return;
}
let timer = Date.now();
let updatedThisRun = 0;
let updatedInTotal = 0;
const numToUpdate = blocksMissingCoinbaseBip54.length;
logger.debug(`Updating blocks missing coinbase bip54 from #${blocksMissingCoinbaseBip54[0].height} to #${blocksMissingCoinbaseBip54[blocksMissingCoinbaseBip54.length - 1].height}`, logger.tags.mining);
for (const block of blocksMissingCoinbaseBip54) {
try {
const coinbaseTx = await bitcoinApi.$getCoinbaseTx(block.id);
const seq = coinbaseTx.vin[0].sequence;
const coinbaseBip54 = block.height > 0 && coinbaseTx.locktime === block.height - 1 && typeof seq === 'number' && seq !== 0xffffffff;
await BlocksRepository.$updateCoinbaseBip54(coinbaseBip54, block.id);
updatedInTotal++;
updatedThisRun++;
} catch (e) {
logger.warn(`Failed to update bip54 field in block #${block.height}`, logger.tags.mining);
}
const elapsedSeconds = (Date.now() - timer) / 1000;
if (elapsedSeconds > 5) {
const perSecond = updatedThisRun / elapsedSeconds;
const progress = (updatedInTotal / numToUpdate) * 100;
logger.debug(`Updated #${block.height}: ${updatedInTotal} / ${numToUpdate} (${progress.toFixed(2)}%) | ~${perSecond.toFixed(1)} blocks/s`, logger.tags.mining);
timer = Date.now();
updatedThisRun = 0;
}
await Common.sleep$(250); //Don't DoS the DB
}
logger.debug(`Update of blocks missing coinbase bip54 completed`, logger.tags.mining);
this.updatingBlocksMissingCoinbaseBip54 = false;
}
/**
* [INDEXING] Index missing coinbase addresses for all blocks
*/

View file

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 111;
private static currentVersion = 112;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@ -1255,6 +1255,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `compact_cpfp_clusters` ADD template_algo TINYINT UNSIGNED NOT NULL DEFAULT 0');
await this.updateToSchemaVersion(111);
}
if (databaseSchemaVersion < 112) {
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_bip_54 TINYINT(1) NULL DEFAULT NULL');
await this.updateToSchemaVersion(112);
}
}
/**

View file

@ -228,6 +228,7 @@ class Indexer {
await BlocksRepository.$migrateBlocks();
// do not wait for classify blocks to finish
void blocks.$classifyBlocks();
void blocks.$updateBlocksMissingCoinbaseBip54();
runSuccessful = true;
} catch (e) {
nextRunDelay = retryDelay;

View file

@ -314,6 +314,7 @@ export interface BlockExtension {
avgFee: number;
avgFeeRate: number;
coinbaseRaw: string;
coinbaseBip54?: boolean;
orphans: OrphanedBlock[] | null;
coinbaseAddress: string | null;
coinbaseAddresses: string[] | null;

View file

@ -61,6 +61,7 @@ interface DatabaseBlock {
totalInputAmt: number;
firstSeen: string; // UNIX_TIMESTAMP() returns a string when applied to datetime(6)
stale: boolean;
coinbaseBip54: number | null;
}
const BLOCK_DB_FIELDS = `
@ -106,7 +107,8 @@ const BLOCK_DB_FIELDS = `
blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt,
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen,
blocks.stale
blocks.stale,
blocks.coinbase_bip_54 AS coinbaseBip54
`;
class BlocksRepository {
@ -132,7 +134,7 @@ class BlocksRepository {
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii, definition_hash, index_version,
stale, first_seen
stale, first_seen, coinbase_bip_54
) VALUE (
?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?,
@ -144,7 +146,7 @@ class BlocksRepository {
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, FROM_UNIXTIME(?)
?, FROM_UNIXTIME(?), ?
)`;
const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
@ -194,7 +196,8 @@ class BlocksRepository {
poolsUpdater.currentSha,
BlocksRepository.version,
(block.stale ? 1 : 0),
block.extras.firstSeen === null ? 1 : block.extras.firstSeen // Sentinel value 1 indicates that we could not find first seen time
block.extras.firstSeen === null ? 1 : block.extras.firstSeen, // Sentinel value 1 indicates that we could not find first seen time
block.extras.coinbaseBip54 ?? null
];
await DB.query(query, params);
@ -1294,6 +1297,7 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0;
extras.coinbaseBip54 = dbBlk.coinbaseBip54 === null ? undefined : !!dbBlk.coinbaseBip54;
extras.firstSeen = null;
if (config.CORE_RPC.DEBUG_LOG_PATH) {
@ -1426,6 +1430,44 @@ class BlocksRepository {
}
return blocksMigrated;
}
/** @asyncSafe */
public async $getBlocksMissingCoinbaseBip54(): Promise<BlockExtended[]> {
const query = `SELECT ${BLOCK_DB_FIELDS} FROM blocks
JOIN pools ON blocks.pool_id = pools.id
where blocks.coinbase_bip_54 IS NULL AND
blocks.height > 0 AND
blocks.blockTimestamp >= FROM_UNIXTIME(1771507776) AND
blocks.stale = 0
ORDER BY blocks.height DESC`;
try {
const [rows]: any[] = await DB.query(query);
const blocks: BlockExtended[] = [];
for (const dbBlock of rows) {
blocks.push(await this.formatDbBlockIntoExtendedBlock(dbBlock as DatabaseBlock));
}
return blocks;
} catch (e) {
logger.err(`Cannot get blocks with missing bip54 flag. Reason: ` + (e instanceof Error ? e.message : e));
}
return [];
}
public async $updateCoinbaseBip54(result: boolean, hash: string): Promise<void> {
const query = `UPDATE blocks SET coinbase_bip_54 = ? WHERE hash = ?`;
const params = [result, hash];
try {
await DB.query(query, params);
} catch (e) {
logger.err(`Couldn't update coinbaseBip54 field for block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
export default new BlocksRepository();

View file

@ -212,6 +212,16 @@
</span>
</td>
</tr>
@if (network !== 'liquid' && network !== 'liquidtestnet' && block.extras.coinbaseBip54) {
<tr>
<td i18n="transaction.features|Transaction features">Features</td>
<td>
<span class="badge bg-success" i18n="block.bip54" i18n-ngbTooltip="bip54-tooltip" ngbTooltip="Coinbase is forward-compatible with BIP-54 (nLockTime = height 1, nSequence ≠ 0xffffffff)." placement="bottom">
BIP-54 coinbase
</span>
</td>
</tr>
}
</ng-container>
<ng-template #loadingRest>
<tr>

View file

@ -215,6 +215,7 @@ export interface BlockExtension {
feeRange?: number[];
reward?: number;
coinbaseRaw?: string;
coinbaseBip54?: boolean;
matchRate?: number;
expectedFees?: number;
expectedWeight?: number;