Skip to content

Commit cd151aa

Browse files
authored
feat: average block times endpoint (#1962)
* feat: average block times endpoint * test: add block times test
1 parent 74c06c6 commit cd151aa

File tree

7 files changed

+187
-1
lines changed

7 files changed

+187
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"last_1h": 846.75,
3+
"last_24h": 635.2,
4+
"last_7d": 731.26,
5+
"last_30d": 738.67
6+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"type": "object",
3+
"title": "AverageBlockTimesResponse",
4+
"description": "Request to fetch average block times (in seconds)",
5+
"additionalProperties": false,
6+
"required": [
7+
"last_1h",
8+
"last_24h",
9+
"last_7d",
10+
"last_30d"
11+
],
12+
"properties": {
13+
"last_1h": {
14+
"type": "number",
15+
"description": "Average block times over the last hour (in seconds)"
16+
},
17+
"last_24h": {
18+
"type": "number",
19+
"description": "Average block times over the last 24 hours (in seconds)"
20+
},
21+
"last_7d": {
22+
"type": "number",
23+
"description": "Average block times over the last 7 days (in seconds)"
24+
},
25+
"last_30d": {
26+
"type": "number",
27+
"description": "Average block times over the last 30 days (in seconds)"
28+
}
29+
}
30+
}

docs/generated.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type SchemaMergeRootStub =
1313
| AddressTransactionsWithTransfersListResponse
1414
| AddressTransactionsListResponse
1515
| AddressTransactionsV2ListResponse
16+
| AverageBlockTimesResponse
1617
| BlockListResponse
1718
| BurnBlockListResponse
1819
| NakamotoBlockListResponse
@@ -1285,6 +1286,27 @@ export interface AddressTransaction {
12851286
[k: string]: unknown | undefined;
12861287
};
12871288
}
1289+
/**
1290+
* Request to fetch average block times (in seconds)
1291+
*/
1292+
export interface AverageBlockTimesResponse {
1293+
/**
1294+
* Average block times over the last hour (in seconds)
1295+
*/
1296+
last_1h: number;
1297+
/**
1298+
* Average block times over the last 24 hours (in seconds)
1299+
*/
1300+
last_24h: number;
1301+
/**
1302+
* Average block times over the last 7 days (in seconds)
1303+
*/
1304+
last_7d: number;
1305+
/**
1306+
* Average block times over the last 30 days (in seconds)
1307+
*/
1308+
last_30d: number;
1309+
}
12881310
/**
12891311
* GET request that returns blocks
12901312
*/

docs/openapi.yaml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,25 @@ paths:
779779
$ref: ./api/blocks/get-nakamoto-blocks.schema.json
780780
example:
781781
$ref: ./api/blocks/get-nakamoto-blocks.example.json
782-
782+
783+
/extended/v2/blocks/average-times:
784+
get:
785+
summary: Get average block times
786+
description: |
787+
Retrieves average block times (in seconds)
788+
tags:
789+
- Blocks
790+
operationId: get_average_block_times
791+
responses:
792+
200:
793+
description: Average block times (in seconds)
794+
content:
795+
application/json:
796+
schema:
797+
$ref: ./api/blocks/get-average-times.schema.json
798+
example:
799+
$ref: ./api/blocks/get-average-times.example.json
800+
783801
/extended/v2/blocks/{height_or_hash}:
784802
get:
785803
summary: Get block

src/api/routes/v2/blocks.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ export function createV2BlocksRouter(db: PgStore): express.Router {
4343
})
4444
);
4545

46+
router.get(
47+
'/average-times',
48+
cacheHandler,
49+
asyncHandler(async (_req, res) => {
50+
const query = await db.v2.getAverageBlockTimes();
51+
// Round to 2 decimal places
52+
const times = {
53+
last_1h: parseFloat(query.last_1h.toFixed(2)),
54+
last_24h: parseFloat(query.last_24h.toFixed(2)),
55+
last_7d: parseFloat(query.last_7d.toFixed(2)),
56+
last_30d: parseFloat(query.last_30d.toFixed(2)),
57+
};
58+
setETagCacheHeaders(res);
59+
res.json(times);
60+
})
61+
);
62+
4663
router.get(
4764
'/:height_or_hash',
4865
cacheHandler,

src/datastore/pg-store-v2.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,60 @@ export class PgStoreV2 extends BasePgStoreModule {
163163
});
164164
}
165165

