Compare commits
6 Commits
mononaut/s
...
natsoni/bl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b3e52436 | ||
|
|
d04e5128ba | ||
|
|
e3c3f31ddb | ||
|
|
70d1f52268 | ||
|
|
e44f30d7a7 | ||
|
|
099d84a395 |
@@ -219,10 +219,10 @@ class Blocks {
|
||||
};
|
||||
}
|
||||
|
||||
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
|
||||
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
|
||||
return {
|
||||
id: hash,
|
||||
transactions: Common.classifyTransactions(transactions),
|
||||
transactions: Common.classifyTransactions(transactions, height),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -616,7 +616,7 @@ class Blocks {
|
||||
// add CPFP
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||
@@ -653,7 +653,7 @@ class Blocks {
|
||||
}
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||
for (const tx of classifiedTxs) {
|
||||
classifiedTxMap[tx.txid] = tx;
|
||||
@@ -912,7 +912,7 @@ class Blocks {
|
||||
}
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
|
||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
@@ -1169,7 +1169,7 @@ class Blocks {
|
||||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
let flags: number = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -1188,7 +1188,7 @@ class Blocks {
|
||||
} else {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(hash, txs);
|
||||
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
@@ -1324,7 +1324,7 @@ class Blocks {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
||||
@@ -10,7 +10,6 @@ import logger from '../logger';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
@@ -200,10 +199,13 @@ export class Common {
|
||||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*
|
||||
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||
* For now, just pull out individual rules into versioned functions where necessary.
|
||||
*/
|
||||
static isNonStandard(tx: TransactionExtended): boolean {
|
||||
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
if (this.isNonStandardVersion(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -250,6 +252,8 @@ export class Common {
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
@@ -335,6 +339,49 @@ export class Common {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Individual versioned standardness rules
|
||||
|
||||
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
|
||||
let TX_MAX_STANDARD_VERSION = 3;
|
||||
if (
|
||||
height != null
|
||||
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
TX_MAX_STANDARD_VERSION = 2;
|
||||
}
|
||||
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
|
||||
if (
|
||||
height != null
|
||||
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
@@ -415,7 +462,7 @@ export class Common {
|
||||
return flags;
|
||||
}
|
||||
|
||||
static getTransactionFlags(tx: TransactionExtended): number {
|
||||
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
@@ -548,7 +595,7 @@ export class Common {
|
||||
if (hasFakePubkey) {
|
||||
flags |= TransactionFlags.fake_pubkey;
|
||||
}
|
||||
|
||||
|
||||
// fast but bad heuristic to detect possible coinjoins
|
||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||
@@ -564,17 +611,17 @@ export class Common {
|
||||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (this.isNonStandard(tx)) {
|
||||
if (this.isNonStandard(tx, height)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
return Number(flags);
|
||||
}
|
||||
|
||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
||||
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||
let flags = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -585,8 +632,8 @@ export class Common {
|
||||
};
|
||||
}
|
||||
|
||||
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
|
||||
return txs.map(Common.classifyTransaction);
|
||||
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||
}
|
||||
|
||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||
|
||||
@@ -30,6 +30,7 @@ class MiningRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustment/:height', this.$getDifficultyAdjustment)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlocksHealth)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
|
||||
@@ -297,6 +298,18 @@ class MiningRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getDifficultyAdjustment(req: Request, res: Response) {
|
||||
try {
|
||||
const adjustment = await DifficultyAdjustmentsRepository.$getAdjustmentAtHeight(parseInt(req.params.height, 10));
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
res.json(adjustment);
|
||||
} catch (e) {
|
||||
res.status(e instanceof Error && e.message === 'not found' ? 204 : 500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getRewardStats(req: Request, res: Response) {
|
||||
try {
|
||||
const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10));
|
||||
|
||||
@@ -1106,7 +1106,7 @@ class BlocksRepository {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
||||
@@ -88,6 +88,22 @@ class DifficultyAdjustmentsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAdjustmentAtHeight(height: number): Promise<IndexedDifficultyAdjustment> {
|
||||
try {
|
||||
if (isNaN(height)) {
|
||||
throw new Error(`argument must be a number`);
|
||||
}
|
||||
const [rows] = await DB.query(`SELECT * FROM difficulty_adjustments WHERE height = ?`, [height]);
|
||||
if (!rows[0]) {
|
||||
throw new Error(`not found`);
|
||||
}
|
||||
return rows[0] as IndexedDifficultyAdjustment;
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot get difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAdjustmentsHeights(): Promise<number[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
|
||||
|
||||
@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
||||
@@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
return;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
||||
@@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
}
|
||||
}
|
||||
const opM = ops.pop();
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||
|
||||
@@ -78,6 +78,25 @@
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
|
||||
<td i18n="mining.difficulty-adjustment">Adjustment</td>
|
||||
<td>
|
||||
<ng-container *ngIf="cacheService.daCache[block.height]?.adjustment > 0; else loadingAdjustment">
|
||||
<div [style.color]="cacheService.daCache[block.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[block.height].adjustment < 1 ? 'var(--red)' : '')">
|
||||
@if (cacheService.daCache[block.height].adjustment > 1) {
|
||||
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
} @else if (cacheService.daCache[block.height].adjustment < 1) {
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
}
|
||||
{{ (cacheService.daCache[block.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loadingAdjustment>
|
||||
<span class="skeleton-loader" style="max-width: 60px"></span>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #skeletonRows>
|
||||
<tr>
|
||||
@@ -193,6 +212,10 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
|
||||
<td i18n="block.difficulty">Difficulty</td>
|
||||
<td>{{ block.difficulty | amountShortener: 2 }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #loadingRest>
|
||||
<tr>
|
||||
|
||||
@@ -280,3 +280,11 @@ h1 {
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
&.up {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private apiService: ApiService,
|
||||
private priceService: PriceService,
|
||||
private cacheService: CacheService,
|
||||
public cacheService: CacheService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private cd: ChangeDetectorRef,
|
||||
private preloadService: PreloadService,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
|
||||
import { Observable, Subscription, delay, filter, of, retryWhen, switchMap, take, tap, throwError } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { Location } from '@angular/common';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { colorFromRetarget } from '../../shared/common.utils';
|
||||
|
||||
interface BlockchainBlock extends BlockExtended {
|
||||
placeholder?: boolean;
|
||||
@@ -77,6 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
public cacheService: CacheService,
|
||||
public apiService: ApiService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private location: Location,
|
||||
) {
|
||||
@@ -334,6 +337,31 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
|
||||
}
|
||||
|
||||
isDA(height: number): boolean {
|
||||
const isDA = height % 2016 === 0 && this.stateService.network === '';
|
||||
if (isDA && !this.cacheService.daCache[height]?.exact) {
|
||||
const estimatedAdjustment = this.cacheService.daCache[height]?.adjustment || 0;
|
||||
this.cacheService.daCache[height] = { adjustment: estimatedAdjustment, exact: true };
|
||||
this.apiService.getDifficultyAdjustmentByHeight$(height).pipe(
|
||||
switchMap(da => {
|
||||
const blocksAvailable = (this.height || this.chainTip) && this.blockStyles[(this.height || this.chainTip) - height];
|
||||
return blocksAvailable ? of(da) : throwError(() => new Error());
|
||||
}),
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(1000),
|
||||
take(3)
|
||||
)
|
||||
),
|
||||
tap((da) => {
|
||||
this.cacheService.daCache[height] = { adjustment: da?.adjustment || 1, exact: true };
|
||||
this.blockStyles[(this.height || this.chainTip) - height].background = colorFromRetarget(da?.adjustment);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
return isDA;
|
||||
}
|
||||
|
||||
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
|
||||
if (!block || block.placeholder) {
|
||||
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);
|
||||
@@ -349,7 +377,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
return {
|
||||
left: addLeft + this.blockOffset * index + 'px',
|
||||
background: `repeating-linear-gradient(
|
||||
background: this.isDA(block.height) ? colorFromRetarget(this.cacheService.daCache[block.height]?.adjustment || 1) :
|
||||
`repeating-linear-gradient(
|
||||
var(--secondary),
|
||||
var(--secondary) ${greenBackgroundHeight}%,
|
||||
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ApiService } from '../../services/api.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { selectPowerOfTen } from '../../bitcoin.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-difficulty-adjustments-table',
|
||||
@@ -27,7 +28,8 @@ export class DifficultyAdjustmentsTable implements OnInit {
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private apiService: ApiService,
|
||||
public stateService: StateService
|
||||
public stateService: StateService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -52,6 +54,7 @@ export class DifficultyAdjustmentsTable implements OnInit {
|
||||
adjustment[2] / selectedPowerOfTen.divider,
|
||||
this.locale, `1.${decimals}-${decimals}`) + selectedPowerOfTen.unit
|
||||
});
|
||||
this.cacheService.daCache[adjustment[1]] = { adjustment: adjustment[3], exact: true };
|
||||
}
|
||||
this.isLoading = false;
|
||||
return tableData.slice(0, 6);
|
||||
|
||||
@@ -41,6 +41,25 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="this.stateService.network === '' && mempoolBlock.height % 2016 === 0">
|
||||
<td i18n="mining.difficulty-adjustment">Adjustment</td>
|
||||
<td>
|
||||
<ng-container *ngIf="cacheService.daCache[mempoolBlock.height]?.adjustment > 0; else loadingAdjustment">
|
||||
<div [style.color]="cacheService.daCache[mempoolBlock.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[mempoolBlock.height].adjustment < 1 ? 'var(--red)' : '')">
|
||||
@if (cacheService.daCache[mempoolBlock.height].adjustment > 1) {
|
||||
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
} @else if (cacheService.daCache[mempoolBlock.height].adjustment < 1) {
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
}
|
||||
{{ (cacheService.daCache[mempoolBlock.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loadingAdjustment>
|
||||
<span class="skeleton-loader" style="max-width: 60px"></span>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
||||
|
||||
@@ -36,3 +36,11 @@ h1 {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
&.up {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mempool-block',
|
||||
@@ -30,6 +31,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
public cacheService: CacheService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||
import { Subscription, Observable, of, combineLatest, throwError } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EtaService } from '../../services/eta.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { delay, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { feeLevels } from '../../app.constants';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
@@ -12,6 +12,8 @@ import { Location } from '@angular/common';
|
||||
import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { colorFromRetarget } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mempool-blocks',
|
||||
@@ -93,6 +95,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
constructor(
|
||||
private router: Router,
|
||||
public stateService: StateService,
|
||||
public cacheService: CacheService,
|
||||
private etaService: EtaService,
|
||||
private themeService: ThemeService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@@ -387,6 +390,37 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.mempoolBlocksFull.forEach((block, i) => this.mempoolBlockStyles.push(this.getStyleForMempoolBlock(block, i)));
|
||||
}
|
||||
|
||||
isDA(height: number): boolean {
|
||||
if (this.chainTip === -1) {
|
||||
return false;
|
||||
}
|
||||
const isDA = height % 2016 === 0 && this.stateService.network === '';
|
||||
if (isDA && !this.cacheService.daCache[height]) {
|
||||
this.cacheService.daCache[height] = { adjustment: 0 };
|
||||
this.difficultyAdjustments$.pipe(
|
||||
filter(da => !!da),
|
||||
switchMap(da => {
|
||||
const mempoolBlocksAvailable = this.chainTip && this.mempoolBlockStyles[height - this.chainTip - 1];
|
||||
return mempoolBlocksAvailable ? of(da) : throwError(() => new Error());
|
||||
}),
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(100),
|
||||
take(3)
|
||||
)
|
||||
),
|
||||
tap(da => {
|
||||
const adjustment = parseFloat((1 + da.difficultyChange / 100).toFixed(4));
|
||||
if (adjustment !== this.cacheService.daCache[height].adjustment) {
|
||||
this.cacheService.daCache[height].adjustment = adjustment;
|
||||
this.mempoolBlockStyles[height - this.chainTip - 1].background = colorFromRetarget(adjustment);
|
||||
}
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
return isDA;
|
||||
}
|
||||
|
||||
getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) {
|
||||
const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / this.stateService.blockVSize * 100, 0);
|
||||
const usedBlockSpace = 100 - emptyBackgroundSpacePercentage;
|
||||
@@ -410,7 +444,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
return {
|
||||
'right': this.containerOffset + index * this.blockOffset + 'px',
|
||||
'background': backgroundGradients.join(',') + ')'
|
||||
'background': this.isDA(mempoolBlock.height) ? colorFromRetarget(this.cacheService.daCache[mempoolBlock.height]?.adjustment || 1) : backgroundGradients.join(',') + ')'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
|
||||
checkAccelerationEligibility() {
|
||||
if (this.tx) {
|
||||
this.tx.flags = getTransactionFlags(this.tx);
|
||||
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
|
||||
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
|
||||
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
|
||||
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
|
||||
|
||||
@@ -901,7 +901,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
|
||||
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
|
||||
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
|
||||
this.tx.flags = getTransactionFlags(this.tx);
|
||||
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
|
||||
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
|
||||
this.checkAccelerationEligibility();
|
||||
} else {
|
||||
|
||||
@@ -3670,6 +3670,39 @@ export const restApiDocsData = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "mining",
|
||||
httpRequestMethod: "GET",
|
||||
fragment: "get-difficulty-adjustment-by-height",
|
||||
title: "GET Difficulty Adjustment",
|
||||
description: {
|
||||
default: "<p>Returns difficulty adjustment for the block at the specified <code>:blockHeight</code>. If no adjustment happened at that height, an empty response is returned.</p>"
|
||||
},
|
||||
urlString: "/v1/mining/difficulty-adjustment/:blockHeight",
|
||||
showConditions: [""],
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `/api/v1/mining/difficulty-adjustment/%{1}`,
|
||||
commonJS: ``,
|
||||
esModule: ``
|
||||
},
|
||||
codeSampleMainnet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [`756000`],
|
||||
response: `{
|
||||
"time": "2022-09-28T02:56:34.000Z",
|
||||
"height": 756000,
|
||||
"difficulty": 31360548173144.9,
|
||||
"adjustment": 0.97863
|
||||
}`
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "mining",
|
||||
|
||||
@@ -315,6 +315,12 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getDifficultyAdjustmentByHeight$(height: number): Observable<any> {
|
||||
return this.httpClient.get<any>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustment/${height}`
|
||||
);
|
||||
}
|
||||
|
||||
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
|
||||
|
||||
@@ -23,6 +23,7 @@ export class CacheService {
|
||||
blockLoading: { [height: number]: boolean } = {};
|
||||
copiesInBlockQueue: { [height: number]: number } = {};
|
||||
blockPriorities: number[] = [];
|
||||
daCache: { [height: number]: { adjustment: number, exact?: boolean } } = {};
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -128,6 +129,7 @@ export class CacheService {
|
||||
this.blockLoading = {};
|
||||
this.copiesInBlockQueue = {};
|
||||
this.blockPriorities = [];
|
||||
this.daCache = {};
|
||||
}
|
||||
|
||||
getCachedBlock(height) {
|
||||
|
||||
@@ -240,3 +240,25 @@ export function md5(inputString): string {
|
||||
}
|
||||
return rh(a)+rh(b)+rh(c)+rh(d);
|
||||
}
|
||||
|
||||
export function colorFromRetarget(da: number): string {
|
||||
const minDA = 0.95;
|
||||
const maxDA = 1.05;
|
||||
const midDA = 1;
|
||||
|
||||
const red = { r: 220, g: 53, b: 69 };
|
||||
const grey = { r: 108, g: 117, b: 125 };
|
||||
const green = { r: 59, g: 204, b: 73 };
|
||||
|
||||
const interpolateColor = (color1, color2, ratio) => {
|
||||
ratio = Math.min(1, Math.max(0, ratio));
|
||||
const r = Math.round(color1.r + ratio * (color2.r - color1.r));
|
||||
const g = Math.round(color1.g + ratio * (color2.g - color1.g));
|
||||
const b = Math.round(color1.b + ratio * (color2.b - color1.b));
|
||||
return `rgba(${r}, ${g}, ${b}, 0.7)`;
|
||||
}
|
||||
|
||||
return da <= midDA ?
|
||||
interpolateColor(red, grey, (da - minDA) / (midDA - minDA)) :
|
||||
interpolateColor(grey, green, (da - midDA) / (maxDA - midDA));
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
|
||||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
@@ -287,7 +287,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
|
||||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
||||
@@ -2,9 +2,9 @@ import { TransactionFlags } from './filters.utils';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface';
|
||||
import { StateService } from '../services/state.service';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
@@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean {
|
||||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*
|
||||
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||
* For now, just pull out individual rules into versioned functions where necessary.
|
||||
*/
|
||||
export function isNonStandard(tx: Transaction): boolean {
|
||||
export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
if (isNonStandardVersion(tx, height, network)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
} else if (isNonStandardAnchor(tx, height, network)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
@@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Individual versioned standardness rules
|
||||
|
||||
const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean {
|
||||
let TX_MAX_STANDARD_VERSION = 3;
|
||||
if (
|
||||
height != null
|
||||
&& network != null
|
||||
&& V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
&& height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
) {
|
||||
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
TX_MAX_STANDARD_VERSION = 2;
|
||||
}
|
||||
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean {
|
||||
if (
|
||||
height != null
|
||||
&& network != null
|
||||
&& ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
&& height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
) {
|
||||
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
|
||||
// followed by a data push between 2 and 40 bytes.
|
||||
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
|
||||
@@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean {
|
||||
].includes(pubkey);
|
||||
}
|
||||
|
||||
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint {
|
||||
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
@@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
|
||||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (isNonStandard(tx)) {
|
||||
if (isNonStandard(tx, height, network)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user