@@ -50,7 +50,8 @@
Pending
- Mined
+ Mined
+ Completed
Canceled
|
From ab0c3eeab6ab17fee82ae7c3e6f31c12f2dfe6e0 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Mon, 26 Feb 2024 16:47:19 +0100
Subject: [PATCH 04/19] [accelerator] cap max width acceleration history list
for now
---
.../accelerations-list/accelerations-list.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html
index 95a9d0efe..177eee973 100644
--- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html
+++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html
@@ -1,4 +1,4 @@
-
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
index de1202193..3a8107634 100644
--- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
+++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
-import { EChartsOption, graphic } from 'echarts';
+import { EChartsOption } from 'echarts';
import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs';
-import { map, max, startWith, switchMap, tap } from 'rxjs/operators';
+import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../../services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@@ -11,7 +11,6 @@ import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
-import { ApiService } from '../../../services/api.service';
@Component({
selector: 'app-acceleration-fees-graph',
@@ -55,7 +54,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
- private apiService: ApiService,
private servicesApiService: ServicesApiServices,
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
@@ -69,104 +67,56 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
- this.isLoading = true;
if (this.widget) {
- this.miningWindowPreference = '1m';
- this.timespan = this.miningWindowPreference;
-
- this.statsObservable$ = combineLatest([
- (this.accelerations$ || this.servicesApiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
- this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
- fromEvent(window, 'resize').pipe(startWith(null)),
- ]).pipe(
- tap(([accelerations, blockFeesResponse]) => {
- this.prepareChartOptions(accelerations, blockFeesResponse.body);
- }),
- map(([accelerations, blockFeesResponse]) => {
- return {
- avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0) / accelerations.length
- };
- }),
- );
+ this.miningWindowPreference = '3m';
} else {
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
- this.miningWindowPreference = this.miningService.getDefaultTimespan('1w');
- this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
- this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
- this.route.fragment.subscribe((fragment) => {
- if (['24h', '3d', '1w', '1m'].indexOf(fragment) > -1) {
- this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
- }
- });
- this.statsObservable$ = combineLatest([
- this.radioGroupForm.get('dateSpan').valueChanges.pipe(
- startWith(this.radioGroupForm.controls.dateSpan.value),
- switchMap((timespan) => {
- this.isLoading = true;
- this.storageService.setValue('miningWindowPreference', timespan);
- this.timespan = timespan;
- return this.servicesApiService.getAccelerationHistory$({});
- })
- ),
- this.radioGroupForm.get('dateSpan').valueChanges.pipe(
- startWith(this.radioGroupForm.controls.dateSpan.value),
- switchMap((timespan) => {
- return this.apiService.getHistoricalBlockFees$(timespan);
- })
- )
- ]).pipe(
- tap(([accelerations, blockFeesResponse]) => {
- this.prepareChartOptions(accelerations, blockFeesResponse.body);
- })
- );
+ this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
}
- this.statsSubscription = this.statsObservable$.subscribe(() => {
- this.isLoading = false;
- this.cd.markForCheck();
+ this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
+ this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
+
+ this.route.fragment.subscribe((fragment) => {
+ if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) {
+ this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
+ }
});
+ this.statsObservable$ = combineLatest([
+ this.radioGroupForm.get('dateSpan').valueChanges.pipe(
+ startWith(this.radioGroupForm.controls.dateSpan.value),
+ switchMap((timespan) => {
+ if (!this.widget) {
+ this.storageService.setValue('miningWindowPreference', timespan);
+ }
+ this.isLoading = true;
+ this.timespan = timespan;
+ return this.servicesApiService.getAggregatedAccelerationHistory$({timeframe: this.timespan});
+ })
+ ),
+ fromEvent(window, 'resize').pipe(startWith(null)),
+ ]).pipe(
+ tap(([history]) => {
+ this.isLoading = false;
+ this.prepareChartOptions(history);
+ this.cd.markForCheck();
+ })
+ );
+
+ this.statsObservable$.subscribe();
}
- prepareChartOptions(accelerations, blockFees) {
+ prepareChartOptions(data) {
let title: object;
-
- const blockAccelerations = {};
-
- for (const acceleration of accelerations) {
- if (acceleration.status === 'completed') {
- if (!blockAccelerations[acceleration.blockHeight]) {
- blockAccelerations[acceleration.blockHeight] = [];
- }
- blockAccelerations[acceleration.blockHeight].push(acceleration);
- }
- }
-
- let last = null;
- let minValue = Infinity;
- let maxValue = 0;
- const data = [];
- for (const val of blockFees) {
- if (last == null) {
- last = val.avgHeight;
- }
- let totalFeeDelta = 0;
- let totalFeePaid = 0;
- let totalCount = 0;
- let blockCount = 0;
- while (last <= val.avgHeight) {
- blockCount++;
- totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0);
- totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0);
- totalCount += (blockAccelerations[last] || []).length;
- last++;
- }
- minValue = Math.min(minValue, val.avgFees);
- maxValue = Math.max(maxValue, val.avgFees);
- data.push({
- ...val,
- feeDelta: totalFeeDelta,
- avgFeePaid: (totalFeePaid / blockCount),
- accelerations: totalCount / blockCount,
- });
+ if (data.length === 0) {
+ title = {
+ textStyle: {
+ color: 'grey',
+ fontSize: 15
+ },
+ text: $localize`No accelerated transaction for this timeframe`,
+ left: 'center',
+ top: 'center'
+ };
}
this.chartOptions = {
@@ -197,29 +147,23 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
align: 'left',
},
borderColor: '#000',
- formatter: function (data) {
- if (data.length <= 0) {
- return '';
- }
- let tooltip = `
- ${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))} `;
+ formatter: (ticks) => {
+ let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10) * 1000)} `;
- for (const tick of data.reverse()) {
- if (tick.data[1] >= 1_000_000) {
- tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100_000_000, this.locale, '1.0-3')} BTC `;
- } else {
- tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')} sats `;
- }
+ if (ticks[0].data[1] > 10_000_000) {
+ tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-0')} BTC `;
+ } else {
+ tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.0-0')} sats `;
}
if (['24h', '3d'].includes(this.timespan)) {
- tooltip += `` + $localize`At block: ${data[0].data[2]}` + ``;
+ tooltip += `` + $localize`At block: ${ticks[0].data[2]}` + ``;
} else {
- tooltip += `` + $localize`Around block: ${data[0].data[2]}` + ``;
+ tooltip += `` + $localize`Around block: ${ticks[0].data[2]}` + ``;
}
return tooltip;
- }.bind(this)
+ }
},
xAxis: data.length === 0 ? undefined :
{
@@ -243,15 +187,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
legend: {
data: [
{
- name: 'In-band fees per block',
- inactiveColor: 'rgb(110, 112, 121)',
- textStyle: {
- color: 'white',
- },
- icon: 'roundRect',
- },
- {
- name: 'Total bid boost per block',
+ name: 'Total bid boost',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@@ -260,8 +196,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
},
],
selected: {
- 'In-band fees per block': false,
- 'Total bid boost per block': true,
+ 'Total bid boost': true,
},
show: !this.widget,
},
@@ -304,21 +239,13 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
{
legendHoverLink: false,
zlevel: 1,
- name: 'Total bid boost per block',
- data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]),
+ name: 'Total bid boost',
+ data: data.map(h => {
+ return [h.timestamp * 1000, h.sumBidBoost, h.avgHeight]
+ }),
stack: 'Total',
type: 'bar',
- barWidth: '100%',
- large: true,
- },
- {
- legendHoverLink: false,
- zlevel: 0,
- name: 'In-band fees per block',
- data: data.map(block => [block.timestamp * 1000, block.avgFees, block.avgHeight]),
- stack: 'Total',
- type: 'bar',
- barWidth: '100%',
+ barWidth: '90%',
large: true,
},
],
@@ -347,17 +274,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
}
},
}],
- visualMap: {
- type: 'continuous',
- min: minValue,
- max: maxValue,
- dimension: 1,
- seriesIndex: 1,
- show: false,
- inRange: {
- color: ['#F4511E7f', '#FB8C007f', '#FFB3007f', '#FDD8357f', '#7CB3427f'].reverse() // Gradient color range
- }
- },
};
}
diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html
index 0afae6e7b..5e049198a 100644
--- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html
+++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html
@@ -22,12 +22,12 @@
Acceleration stats
- (1 month)
+ (3 months)
@@ -59,7 +59,6 @@
[height]="graphHeight"
[attr.data-cy]="'acceleration-fees'"
[widget]=true
- [accelerations$]="accelerations$"
>
diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts
index 04fa2a4cd..ba9240d1b 100644
--- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts
+++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts
@@ -60,7 +60,7 @@ export class AcceleratorDashboardComponent implements OnInit {
this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap(() => {
- return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m', page: 1, pageLength: 100}).pipe(
+ return this.serviceApiServices.getAccelerationHistory$({ timeframe: '3m', page: 1, pageLength: 100}).pipe(
catchError(() => {
return of([]);
}),
diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts
index 5ec1e4240..f41c5b42c 100644
--- a/frontend/src/app/services/services-api.service.ts
+++ b/frontend/src/app/services/services-api.service.ts
@@ -145,6 +145,10 @@ export class ServicesApiServices {
return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
}
+ getAggregatedAccelerationHistory$(params: AccelerationHistoryParams): Observable {
+ return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history/aggregated`, { params: { ...params } });
+ }
+
getAccelerationHistory$(params: AccelerationHistoryParams): Observable {
return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } });
}
From d4c01f6b1fc3487a09c87b62d70debc1b00f418b Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Sun, 3 Mar 2024 07:57:36 +0100
Subject: [PATCH 10/19] [accelerator] acceleration list query 1y
---
.../accelerations-list/accelerations-list.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts
index a04e45150..974f9b71b 100644
--- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts
+++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts
@@ -44,7 +44,7 @@ export class AccelerationsListComponent implements OnInit {
this.accelerationList$ = this.pageSubject.pipe(
switchMap((page) => {
- const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1m', page: page }));
+ const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1y', page: page }));
return accelerationObservable$.pipe(
switchMap(response => {
let accelerations = response;
From c74ce7de83db48caf62e027f545846aa74a5dac2 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Sun, 3 Mar 2024 15:40:31 +0100
Subject: [PATCH 11/19] [accelerator] fix tooltip timestamp accel graph
---
.../acceleration-fees-graph.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
index 3a8107634..b958361d1 100644
--- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
+++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
@@ -148,7 +148,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
},
borderColor: '#000',
formatter: (ticks) => {
- let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10) * 1000)} `;
+ let tooltip = `${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))} `;
if (ticks[0].data[1] > 10_000_000) {
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-0')} BTC `;
From f083aa37d63acc4da7cbfe1fc4cd7205393be5a2 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Sun, 3 Mar 2024 15:43:29 +0100
Subject: [PATCH 12/19] [accelerator] fix xaxis type accel graph
---
.../acceleration-fees-graph.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
index b958361d1..001f005a1 100644
--- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
+++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts
@@ -172,7 +172,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
nameTextStyle: {
padding: [10, 0, 0, 0],
},
- type: 'category',
+ type: 'time',
boundaryGap: false,
axisLine: { onZero: true },
axisLabel: {
From 249c57f37659242deebf5ace6201a9509991bc9c Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sat, 24 Feb 2024 22:47:50 +0000
Subject: [PATCH 13/19] Add non-standard Goggles filter
---
backend/src/api/common.ts | 157 ++++++++++++++++++
backend/src/mempool.interfaces.ts | 1 +
.../mempool-block-overview.component.html | 1 +
frontend/src/app/shared/filters.utils.ts | 4 +-
4 files changed, 162 insertions(+), 1 deletion(-)
diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts
index 45a3eb19b..72178df3e 100644
--- a/backend/src/api/common.ts
+++ b/backend/src/api/common.ts
@@ -7,6 +7,24 @@ import { isIP } from 'net';
import transactionUtils from './transaction-utils';
import { isPoint } from '../utils/secp256k1';
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);
+const MIN_STANDARD_TX_NONWITNESS_SIZE = 65;
+const MAX_P2SH_SIGOPS = 15;
+const MAX_STANDARD_P2WSH_STACK_ITEMS = 100;
+const MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80;
+const MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80;
+const MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600;
+const MAX_STANDARD_SCRIPTSIG_SIZE = 1650;
+const DUST_RELAY_TX_FEE = 3;
+const MAX_OP_RETURN_RELAY = 83;
+const DEFAULT_PERMIT_BAREMULTISIG = true;
+
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
@@ -177,6 +195,141 @@ export class Common {
);
}
+ /**
+ * Validates most standardness rules
+ *
+ * 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)
+ */
+ static isNonStandard(tx: TransactionExtended): boolean {
+ // version
+ if (tx.version > TX_MAX_STANDARD_VERSION) {
+ return true;
+ }
+
+ // tx-size
+ if (tx.weight > MAX_STANDARD_TX_WEIGHT) {
+ return true;
+ }
+
+ // tx-size-small
+ if (this.getNonWitnessSize(tx) < MIN_STANDARD_TX_NONWITNESS_SIZE) {
+ return true;
+ }
+
+ // bad-txns-too-many-sigops
+ if (tx.sigops && tx.sigops > MAX_STANDARD_TX_SIGOPS_COST) {
+ return true;
+ }
+
+ // input validation
+ for (const vin of tx.vin) {
+ if (vin.is_coinbase) {
+ // standardness rules don't apply to coinbase transactions
+ return false;
+ }
+ // scriptsig-size
+ if ((vin.scriptsig.length / 2) > MAX_STANDARD_SCRIPTSIG_SIZE) {
+ return true;
+ }
+ // scriptsig-not-pushonly
+ if (vin.scriptsig_asm) {
+ for (const op of vin.scriptsig_asm.split(' ')) {
+ if (opcodes[op] && opcodes[op] > opcodes['OP_16']) {
+ return true;
+ }
+ }
+ }
+ // bad-txns-nonstandard-inputs
+ if (vin.prevout?.scriptpubkey_type === 'p2sh') {
+ // TODO: evaluate script (https://github.com/bitcoin/bitcoin/blob/1ac627c485a43e50a9a49baddce186ee3ad4daad/src/policy/policy.cpp#L177)
+ // countScriptSigops returns the witness-scaled sigops, so divide by 4 before comparison with MAX_P2SH_SIGOPS
+ const sigops = (transactionUtils.countScriptSigops(vin.inner_redeemscript_asm) / 4);
+ if (sigops > MAX_P2SH_SIGOPS) {
+ return true;
+ }
+ } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
+ return true;
+ }
+ // TODO: bad-witness-nonstandard
+ }
+
+ // output validation
+ let opreturnCount = 0;
+ for (const vout of tx.vout) {
+ // scriptpubkey
+ if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
+ // (non-standard output type)
+ return true;
+ } else if (vout.scriptpubkey_type === 'multisig') {
+ if (!DEFAULT_PERMIT_BAREMULTISIG) {
+ // bare-multisig
+ return true;
+ }
+ const mOfN = parseMultisigScript(vout.scriptpubkey_asm);
+ if (!mOfN || mOfN.n < 1 || mOfN.n > 3 || mOfN.m < 1 || mOfN.m > mOfN.n) {
+ // (non-standard bare multisig threshold)
+ return true;
+ }
+ } else if (vout.scriptpubkey_type === 'op_return') {
+ opreturnCount++;
+ if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) {
+ // over default datacarrier limit
+ return true;
+ }
+ }
+ // dust
+ // (we could probably hardcode this for the different output types...)
+ if (vout.scriptpubkey_type !== 'op_return') {
+ let dustSize = (vout.scriptpubkey.length / 2);
+ // add varint length overhead
+ dustSize += getVarIntLength(dustSize);
+ // add value size
+ dustSize += 8;
+ if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) {
+ dustSize += 67;
+ } else {
+ dustSize += 148;
+ }
+ if (vout.value < (dustSize * DUST_RELAY_TX_FEE)) {
+ // under minimum output size
+ console.log(`NON-STANDARD | dust | ${vout.value} | ${dustSize} ${dustSize * DUST_RELAY_TX_FEE} `, tx.txid);
+ return true;
+ }
+ }
+ }
+
+ // multi-op-return
+ if (opreturnCount > 1) {
+ return true;
+ }
+
+ // TODO: non-mandatory-script-verify-flag
+
+ return false;
+ }
+
+ static getNonWitnessSize(tx: TransactionExtended): number {
+ let weight = tx.weight;
+ let hasWitness = false;
+ for (const vin of tx.vin) {
+ if (vin.witness?.length) {
+ hasWitness = true;
+ // witness count
+ weight -= getVarIntLength(vin.witness.length);
+ for (const witness of vin.witness) {
+ // witness item size + content
+ weight -= getVarIntLength(witness.length / 2) + (witness.length / 2);
+ }
+ }
+ }
+ if (hasWitness) {
+ // marker & segwit flag
+ weight -= 2;
+ }
+ return Math.ceil(weight / 4);
+ }
+
static setSegwitSighashFlags(flags: bigint, witness: string[]): bigint {
for (const w of witness) {
if (this.isDERSig(w)) {
@@ -351,6 +504,10 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
+ if (this.isNonStandard(tx)) {
+ flags |= TransactionFlags.nonstandard;
+ }
+
return Number(flags);
}
diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts
index 114e0ab88..b68b137bb 100644
--- a/backend/src/mempool.interfaces.ts
+++ b/backend/src/mempool.interfaces.ts
@@ -209,6 +209,7 @@ export const TransactionFlags = {
v1: 0b00000100n,
v2: 0b00001000n,
v3: 0b00010000n,
+ nonstandard: 0b00100000n,
// address types
p2pk: 0b00000001_00000000n,
p2ms: 0b00000010_00000000n,
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
index 7cc458e60..6fb8dd4d6 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
+++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
@@ -8,6 +8,7 @@
[showFilters]="showFilters"
[filterFlags]="filterFlags"
[filterMode]="filterMode"
+ [excludeFilters]="['nonstandard']"
[overrideColors]="overrideColors"
(txClickEvent)="onTxClick($event)"
>
diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts
index 1e55c495b..da22efb66 100644
--- a/frontend/src/app/shared/filters.utils.ts
+++ b/frontend/src/app/shared/filters.utils.ts
@@ -22,6 +22,7 @@ export const TransactionFlags = {
v1: 0b00000100n,
v2: 0b00001000n,
v3: 0b00010000n,
+ nonstandard: 0b00100000n,
// address types
p2pk: 0b00000001_00000000n,
p2ms: 0b00000010_00000000n,
@@ -66,6 +67,7 @@ export const TransactionFilters: { [key: string]: Filter } = {
v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' },
v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' },
v3: { key: 'v3', label: 'Version 3', flag: TransactionFlags.v3, toggle: 'version' },
+ nonstandard: { key: 'nonstandard', label: 'Non-Standard', flag: TransactionFlags.nonstandard, important: true },
/* address types */
p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true },
p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true },
@@ -96,7 +98,7 @@ export const TransactionFilters: { [key: string]: Filter } = {
};
export const FilterGroups: { label: string, filters: Filter[]}[] = [
- { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3'] },
+ { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'v3', 'nonstandard'] },
{ label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
{ label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement', 'acceleration'] },
{ label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] },
From 882e8ec59878ea78bd5a4fec30c8756ad55367b3 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Tue, 27 Feb 2024 16:48:52 +0000
Subject: [PATCH 14/19] Check in missing script utility file
---
backend/src/utils/bitcoin-script.ts | 203 ++++++++++++++++++++++++++++
1 file changed, 203 insertions(+)
create mode 100644 backend/src/utils/bitcoin-script.ts
diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts
new file mode 100644
index 000000000..3414e8269
--- /dev/null
+++ b/backend/src/utils/bitcoin-script.ts
@@ -0,0 +1,203 @@
+const opcodes = {
+ OP_FALSE: 0,
+ OP_0: 0,
+ OP_PUSHDATA1: 76,
+ OP_PUSHDATA2: 77,
+ OP_PUSHDATA4: 78,
+ OP_1NEGATE: 79,
+ OP_PUSHNUM_NEG1: 79,
+ OP_RESERVED: 80,
+ OP_TRUE: 81,
+ OP_1: 81,
+ OP_2: 82,
+ OP_3: 83,
+ OP_4: 84,
+ OP_5: 85,
+ OP_6: 86,
+ OP_7: 87,
+ OP_8: 88,
+ OP_9: 89,
+ OP_10: 90,
+ OP_11: 91,
+ OP_12: 92,
+ OP_13: 93,
+ OP_14: 94,
+ OP_15: 95,
+ OP_16: 96,
+ OP_PUSHNUM_1: 81,
+ OP_PUSHNUM_2: 82,
+ OP_PUSHNUM_3: 83,
+ OP_PUSHNUM_4: 84,
+ OP_PUSHNUM_5: 85,
+ OP_PUSHNUM_6: 86,
+ OP_PUSHNUM_7: 87,
+ OP_PUSHNUM_8: 88,
+ OP_PUSHNUM_9: 89,
+ OP_PUSHNUM_10: 90,
+ OP_PUSHNUM_11: 91,
+ OP_PUSHNUM_12: 92,
+ OP_PUSHNUM_13: 93,
+ OP_PUSHNUM_14: 94,
+ OP_PUSHNUM_15: 95,
+ OP_PUSHNUM_16: 96,
+ OP_NOP: 97,
+ OP_VER: 98,
+ OP_IF: 99,
+ OP_NOTIF: 100,
+ OP_VERIF: 101,
+ OP_VERNOTIF: 102,
+ OP_ELSE: 103,
+ OP_ENDIF: 104,
+ OP_VERIFY: 105,
+ OP_RETURN: 106,
+ OP_TOALTSTACK: 107,
+ OP_FROMALTSTACK: 108,
+ OP_2DROP: 109,
+ OP_2DUP: 110,
+ OP_3DUP: 111,
+ OP_2OVER: 112,
+ OP_2ROT: 113,
+ OP_2SWAP: 114,
+ OP_IFDUP: 115,
+ OP_DEPTH: 116,
+ OP_DROP: 117,
+ OP_DUP: 118,
+ OP_NIP: 119,
+ OP_OVER: 120,
+ OP_PICK: 121,
+ OP_ROLL: 122,
+ OP_ROT: 123,
+ OP_SWAP: 124,
+ OP_TUCK: 125,
+ OP_CAT: 126,
+ OP_SUBSTR: 127,
+ OP_LEFT: 128,
+ OP_RIGHT: 129,
+ OP_SIZE: 130,
+ OP_INVERT: 131,
+ OP_AND: 132,
+ OP_OR: 133,
+ OP_XOR: 134,
+ OP_EQUAL: 135,
+ OP_EQUALVERIFY: 136,
+ OP_RESERVED1: 137,
+ OP_RESERVED2: 138,
+ OP_1ADD: 139,
+ OP_1SUB: 140,
+ OP_2MUL: 141,
+ OP_2DIV: 142,
+ OP_NEGATE: 143,
+ OP_ABS: 144,
+ OP_NOT: 145,
+ OP_0NOTEQUAL: 146,
+ OP_ADD: 147,
+ OP_SUB: 148,
+ OP_MUL: 149,
+ OP_DIV: 150,
+ OP_MOD: 151,
+ OP_LSHIFT: 152,
+ OP_RSHIFT: 153,
+ OP_BOOLAND: 154,
+ OP_BOOLOR: 155,
+ OP_NUMEQUAL: 156,
+ OP_NUMEQUALVERIFY: 157,
+ OP_NUMNOTEQUAL: 158,
+ OP_LESSTHAN: 159,
+ OP_GREATERTHAN: 160,
+ OP_LESSTHANOREQUAL: 161,
+ OP_GREATERTHANOREQUAL: 162,
+ OP_MIN: 163,
+ OP_MAX: 164,
+ OP_WITHIN: 165,
+ OP_RIPEMD160: 166,
+ OP_SHA1: 167,
+ OP_SHA256: 168,
+ OP_HASH160: 169,
+ OP_HASH256: 170,
+ OP_CODESEPARATOR: 171,
+ OP_CHECKSIG: 172,
+ OP_CHECKSIGVERIFY: 173,
+ OP_CHECKMULTISIG: 174,
+ OP_CHECKMULTISIGVERIFY: 175,
+ OP_NOP1: 176,
+ OP_NOP2: 177,
+ OP_CHECKLOCKTIMEVERIFY: 177,
+ OP_CLTV: 177,
+ OP_NOP3: 178,
+ OP_CHECKSEQUENCEVERIFY: 178,
+ OP_CSV: 178,
+ OP_NOP4: 179,
+ OP_NOP5: 180,
+ OP_NOP6: 181,
+ OP_NOP7: 182,
+ OP_NOP8: 183,
+ OP_NOP9: 184,
+ OP_NOP10: 185,
+ OP_CHECKSIGADD: 186,
+ OP_PUBKEYHASH: 253,
+ OP_PUBKEY: 254,
+ OP_INVALIDOPCODE: 255,
+};
+// add unused opcodes
+for (let i = 187; i <= 255; i++) {
+ opcodes[`OP_RETURN_${i}`] = i;
+}
+
+export { opcodes };
+
+/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */
+export function parseMultisigScript(script: string): void | { m: number, n: number } {
+ if (!script) {
+ return;
+ }
+ const ops = script.split(' ');
+ if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
+ return;
+ }
+ const opN = ops.pop();
+ if (!opN) {
+ return;
+ }
+ if (!opN.startsWith('OP_PUSHNUM_')) {
+ return;
+ }
+ const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
+ if (ops.length < n * 2 + 1) {
+ return;
+ }
+ // pop n public keys
+ for (let i = 0; i < n; i++) {
+ if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop() || '')) {
+ return;
+ }
+ if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop() || '')) {
+ return;
+ }
+ }
+ const opM = ops.pop();
+ if (!opM) {
+ return;
+ }
+ if (!opM.startsWith('OP_PUSHNUM_')) {
+ return;
+ }
+ const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
+
+ if (ops.length) {
+ return;
+ }
+
+ return { m, n };
+}
+
+export function getVarIntLength(n: number): number {
+ if (n < 0xfd) {
+ return 1;
+ } else if (n <= 0xffff) {
+ return 3;
+ } else if (n <= 0xffffffff) {
+ return 5;
+ } else {
+ return 9;
+ }
+}
\ No newline at end of file
From eadbc139fad71ee9ce4288d480888d5dc52c3b9a Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 3 Mar 2024 16:01:13 +0000
Subject: [PATCH 15/19] Handle missing inner_redeemscript
---
backend/src/api/transaction-utils.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts
index 6ff1c10b7..9107f2ae7 100644
--- a/backend/src/api/transaction-utils.ts
+++ b/backend/src/api/transaction-utils.ts
@@ -145,6 +145,10 @@ class TransactionUtils {
}
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
+ if (!script?.length) {
+ return 0;
+ }
+
let sigops = 0;
// count OP_CHECKSIG and OP_CHECKSIGVERIFY
sigops += (script.match(/OP_CHECKSIG/g)?.length || 0);
From 8626846264fa7ac5033bc63dfdc306fc150ffd92 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 3 Mar 2024 16:40:28 +0000
Subject: [PATCH 16/19] Hide effective rate on tooltip where irrelevant
---
frontend/src/app/components/block-overview-graph/tx-view.ts | 2 +-
.../block-overview-tooltip.component.html | 2 +-
.../block-overview-tooltip.component.ts | 4 ++++
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts
index f422e2cbe..b6008ef1d 100644
--- a/frontend/src/app/components/block-overview-graph/tx-view.ts
+++ b/frontend/src/app/components/block-overview-graph/tx-view.ts
@@ -59,7 +59,7 @@ export default class TxView implements TransactionStripped {
this.acc = tx.acc;
this.rate = tx.rate;
this.status = tx.status;
- this.bigintFlags = tx.flags ? (BigInt(tx.flags) ^ (this.acc ? TransactionFlags.acceleration : 0n)): 0n;
+ this.bigintFlags = tx.flags ? (BigInt(tx.flags) | (this.acc ? TransactionFlags.acceleration : 0n)): 0n;
this.initialised = false;
this.vertexArray = scene.vertexArray;
diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
index 9e3e94111..8fb687ebd 100644
--- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
+++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html
@@ -28,7 +28,7 @@
|
-
+
Effective fee rate |
Accelerated fee rate |
diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts
index a6e2a2697..6b23276c8 100644
--- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts
+++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts
@@ -2,6 +2,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStra
import { Position } from '../../components/block-overview-graph/sprite-types.js';
import { Price } from '../../services/price.service';
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
+import { TransactionFlags } from '../../shared/filters.utils';
@Component({
selector: 'app-block-overview-tooltip',
@@ -22,6 +23,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
feeRate = 0;
effectiveRate;
acceleration;
+ hasEffectiveRate: boolean = false;
tooltipPosition: Position = { x: 0, y: 0 };
@@ -55,6 +57,8 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.feeRate = this.fee / this.vsize;
this.effectiveRate = tx.rate;
this.acceleration = tx.acc;
+ this.hasEffectiveRate = Math.abs((this.fee / this.vsize) - this.effectiveRate) > 0.05
+ || (tx.bigintFlags && (tx.bigintFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n);
}
}
}
From 130bdac58cc7a833bfbac5ecff00b54355c67891 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Tue, 27 Feb 2024 18:09:15 +0000
Subject: [PATCH 17/19] Calculate & save acceleration bid boost rates
---
backend/src/api/acceleration.ts | 738 ++++++++++++++++++
backend/src/api/database-migration.ts | 24 +-
backend/src/api/websocket-handler.ts | 25 +-
.../repositories/AccelerationRepository.ts | 43 +
4 files changed, 828 insertions(+), 2 deletions(-)
create mode 100644 backend/src/api/acceleration.ts
create mode 100644 backend/src/repositories/AccelerationRepository.ts
diff --git a/backend/src/api/acceleration.ts b/backend/src/api/acceleration.ts
new file mode 100644
index 000000000..2b032d09f
--- /dev/null
+++ b/backend/src/api/acceleration.ts
@@ -0,0 +1,738 @@
+import logger from '../logger';
+import { MempoolTransactionExtended } from '../mempool.interfaces';
+import { IEsploraApi } from './bitcoin/esplora-api.interface';
+
+const BLOCK_WEIGHT_UNITS = 4_000_000;
+const BLOCK_SIGOPS = 80_000;
+const MAX_RELATIVE_GRAPH_SIZE = 200;
+const BID_BOOST_WINDOW = 40_000;
+const BID_BOOST_MIN_OFFSET = 10_000;
+const BID_BOOST_MAX_OFFSET = 400_000;
+
+type Acceleration = {
+ txid: string;
+ max_bid: number;
+};
+
+interface TxSummary {
+ txid: string; // txid of the current transaction
+ effectiveVsize: number; // Total vsize of the dependency tree
+ effectiveFee: number; // Total fee of the dependency tree in sats
+ ancestorCount: number; // Number of ancestors
+}
+
+export interface AccelerationInfo {
+ txSummary: TxSummary;
+ targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
+ nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
+ cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
+}
+
+interface GraphTx {
+ txid: string;
+ vsize: number;
+ weight: number;
+ fees: {
+ base: number;
+ };
+ depends: string[];
+ spentby: string[];
+}
+
+interface MempoolTx extends GraphTx {
+ ancestorcount: number;
+ ancestorsize: number;
+ fees: {
+ base: number;
+ ancestor: number;
+ };
+
+ ancestors: Map,
+ ancestorRate: number;
+ individualRate: number;
+ score: number;
+}
+
+class AccelerationCosts {
+ /**
+ * Takes a list of accelerations and verbose block data
+ * Returns the "fair" boost rate to charge accelerations
+ *
+ * @param accelerationsx
+ * @param verboseBlock
+ */
+ public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
+ // Run GBT ourselves to calculate accurate effective fee rates
+ // the list of transactions comes from a mined block, so we already know everything fits within consensus limits
+ const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);
+
+ // initialize working maps for fast tx lookups
+ const accMap = {};
+ const txMap = {};
+ for (const acceleration of accelerations) {
+ accMap[acceleration.txid] = acceleration;
+ }
+ for (const tx of template) {
+ txMap[tx.txid] = tx;
+ }
+
+ // Identify and exclude accelerated and otherwise prioritized transactions
+ const excludeMap = {};
+ let totalWeight = 0;
+ let minAcceleratedPackage = Infinity;
+ let lastEffectiveRate = 0;
+ // Iterate over the mined template from bottom to top.
+ // Transactions should appear in ascending order of mining priority.
+ for (const blockTx of [...blockTxs].reverse()) {
+ const txid = blockTx.txid;
+ const tx = txMap[txid];
+ totalWeight += tx.weight;
+ const isAccelerated = accMap[txid] != null;
+ // If a cluster has a in-band effective fee rate than the previous cluster,
+ // it must have been prioritized out-of-band (in order to have a higher mining priority)
+ // so exclude from the analysis.
+ const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
+ if (isPrioritized || isAccelerated) {
+ let packageWeight = 0;
+ // exclude this whole CPFP cluster
+ for (const clusterTxid of tx.cluster) {
+ packageWeight += txMap[clusterTxid].weight;
+ if (!excludeMap[clusterTxid]) {
+ excludeMap[clusterTxid] = true;
+ }
+ }
+ // keep track of the smallest accelerated CPFP cluster for later
+ if (isAccelerated) {
+ minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
+ }
+ }
+ if (!isPrioritized) {
+ if (!isAccelerated || !lastEffectiveRate) {
+ lastEffectiveRate = tx.effectiveFeePerVsize;
+ }
+ }
+ }
+
+ // The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
+ // where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
+ // then taking the average fee rate of the following BID_BOOST_WINDOW weight units
+ // (ignoring accelerated transactions and their ancestors).
+ //
+ // Transactions within the offset might pay less than the fair rate due to bin-packing effects
+ // But the average rate paid by the next chunk of non-accelerated transactions provides a good
+ // upper bound on the "next best rate" of alternatives to including the accelerated transactions
+ // (since, if there were any better options, they would have been included instead)
+ const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
+ const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
+ const leftBound = windowOffset;
+ const rightBound = windowOffset + BID_BOOST_WINDOW;
+ let totalFeeInWindow = 0;
+ let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
+ let txIndex = blockTxs.length - 1;
+ for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
+ const txid = blockTxs[txIndex].txid;
+ const tx = txMap[txid];
+ if (excludeMap[txid]) {
+ // skip prioritized transactions and their ancestors
+ continue;
+ }
+
+ const left = offset;
+ const right = offset + tx.weight;
+ offset += tx.weight;
+ if (right < leftBound) {
+ // not within window yet
+ continue;
+ }
+ if (left > rightBound) {
+ // past window
+ break;
+ }
+ // count fees for weight units within the window
+ const overlapLeft = Math.max(leftBound, left);
+ const overlapRight = Math.min(rightBound, right);
+ const overlapUnits = overlapRight - overlapLeft;
+ totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
+ totalWeightInWindow += overlapUnits;
+ }
+
+ if (totalWeightInWindow < BID_BOOST_WINDOW) {
+ // not enough un-prioritized transactions to calculate a fair rate
+ // just charge everyone their max bids
+ return Infinity;
+ }
+ // Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
+ const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
+ return averageRate;
+ }
+
+
+ /**
+ * Takes an accelerated mined txid and a target rate
+ * Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
+ *
+ * @param txid
+ * @param medianFeeRate
+ */
+ public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
+ // Get same-block transaction ancestors
+ const allRelatives = this.getSameBlockRelatives(tx, transactions);
+ const relativesMap = this.initializeRelatives(allRelatives);
+ const rootTx = relativesMap.get(tx.txid) as MempoolTx;
+
+ // Calculate cost to boost
+ return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
+ }
+
+ /**
+ * Takes a raw transaction, and builds a graph of same-block relatives,
+ * and returns as a MempoolTx
+ *
+ * @param tx
+ */
+ private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map {
+ const blockTxs = new Map(); // map of txs in this block
+ const spendMap = new Map(); // map of outpoints to spending txids
+ for (const tx of transactions) {
+ blockTxs.set(tx.txid, tx);
+ for (const vin of tx.vin) {
+ spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
+ }
+ }
+
+ const relatives: Map = new Map();
+ const stack: string[] = [tx.txid];
+
+ // build set of same-block ancestors
+ while (stack.length > 0) {
+ const nextTxid = stack.pop();
+ const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
+ if (!nextTx || relatives.has(nextTx.txid)) {
+ continue;
+ }
+
+ const mempoolTx = this.convertToGraphTx(nextTx);
+
+ mempoolTx.fees.base = nextTx.fee || 0;
+ mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
+ mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];
+
+ for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
+ if (txid) {
+ stack.push(txid);
+ }
+ }
+
+ relatives.set(mempoolTx.txid, mempoolTx);
+ }
+
+ return relatives;
+ }
+
+ /**
+ * Takes a raw transaction and converts it to MempoolTx format
+ * fee and ancestor data is initialized with dummy/null values
+ *
+ * @param tx
+ */
+ private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
+ return {
+ txid: tx.txid,
+ vsize: tx.vsize,
+ weight: tx.weight,
+ fees: {
+ base: 0, // dummy
+ },
+ depends: [], // dummy
+ spentby: [], //dummy
+ };
+ }
+
+ private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
+ return {
+ ...tx,
+ fees: {
+ base: tx.fees.base,
+ ancestor: tx.fees.base,
+ },
+ ancestorcount: 1,
+ ancestorsize: tx.vsize,
+ ancestors: new Map(),
+ ancestorRate: 0,
+ individualRate: 0,
+ score: 0,
+ };
+ }
+
+ /**
+ * Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
+ * Calculate the minimum set of transactions to fee-bump, their total vsize + fees
+ *
+ * @param tx
+ * @param ancestors
+ */
+ private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map, targetFeeRate: number): AccelerationInfo {
+ // add root tx to the ancestor map
+ relatives.set(tx.txid, tx);
+
+ // Check for high-sigop transactions (not supported)
+ relatives.forEach(entry => {
+ if (entry.vsize > Math.ceil(entry.weight / 4)) {
+ throw new Error(`high_sigop_tx`);
+ }
+ });
+
+ // Initialize individual & ancestor fee rates
+ relatives.forEach(entry => this.setAncestorScores(entry));
+
+ // Sort by descending ancestor score
+ let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
+
+ let includedInCluster: Map | null = null;
+
+ // While highest score >= targetFeeRate
+ let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
+ while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
+ maxIterations--;
+ // Grab the highest scoring entry
+ const best = sortedRelatives.shift();
+ if (best) {
+ const cluster = new Map(best.ancestors?.entries() || []);
+ if (best.ancestors.has(tx.txid)) {
+ includedInCluster = cluster;
+ }
+ cluster.set(best.txid, best);
+ // Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
+ // and update scores, ancestor totals and dependencies for the survivors
+ this.removeAncestors(cluster, relatives);
+
+ // re-sort
+ sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
+ }
+ }
+
+ // sanity check for infinite loops / too many ancestors (should never happen)
+ if (maxIterations <= 0) {
+ logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
+ throw new Error('invalid_tx_dependencies');
+ }
+
+ let totalFee = Math.round(tx.fees.ancestor * 100_000_000);
+
+ // transaction is already CPFP-d above the target rate by some descendant
+ if (includedInCluster) {
+ let clusterSize = 0;
+ let clusterFee = 0;
+ includedInCluster.forEach(entry => {
+ clusterSize += entry.vsize;
+ clusterFee += (entry.fees.base * 100_000_000);
+ });
+ const clusterRate = clusterFee / clusterSize;
+ totalFee = Math.ceil(tx.ancestorsize * clusterRate);
+ }
+
+ // Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
+ // Cost = (totalVsize * targetFeeRate) - totalFee
+ return {
+ txSummary: {
+ txid: tx.txid,
+ effectiveVsize: tx.ancestorsize,
+ effectiveFee: totalFee,
+ ancestorCount: tx.ancestorcount,
+ },
+ cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
+ targetFeeRate,
+ nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
+ };
+ }
+
+ /**
+ * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
+ * for each transaction.
+ *
+ * @param tx
+ * @param all
+ */
+ private setAncestors(tx: MempoolTx, all: Map, visited: Map>, depth: number = 0): Map {
+ // sanity check for infinite recursion / too many ancestors (should never happen)
+ if (depth >= 100) {
+ logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
+ throw new Error('invalid_tx_dependencies');
+ }
+
+ // initialize the ancestor map for this tx
+ tx.ancestors = new Map();
+ tx.depends.forEach(parentId => {
+ const parent = all.get(parentId);
+ if (parent) {
+ // add the parent
+ tx.ancestors?.set(parentId, parent);
+ // check for a cached copy of this parent's ancestors
+ let ancestors = visited.get(parent.txid);
+ if (!ancestors) {
+ // recursively fetch the parent's ancestors
+ ancestors = this.setAncestors(parent, all, visited, depth + 1);
+ }
+ // and add to this tx's map
+ ancestors.forEach((ancestor, ancestorId) => {
+ tx.ancestors?.set(ancestorId, ancestor);
+ });
+ }
+ });
+ visited.set(tx.txid, tx.ancestors);
+
+ return tx.ancestors;
+ }
+
+ /**
+ * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
+ * by running setAncestors on each leaf, and caching intermediate results.
+ * then initializes ancestor data for each transaction
+ *
+ * @param all
+ */
+ private initializeRelatives(all: Map): Map {
+ const mempoolTxs = new Map();
+ all.forEach(entry => {
+ mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
+ });
+ const visited: Map> = new Map();
+ const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
+ for (const leaf of leaves) {
+ this.setAncestors(leaf, mempoolTxs, visited);
+ }
+ mempoolTxs.forEach(entry => {
+ entry.ancestors?.forEach(ancestor => {
+ entry.ancestorcount++;
+ entry.ancestorsize += ancestor.vsize;
+ entry.fees.ancestor += ancestor.fees.base;
+ });
+ this.setAncestorScores(entry);
+ });
+ return mempoolTxs;
+ }
+
+ /**
+ * Remove a cluster of transactions from an in-mempool dependency graph
+ * and update the survivors' scores and ancestors
+ *
+ * @param cluster
+ * @param ancestors
+ */
+ private removeAncestors(cluster: Map, all: Map): void {
+ // remove
+ cluster.forEach(tx => {
+ all.delete(tx.txid);
+ });
+
+ // update survivors
+ all.forEach(tx => {
+ cluster.forEach(remove => {
+ if (tx.ancestors?.has(remove.txid)) {
+ // remove as dependency
+ tx.ancestors.delete(remove.txid);
+ tx.depends = tx.depends.filter(parent => parent !== remove.txid);
+ // update ancestor sizes and fees
+ tx.ancestorsize -= remove.vsize;
+ tx.fees.ancestor -= remove.fees.base;
+ }
+ });
+ // recalculate fee rates
+ this.setAncestorScores(tx);
+ });
+ }
+
+ /**
+ * Take a mempool transaction, and set the fee rates and ancestor score
+ *
+ * @param tx
+ */
+ private setAncestorScores(tx: MempoolTx): void {
+ tx.individualRate = (tx.fees.base * 100_000_000) / tx.vsize;
+ tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize;
+ tx.score = Math.min(tx.individualRate, tx.ancestorRate);
+ }
+
+ // Sort by descending score
+ private mempoolComparator(a, b): number {
+ return b.score - a.score;
+ }
+}
+
+export default new AccelerationCosts;
+
+interface TemplateTransaction {
+ txid: string;
+ order: number;
+ weight: number;
+ adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
+ sigops: number;
+ fee: number;
+ feeDelta: number;
+ ancestors: string[];
+ cluster: string[];
+ effectiveFeePerVsize: number;
+}
+
+interface MinerTransaction extends TemplateTransaction {
+ inputs: string[];
+ feePerVsize: number;
+ relativesSet: boolean;
+ ancestorMap: Map;
+ children: Set;
+ ancestorFee: number;
+ ancestorVsize: number;
+ ancestorSigops: number;
+ score: number;
+ used: boolean;
+ modified: boolean;
+ dependencyRate: number;
+}
+
+/*
+* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
+* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
+*/
+function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
+ const auditPool: Map = new Map();
+ const mempoolArray: MinerTransaction[] = [];
+
+ candidates.forEach(tx => {
+ // initializing everything up front helps V8 optimize property access later
+ const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
+ const feePerVsize = (tx.fee / adjustedVsize);
+ auditPool.set(tx.txid, {
+ txid: tx.txid,
+ order: txidToOrdering(tx.txid),
+ fee: tx.fee,
+ feeDelta: 0,
+ weight: tx.weight,
+ adjustedVsize,
+ feePerVsize: feePerVsize,
+ effectiveFeePerVsize: feePerVsize,
+ dependencyRate: feePerVsize,
+ sigops: tx.sigops || 0,
+ inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
+ relativesSet: false,
+ ancestors: [],
+ cluster: [],
+ ancestorMap: new Map(),
+ children: new Set(),
+ ancestorFee: 0,
+ ancestorVsize: 0,
+ ancestorSigops: 0,
+ score: 0,
+ used: false,
+ modified: false,
+ });
+ mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
+ });
+
+ // set accelerated effective fee
+ for (const acceleration of accelerations) {
+ const tx = auditPool.get(acceleration.txid);
+ if (tx) {
+ tx.feeDelta = acceleration.max_bid;
+ tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
+ tx.effectiveFeePerVsize = tx.feePerVsize;
+ tx.dependencyRate = tx.feePerVsize;
+ }
+ }
+
+ // Build relatives graph & calculate ancestor scores
+ for (const tx of mempoolArray) {
+ if (!tx.relativesSet) {
+ setRelatives(tx, auditPool);
+ }
+ }
+
+ // Sort by descending ancestor score
+ mempoolArray.sort(priorityComparator);
+
+ // Build blocks by greedily choosing the highest feerate package
+ // (i.e. the package rooted in the transaction with the best ancestor score)
+ const blocks: number[][] = [];
+ let blockWeight = 0;
+ let blockSigops = 0;
+ const transactions: MinerTransaction[] = [];
+ let modified: MinerTransaction[] = [];
+ const overflow: MinerTransaction[] = [];
+ let failures = 0;
+ while (mempoolArray.length || modified.length) {
+ // skip invalid transactions
+ while (mempoolArray[0].used || mempoolArray[0].modified) {
+ mempoolArray.shift();
+ }
+
+ // Select best next package
+ let nextTx;
+ const nextPoolTx = mempoolArray[0];
+ const nextModifiedTx = modified[0];
+ if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
+ nextTx = nextPoolTx;
+ mempoolArray.shift();
+ } else {
+ modified.shift();
+ if (nextModifiedTx) {
+ nextTx = nextModifiedTx;
+ }
+ }
+
+ if (nextTx && !nextTx?.used) {
+ // Check if the package fits into this block
+ if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
+ const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
+ // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
+ const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
+ const clusterTxids = sortedTxSet.map(tx => tx.txid);
+ const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
+ const used: MinerTransaction[] = [];
+ while (sortedTxSet.length) {
+ const ancestor = sortedTxSet.pop();
+ if (!ancestor) {
+ continue;
+ }
+ ancestor.used = true;
+ ancestor.usedBy = nextTx.txid;
+ // update this tx with effective fee rate & relatives data
+ if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
+ ancestor.effectiveFeePerVsize = effectiveFeeRate;
+ }
+ ancestor.cluster = clusterTxids;
+ transactions.push(ancestor);
+ blockWeight += ancestor.weight;
+ blockSigops += ancestor.sigops;
+ used.push(ancestor);
+ }
+
+ // remove these as valid package ancestors for any descendants remaining in the mempool
+ if (used.length) {
+ used.forEach(tx => {
+ modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
+ });
+ }
+
+ failures = 0;
+ } else {
+ // hold this package in an overflow list while we check for smaller options
+ overflow.push(nextTx);
+ failures++;
+ }
+ }
+
+ // this block is full
+ const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
+ const queueEmpty = !mempoolArray.length && !modified.length;
+
+ if (exceededPackageTries || queueEmpty) {
+ break;
+ }
+ }
+
+ for (const tx of transactions) {
+ tx.ancestors = Object.values(tx.ancestorMap);
+ }
+
+ return transactions;
+}
+
+// traverse in-mempool ancestors
+// recursion unavoidable, but should be limited to depth < 25 by mempool policy
+function setRelatives(
+ tx: MinerTransaction,
+ mempool: Map,
+): void {
+ for (const parent of tx.inputs) {
+ const parentTx = mempool.get(parent);
+ if (parentTx && !tx.ancestorMap?.has(parent)) {
+ tx.ancestorMap.set(parent, parentTx);
+ parentTx.children.add(tx);
+ // visit each node only once
+ if (!parentTx.relativesSet) {
+ setRelatives(parentTx, mempool);
+ }
+ parentTx.ancestorMap.forEach((ancestor) => {
+ tx.ancestorMap.set(ancestor.txid, ancestor);
+ });
+ }
+ };
+ tx.ancestorFee = (tx.fee + tx.feeDelta);
+ tx.ancestorVsize = tx.adjustedVsize || 0;
+ tx.ancestorSigops = tx.sigops || 0;
+ tx.ancestorMap.forEach((ancestor) => {
+ tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
+ tx.ancestorVsize += ancestor.adjustedVsize;
+ tx.ancestorSigops += ancestor.sigops;
+ });
+ tx.score = tx.ancestorFee / tx.ancestorVsize;
+ tx.relativesSet = true;
+}
+
+// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
+// avoids recursion to limit call stack depth
+function updateDescendants(
+ rootTx: MinerTransaction,
+ mempool: Map,
+ modified: MinerTransaction[],
+ clusterRate: number,
+): MinerTransaction[] {
+ const descendantSet: Set = new Set();
+ // stack of nodes left to visit
+ const descendants: MinerTransaction[] = [];
+ let descendantTx: MinerTransaction | undefined;
+ rootTx.children.forEach(childTx => {
+ if (!descendantSet.has(childTx)) {
+ descendants.push(childTx);
+ descendantSet.add(childTx);
+ }
+ });
+ while (descendants.length) {
+ descendantTx = descendants.pop();
+ if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
+ // remove tx as ancestor
+ descendantTx.ancestorMap.delete(rootTx.txid);
+ descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
+ descendantTx.ancestorVsize -= rootTx.adjustedVsize;
+ descendantTx.ancestorSigops -= rootTx.sigops;
+ descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
+ descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;
+
+ if (!descendantTx.modified) {
+ descendantTx.modified = true;
+ modified.push(descendantTx);
+ }
+
+ // add this node's children to the stack
+ descendantTx.children.forEach(childTx => {
+ // visit each node only once
+ if (!descendantSet.has(childTx)) {
+ descendants.push(childTx);
+ descendantSet.add(childTx);
+ }
+ });
+ }
+ }
+ // return new, resorted modified list
+ return modified.sort(priorityComparator);
+}
+
+// Used to sort an array of MinerTransactions by descending ancestor score
+function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
+ if (b.score === a.score) {
+ // tie-break by txid for stability
+ return a.order - b.order;
+ } else {
+ return b.score - a.score;
+ }
+}
+
+// returns the most significant 4 bytes of the txid as an integer
+function txidToOrdering(txid: string): number {
+ return parseInt(
+ txid.substring(62, 64) +
+ txid.substring(60, 62) +
+ txid.substring(58, 60) +
+ txid.substring(56, 58),
+ 16
+ );
+}
diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts
index 9a5eb310a..539316e39 100644
--- a/backend/src/api/database-migration.ts
+++ b/backend/src/api/database-migration.ts
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
- private static currentVersion = 68;
+ private static currentVersion = 69;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -580,6 +580,11 @@ class DatabaseMigration {
await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`);
await this.updateToSchemaVersion(68);
}
+
+ if (databaseSchemaVersion < 69 && config.MEMPOOL.NETWORK === 'mainnet') {
+ await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
+ await this.updateToSchemaVersion(69);
+ }
}
/**
@@ -1123,6 +1128,23 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
+ private getCreateAccelerationsTableQuery(): string {
+ return `CREATE TABLE IF NOT EXISTS accelerations (
+ txid varchar(65) NOT NULL,
+ added date NOT NULL,
+ height int(10) NOT NULL,
+ pool smallint unsigned NULL,
+ effective_vsize int(10) NOT NULL,
+ effective_fee bigint(20) unsigned NOT NULL,
+ boost_rate float unsigned,
+ boost_cost bigint(20) unsigned NOT NULL,
+ PRIMARY KEY (txid),
+ INDEX (added),
+ INDEX (height),
+ INDEX (pool)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
+ }
+
public async $blocksReindexingTruncate(): Promise {
logger.warn(`Truncating pools, blocks, hashrates and difficulty_adjustments tables for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`);
await Common.sleep$(5000);
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index b78389b64..5c007fadd 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -24,6 +24,8 @@ import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
+import accelerationCosts from './acceleration';
+import accelerationRepository from '../repositories/AccelerationRepository';
interface AddressTransactions {
mempool: MempoolTransactionExtended[],
@@ -728,6 +730,28 @@ class WebsocketHandler {
const _memPool = memPool.getMempool();
+ const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
+
+
+ if (isAccelerated) {
+ const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
+ for (const tx of transactions) {
+ blockTxs[tx.txid] = tx;
+ }
+ const accelerations = Object.values(mempool.getAccelerations());
+ const boostRate = accelerationCosts.calculateBoostRate(
+ accelerations.map(acc => ({ txid: acc.txid, max_bid: acc.feeDelta })),
+ transactions
+ );
+ for (const acc of accelerations) {
+ if (blockTxs[acc.txid]) {
+ const tx = blockTxs[acc.txid];
+ const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
+ accelerationRepository.$saveAcceleration(accelerationInfo, block, block.extras.pool.id);
+ }
+ }
+ }
+
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
memPool.handleMinedRbfTransactions(rbfTransactions);
memPool.removeFromSpendMap(transactions);
@@ -735,7 +759,6 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
let auditMempool = _memPool;
- const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
// template calculation functions have mempool side effects, so calculate audits using
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts
new file mode 100644
index 000000000..9fbed4673
--- /dev/null
+++ b/backend/src/repositories/AccelerationRepository.ts
@@ -0,0 +1,43 @@
+import { AccelerationInfo } from '../api/acceleration';
+import { ResultSetHeader, RowDataPacket } from 'mysql2';
+import DB from '../database';
+import logger from '../logger';
+import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
+
+class AccelerationRepository {
+ public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise {
+ try {
+ await DB.query(`
+ INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost)
+ VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE
+ added = FROM_UNIXTIME(?),
+ height = ?,
+ pool = ?,
+ effective_vsize = ?,
+ effective_fee = ?,
+ boost_rate = ?,
+ boost_cost = ?
+ `, [
+ acceleration.txSummary.txid,
+ block.timestamp,
+ block.height,
+ pool_id,
+ acceleration.txSummary.effectiveVsize,
+ acceleration.txSummary.effectiveFee,
+ acceleration.targetFeeRate, acceleration.cost,
+ block.timestamp,
+ block.height,
+ pool_id,
+ acceleration.txSummary.effectiveVsize,
+ acceleration.txSummary.effectiveFee,
+ acceleration.targetFeeRate, acceleration.cost,
+ ]);
+ } catch (e: any) {
+ logger.err(`Cannot save acceleration (${acceleration.txSummary.txid}) into db. Reason: ` + (e instanceof Error ? e.message : e));
+ // We don't throw, not a critical issue if we miss some accelerations
+ }
+ }
+}
+
+export default new AccelerationRepository();
From bd0de745e2e1b18bdd56eaf41b5ee306de6baf5c Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Wed, 28 Feb 2024 20:43:32 +0000
Subject: [PATCH 18/19] Add local acceleration info APIs
---
backend/src/api/mining/mining-routes.ts | 51 ++++++++++++++
.../repositories/AccelerationRepository.ts | 66 +++++++++++++++++++
2 files changed, 117 insertions(+)
diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts
index bb78de44a..6b9ef7b9f 100644
--- a/backend/src/api/mining/mining-routes.ts
+++ b/backend/src/api/mining/mining-routes.ts
@@ -8,6 +8,7 @@ import HashratesRepository from '../../repositories/HashratesRepository';
import bitcoinClient from '../bitcoin/bitcoin-client';
import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
+import AccelerationRepository from '../../repositories/AccelerationRepository';
class MiningRoutes {
public initRoutes(app: Application) {
@@ -34,6 +35,10 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
.get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice)
+
+ .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/pool/:slug', this.$getAccelerationsByPool)
+ .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight)
+ .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations)
;
}
@@ -352,6 +357,52 @@ class MiningRoutes {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
+
+ private async $getAccelerationsByPool(req: Request, res: Response): Promise {
+ try {
+ res.header('Pragma', 'public');
+ res.header('Cache-control', 'public');
+ res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
+ if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
+ res.status(400).send('Acceleration data is not available.');
+ return;
+ }
+ res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
+ } catch (e) {
+ res.status(500).send(e instanceof Error ? e.message : e);
+ }
+ }
+
+ private async $getAccelerationsByHeight(req: Request, res: Response): Promise {
+ try {
+ res.header('Pragma', 'public');
+ res.header('Cache-control', 'public');
+ res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
+ if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
+ res.status(400).send('Acceleration data is not available.');
+ return;
+ }
+ const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
+ res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
+ } catch (e) {
+ res.status(500).send(e instanceof Error ? e.message : e);
+ }
+ }
+
+ private async $getRecentAccelerations(req: Request, res: Response): Promise {
+ try {
+ res.header('Pragma', 'public');
+ res.header('Cache-control', 'public');
+ res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
+ if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
+ res.status(400).send('Acceleration data is not available.');
+ return;
+ }
+ res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
+ } catch (e) {
+ res.status(500).send(e instanceof Error ? e.message : e);
+ }
+ }
}
export default new MiningRoutes();
diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts
index 9fbed4673..868f8526f 100644
--- a/backend/src/repositories/AccelerationRepository.ts
+++ b/backend/src/repositories/AccelerationRepository.ts
@@ -3,6 +3,22 @@ import { ResultSetHeader, RowDataPacket } from 'mysql2';
import DB from '../database';
import logger from '../logger';
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
+import { Common } from '../api/common';
+import config from '../config';
+
+export interface PublicAcceleration {
+ txid: string,
+ height: number,
+ pool: {
+ id: number,
+ slug: string,
+ name: string,
+ },
+ effective_vsize: number,
+ effective_fee: number,
+ boost_rate: number,
+ boost_cost: number,
+}
class AccelerationRepository {
public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise {
@@ -38,6 +54,56 @@ class AccelerationRepository {
// We don't throw, not a critical issue if we miss some accelerations
}
}
+
+ public async $getAccelerationInfo(poolSlug: string | null = null, height: number | null = null, interval: string | null = null): Promise {
+ interval = Common.getSqlInterval(interval);
+
+ if (!config.MEMPOOL_SERVICES.ACCELERATIONS || (interval == null && poolSlug == null && height == null)) {
+ return [];
+ }
+
+ let query = `
+ SELECT * FROM accelerations
+ JOIN pools on pools.unique_id = accelerations.pool
+ `;
+ let params: any[] = [];
+
+ if (interval) {
+ query += ` WHERE accelerations.added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() `;
+ } else if (height != null) {
+ query += ` WHERE accelerations.height = ? `;
+ params.push(height);
+ } else if (poolSlug != null) {
+ query += ` WHERE pools.slug = ? `;
+ params.push(poolSlug);
+ }
+
+ query += ` ORDER BY accelerations.added DESC `;
+
+ try {
+ const [rows] = await DB.query(query, params) as RowDataPacket[][];
+ if (rows?.length) {
+ return rows.map(row => ({
+ txid: row.txid,
+ height: row.height,
+ pool: {
+ id: row.id,
+ slug: row.slug,
+ name: row.name,
+ },
+ effective_vsize: row.effective_vsize,
+ effective_fee: row.effective_fee,
+ boost_rate: row.boost_rate,
+ boost_cost: row.boost_cost,
+ }));
+ } else {
+ return [];
+ }
+ } catch (e) {
+ logger.err(`Cannot query acceleration info. Reason: ` + (e instanceof Error ? e.message : e));
+ throw e;
+ }
+ }
}
export default new AccelerationRepository();
From f923ee4edef8bb945f8c42b61372d53d06fcaf99 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Mon, 4 Mar 2024 16:32:43 +0000
Subject: [PATCH 19/19] Accelerator audit use datetime for added column
---
backend/src/api/database-migration.ts | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts
index 539316e39..861830226 100644
--- a/backend/src/api/database-migration.ts
+++ b/backend/src/api/database-migration.ts
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
- private static currentVersion = 69;
+ private static currentVersion = 70;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -585,6 +585,11 @@ class DatabaseMigration {
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
await this.updateToSchemaVersion(69);
}
+
+ if (databaseSchemaVersion < 70 && config.MEMPOOL.NETWORK === 'mainnet') {
+ await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
+ await this.updateToSchemaVersion(70);
+ }
}
/**
@@ -1131,7 +1136,7 @@ class DatabaseMigration {
private getCreateAccelerationsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS accelerations (
txid varchar(65) NOT NULL,
- added date NOT NULL,
+ added datetime NOT NULL,
height int(10) NOT NULL,
pool smallint unsigned NULL,
effective_vsize int(10) NOT NULL,
|