From fbebfdae043d523c0d8463318fa48b4459482c0d Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 26 Feb 2024 15:54:10 +0100 Subject: [PATCH 01/19] [accelerator] add pagination support in acceleration list --- .../accelerations-list.component.html | 9 +++ .../accelerations-list.component.ts | 64 +++++++++++-------- .../accelerator-dashboard.component.html | 2 +- .../src/app/interfaces/node-api.interface.ts | 11 ++-- .../src/app/services/services-api.service.ts | 4 ++ 5 files changed, 59 insertions(+), 31 deletions(-) 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 9a919ca54..5481bed53 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 @@ -17,6 +17,7 @@ Bid Boost Block Status + Date @@ -52,6 +53,9 @@ Mined Canceled + + + @@ -75,6 +79,11 @@ + + +

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 c1ab011ea..ae601fc62 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 @@ -1,5 +1,5 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; -import { Observable, catchError, of, switchMap, tap } from 'rxjs'; +import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { StateService } from '../../../services/state.service'; import { WebsocketService } from '../../../services/websocket.service'; @@ -21,9 +21,10 @@ export class AccelerationsListComponent implements OnInit { isLoading = true; paginationMaxSize: number; page = 1; - lastPage = 1; + accelerationCount: number; maxSize = window.innerWidth <= 767.98 ? 3 : 5; skeletonLines: number[] = []; + pageSubject: BehaviorSubject = new BehaviorSubject(this.page); constructor( private servicesApiService: ServicesApiServices, @@ -40,34 +41,45 @@ export class AccelerationsListComponent implements OnInit { this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; - - const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistory$({ timeframe: '1m' })); - this.accelerationList$ = accelerationObservable$.pipe( - switchMap(accelerations => { - if (this.pending) { - for (const acceleration of accelerations) { - acceleration.status = acceleration.status || 'accelerating'; - } - } - for (const acc of accelerations) { - acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee; - } - if (this.widget) { - return of(accelerations.slice(-6).reverse()); - } else { - return of(accelerations.reverse()); - } - }), - catchError((err) => { - this.isLoading = false; - return of([]); - }), - tap(() => { - this.isLoading = false; + + this.accelerationList$ = this.pageSubject.pipe( + switchMap((page) => { + const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1m', page: page })); + return accelerationObservable$.pipe( + switchMap(response => { + const accelerations = response.body; + this.accelerationCount = parseInt(response.headers.get('x-total-count'), 10); + console.log(this.accelerationCount); + if (this.pending) { + for (const acceleration of accelerations) { + acceleration.status = acceleration.status || 'accelerating'; + } + } + for (const acc of accelerations) { + acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee; + } + if (this.widget) { + return of(accelerations.slice(0, 6)); + } else { + return of(accelerations); + } + }), + catchError((err) => { + this.isLoading = false; + return of([]); + }), + tap(() => { + this.isLoading = false; + }) + ); }) ); } + pageChange(page: number): void { + this.pageSubject.next(page); + } + trackByBlock(index: number, block: BlockExtended): number { return block.height; } 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 6d9e49265..0fd70269c 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 @@ -84,7 +84,7 @@ - + diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 6ef650e32..e5764e785 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -393,8 +393,11 @@ export interface Acceleration { } export interface AccelerationHistoryParams { - timeframe?: string, - status?: string, - pool?: string, - blockHash?: string, + status?: string; + timeframe?: string; + poolUniqueId?: number; + blockHash?: string; + blockHeight?: number; + page?: number; + pageLength?: number; } \ No newline at end of file diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index f11b3460c..a1f023b9b 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -147,4 +147,8 @@ export class ServicesApiServices { getAccelerationHistory$(params: AccelerationHistoryParams): Observable { return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } }); } + + getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable { + return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'}); + } } From b3b65c59dcc29889c43045784073bf9276988221 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 26 Feb 2024 16:23:00 +0100 Subject: [PATCH 02/19] [accelerator] improve dashboard responsiveness a bit --- .../accelerations-list.component.html | 4 +- .../accelerations-list.component.scss | 60 +++++++++---------- .../accelerations-list.component.ts | 8 ++- 3 files changed, 37 insertions(+), 35 deletions(-) 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 5481bed53..c5862cb24 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 @@ -17,7 +17,7 @@ Bid Boost Block Status - Date + Date @@ -53,7 +53,7 @@ Mined Canceled - + diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss index 110ff033c..85e655b25 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.scss @@ -63,16 +63,28 @@ tr, td, th { } .txid { - @media (max-width: 500px) { + width: 20%; +} + +.fee { + width: 15%; +} + +.block { + width: 15%; + @media (max-width: 700px) { display: none; } } -.fee, .block, .status { - width: 15%; +.status { + width: 13%; +} - @media (max-width: 720px) { - width: 20%; +.date { + width: 20%; + @media (max-width: 600px) { + display: none; } } @@ -83,23 +95,12 @@ tr, td, th { text-overflow: ellipsis; white-space: nowrap; max-width: 30%; - @media (max-width: 1060px) and (min-width: 768px) { - display: none; - } - @media (max-width: 500px) { - display: none; - } } .fee-rate { width: 20%; - @media (max-width: 1060px) and (min-width: 768px) { - text-align: start !important; - } - @media (max-width: 500px) { - text-align: start !important; - } - @media (max-width: 840px) and (min-width: 768px) { + text-align: end !important; + @media (max-width: 975px) and (min-width: 768px) { display: none; } @media (max-width: 410px) { @@ -108,32 +109,31 @@ tr, td, th { } .bid { + text-align: end !important; width: 30%; min-width: 150px; - @media (max-width: 840px) and (min-width: 768px) { - text-align: start !important; - } - @media (max-width: 410px) { - text-align: start !important; - } } .time { width: 25%; + @media (max-width: 600px) { + display: none; + } + @media (max-width: 1200px) and (min-width: 768px) { + display: none; + } } .fee { width: 30%; - @media (max-width: 1060px) and (min-width: 768px) { - text-align: start !important; - } - @media (max-width: 500px) { - text-align: start !important; - } + text-align: end !important; } .block { width: 20%; + @media (max-width: 1200px) and (min-width: 768px) { + display: none; + } } .status { 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 ae601fc62..a04e45150 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 @@ -47,9 +47,11 @@ export class AccelerationsListComponent implements OnInit { const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1m', page: page })); return accelerationObservable$.pipe( switchMap(response => { - const accelerations = response.body; - this.accelerationCount = parseInt(response.headers.get('x-total-count'), 10); - console.log(this.accelerationCount); + let accelerations = response; + if (response.body) { + accelerations = response.body; + this.accelerationCount = parseInt(response.headers.get('x-total-count'), 10); + } if (this.pending) { for (const acceleration of accelerations) { acceleration.status = acceleration.status || 'accelerating'; From e26fcff063ebf3354aad2b006b28c08402c309d3 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 26 Feb 2024 16:40:22 +0100 Subject: [PATCH 03/19] [accelerator] cleanup status --- .../accelerations-list/accelerations-list.component.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 c5862cb24..95a9d0efe 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 @@ -17,7 +17,7 @@ Bid Boost Block Status - Date + Requested @@ -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 @@ -
+

Accelerations

From 3f8a4b511900cea9e68e24960b2b63f2c7d8dc81 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 27 Feb 2024 15:20:15 +0100 Subject: [PATCH 05/19] [accelerator] move accel stats into backend --- .../acceleration-stats.component.html | 6 +-- .../acceleration-stats.component.ts | 43 ++++++------------- .../accelerator-dashboard.component.html | 2 +- .../accelerator-dashboard.component.ts | 4 +- .../src/app/services/services-api.service.ts | 5 +++ 5 files changed, 23 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html index 5e8aa729a..fef91acc0 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html @@ -3,16 +3,16 @@
Requests
-
{{ stats.count }}
+
{{ stats.totalRequested }}
accelerated
Total Bid Boost
-
{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} BTC
+
{{ stats.totalBidBoost / 100_000_000 | amountShortener: 4 }} BTC
- +
diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts index 0a6ef065c..29909ff39 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts @@ -1,9 +1,12 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { Observable, of } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; -import { ApiService } from '../../../services/api.service'; -import { StateService } from '../../../services/state.service'; -import { Acceleration } from '../../../interfaces/node-api.interface'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ServicesApiServices } from '../../../services/services-api.service'; + +export type AccelerationStats = { + totalRequested: number; + totalBidBoost: number; + successRate: number; +} @Component({ selector: 'app-acceleration-stats', @@ -12,35 +15,13 @@ import { Acceleration } from '../../../interfaces/node-api.interface'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AccelerationStatsComponent implements OnInit { - @Input() timespan: '24h' | '1w' | '1m' = '24h'; - @Input() accelerations$: Observable; - public accelerationStats$: Observable; + accelerationStats$: Observable; constructor( - private apiService: ApiService, - private stateService: StateService, + private servicesApiService: ServicesApiServices ) { } ngOnInit(): void { - this.accelerationStats$ = this.accelerations$.pipe( - switchMap(accelerations => { - let totalFeesPaid = 0; - let totalSucceeded = 0; - let totalCanceled = 0; - for (const acc of accelerations) { - if (acc.status === 'completed') { - totalSucceeded++; - totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0; - } else if (acc.status === 'failed') { - totalCanceled++; - } - } - return of({ - count: totalSucceeded, - totalFeesPaid, - successRate: (totalSucceeded + totalCanceled > 0) ? ((totalSucceeded / (totalSucceeded + totalCanceled)) * 100) : 0.0, - }); - }) - ); + this.accelerationStats$ = this.servicesApiService.getAccelerationStats$(); } } 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 0fd70269c..0afae6e7b 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 @@ -27,7 +27,7 @@
- +
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 a2abc657a..57cb605ca 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' }).pipe( + return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m', page: 1, pageLength: 100}).pipe( catchError(() => { return of([]); }), @@ -71,7 +71,7 @@ export class AcceleratorDashboardComponent implements OnInit { this.minedAccelerations$ = this.accelerations$.pipe( map(accelerations => { - return accelerations.filter(acc => ['mined', 'completed', 'failed'].includes(acc.status)); + return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status)); }) ); diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index a1f023b9b..a2e62cf36 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -7,6 +7,7 @@ import { MenuGroup } from '../interfaces/services.interface'; import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; import { IBackendInfo } from '../interfaces/websocket.interface'; import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; +import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom'; export interface IUser { @@ -151,4 +152,8 @@ export class ServicesApiServices { getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable { return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'}); } + + getAccelerationStats$(): Observable { + return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/stats`); + } } From 867e9594302b7afdf0bca8a0f52a63d2ae688df2 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 27 Feb 2024 15:22:59 +0100 Subject: [PATCH 06/19] [accelerator] /accelerator/stats -> /accelerator/accelerations/stats --- frontend/src/app/services/services-api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index a2e62cf36..5ec1e4240 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -154,6 +154,6 @@ export class ServicesApiServices { } getAccelerationStats$(): Observable { - return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/stats`); + return this.httpClient.get(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`); } } From 093649136e5ee619edded265ef57ab005e9ce242 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Thu, 29 Feb 2024 18:42:29 -0800 Subject: [PATCH 07/19] Push the latest tag to Docker Hub again --- .github/workflows/on-tag.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 919130c53..55a5585cc 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -100,5 +100,6 @@ jobs: --cache-to "type=local,dest=/tmp/.buildx-cache" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ + --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --output "type=registry" ./${{ matrix.service }}/ \ --build-arg commitHash=$SHORT_SHA From cda5448f134831c000f3a11aea75d53f47a6d3d3 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 1 Mar 2024 11:04:40 +0100 Subject: [PATCH 08/19] [accelerator] fix accel bid boost chart height --- .../acceleration-fees-graph.component.scss | 4 +++- .../acceleration-fees-graph.component.ts | 8 ++++---- .../accelerator-dashboard.component.ts | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss index e01beb350..11b468a24 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.scss @@ -25,7 +25,8 @@ flex-direction: column; padding: 0px 15px; width: 100%; - height: calc(100vh - 250px); + height: calc(100vh - 225px); + min-height: 400px; @media (min-width: 992px) { height: calc(100vh - 150px); } @@ -35,6 +36,7 @@ display: flex; flex: 1; width: 100%; + height: 100%; padding-bottom: 20px; padding-right: 10px; @media (max-width: 992px) { 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 e786635ba..de1202193 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 @@ -29,7 +29,7 @@ import { ApiService } from '../../../services/api.service'; }) export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { @Input() widget: boolean = false; - @Input() height: number | string = '200'; + @Input() height: number = 300; @Input() right: number | string = 45; @Input() left: number | string = 75; @Input() accelerations$: Observable; @@ -177,11 +177,11 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { ], animation: false, grid: { - height: this.height, + height: (this.widget && this.height) ? this.height - 30 : undefined, + top: this.widget ? 20 : 40, + bottom: this.widget ? 30 : 80, right: this.right, left: this.left, - bottom: this.widget ? 30 : 80, - top: this.widget ? 20 : (this.isMobile() ? 10 : 50), }, tooltip: { show: !this.isMobile(), 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 57cb605ca..04fa2a4cd 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 @@ -128,11 +128,11 @@ export class AcceleratorDashboardComponent implements OnInit { @HostListener('window:resize', ['$event']) onResize(): void { if (window.innerWidth >= 992) { - this.graphHeight = 330; + this.graphHeight = 380; } else if (window.innerWidth >= 768) { - this.graphHeight = 245; + this.graphHeight = 300; } else { - this.graphHeight = 210; + this.graphHeight = 270; } } } From 8bd939b3741ff00c95b4ddcec0d898ff68a7b67b Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 1 Mar 2024 12:53:29 +0100 Subject: [PATCH 09/19] [accelerator] show 3m stats, fix accel fee chart --- .../acceleration-fees-graph.component.html | 3 + .../acceleration-fees-graph.component.ts | 204 ++++++------------ .../accelerator-dashboard.component.html | 5 +- .../accelerator-dashboard.component.ts | 2 +- .../src/app/services/services-api.service.ts | 4 + 5 files changed, 70 insertions(+), 148 deletions(-) diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html index 3698a3060..faf48eac7 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html @@ -23,6 +23,9 @@ +
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,