166+
async getAverageBlockTimes(): Promise<{
167+
last_1h: number;
168+
last_24h: number;
169+
last_7d: number;
170+
last_30d: number;
171+
}> {
172+
return await this.sqlTransaction(async sql => {
173+
// Query against block_time but fallback to burn_block_time if block_time is 0 (work around for recent bug).
174+
// TODO: remove the burn_block_time fallback once all blocks for last N time have block_time set.
175+
const avgBlockTimeQuery = await sql<
176+
{
177+
last_1h: string | null;
178+
last_24h: string | null;
179+
last_7d: string | null;
180+
last_30d: string | null;
181+
}[]
182+
>`
183+
WITH TimeThresholds AS (
184+
SELECT
185+
FLOOR(EXTRACT(EPOCH FROM NOW() - INTERVAL '1 HOUR'))::INT AS h1,
186+
FLOOR(EXTRACT(EPOCH FROM NOW() - INTERVAL '24 HOURS'))::INT AS h24,
187+
FLOOR(EXTRACT(EPOCH FROM NOW() - INTERVAL '7 DAYS'))::INT AS d7,
188+
FLOOR(EXTRACT(EPOCH FROM NOW() - INTERVAL '30 DAYS'))::INT AS d30
189+
),
190+
OrderedCanonicalBlocks AS (
191+
SELECT
192+
CASE WHEN block_time = 0 THEN burn_block_time ELSE block_time END AS effective_time,
193+
LAG(CASE WHEN block_time = 0 THEN burn_block_time ELSE block_time END) OVER (ORDER BY block_height) AS prev_time
194+
FROM
195+
blocks
196+
WHERE
197+
canonical = true AND
198+
(CASE WHEN block_time = 0 THEN burn_block_time ELSE block_time END) >= (SELECT d30 FROM TimeThresholds)
199+
)
200+
SELECT
201+
AVG(CASE WHEN effective_time >= (SELECT h1 FROM TimeThresholds) THEN effective_time - prev_time ELSE NULL END) AS last_1h,
202+
AVG(CASE WHEN effective_time >= (SELECT h24 FROM TimeThresholds) THEN effective_time - prev_time ELSE NULL END) AS last_24h,
203+
AVG(CASE WHEN effective_time >= (SELECT d7 FROM TimeThresholds) THEN effective_time - prev_time ELSE NULL END) AS last_7d,
204+
AVG(effective_time - prev_time) AS last_30d
205+
FROM
206+
OrderedCanonicalBlocks
207+
WHERE
208+
prev_time IS NOT NULL
209+
`;
210+
const times = {
211+
last_1h: Number.parseFloat(avgBlockTimeQuery[0]?.last_1h ?? '0'),
212+
last_24h: Number.parseFloat(avgBlockTimeQuery[0]?.last_24h ?? '0'),
213+
last_7d: Number.parseFloat(avgBlockTimeQuery[0]?.last_7d ?? '0'),
214+
last_30d: Number.parseFloat(avgBlockTimeQuery[0]?.last_30d ?? '0'),
215+
};
216+
return times;
217+
});
218+
}
219+
166220
async getBlockTransactions(
167221
args: BlockParams & TransactionPaginationQueryParams
168222
): Promise<DbPaginatedResult<DbTx>> {

src/tests/block-tests.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { TestBlockBuilder, TestMicroblockStreamBuilder } from '../test-utils/tes
1414
import { PgWriteStore } from '../datastore/pg-write-store';
1515
import { PgSqlClient, bufferToHex } from '@hirosystems/api-toolkit';
1616
import { migrate } from '../test-utils/test-helpers';
17+
import { AverageBlockTimesResponse } from '@stacks/stacks-blockchain-api-types';
1718

1819
describe('block tests', () => {
1920
let db: PgWriteStore;
@@ -809,4 +810,42 @@ describe('block tests', () => {
809810
expect(fetch.status).toBe(200);
810811
expect(json).toStrictEqual(block5);
811812
});
813+
814+
test('blocks average time', async () => {
815+
const blockCount = 50;
816+
const now = Math.round(Date.now() / 1000);
817+
const thirtyMinutes = 30 * 60;
818+
// Return timestamp in seconds for block, latest block will be now(), and previous blocks will be 30 minutes apart
819+
const timeForBlock = (blockHeight: number) => {
820+
const blockDistance = blockCount - blockHeight;
821+
return now - thirtyMinutes * blockDistance;
822+
};
823+
for (let i = 1; i <= blockCount; i++) {
824+
const block = new TestBlockBuilder({
825+
block_height: i,
826+
block_time: timeForBlock(i),
827+
block_hash: `0x${i.toString().padStart(64, '0')}`,
828+
index_block_hash: `0x11${i.toString().padStart(62, '0')}`,
829+
parent_index_block_hash: `0x11${(i - 1).toString().padStart(62, '0')}`,
830+
parent_block_hash: `0x${(i - 1).toString().padStart(64, '0')}`,
831+
burn_block_height: 700000,
832+
burn_block_hash: '0x00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee5ca3d8',
833+
})
834+
.addTx({ tx_id: `0x${i.toString().padStart(64, '0')}` })
835+
.build();
836+
await db.update(block);
837+
}
838+
839+
const fetch = await supertest(api.server).get(`/extended/v2/blocks/average-times`);
840+
const response: AverageBlockTimesResponse = fetch.body;
841+
expect(fetch.status).toBe(200);
842+
843+
// All block time averages should be about 30 minutes
844+
const getRatio = (time: number) =>
845+
Math.min(thirtyMinutes, time) / Math.max(thirtyMinutes, time);
846+
expect(getRatio(response.last_1h)).toBeGreaterThanOrEqual(0.9);
847+
expect(getRatio(response.last_24h)).toBeGreaterThanOrEqual(0.9);
848+
expect(getRatio(response.last_7d)).toBeGreaterThanOrEqual(0.9);
849+
expect(getRatio(response.last_30d)).toBeGreaterThanOrEqual(0.9);
850+
});
812851
});

0 commit comments

Comments
 (0)