feat: mempool stats endpoint and prometheus metrics (#1241)

* feat: implement endpoint for various mempool stats

* chore: remove coinbase tx entries from mempool stats (will always be empty)

* chore: update prometheus mempool stats on new block, microblock, and mempool tx

* chore: add mempool tx byte size stats

* chore: add indexes to speedup mempool table queries

* chore: move `registerMempoolPromStats` fn to db helper module

* docs: add OpenAPI schema for new mempool stats endpoint

* chore: tests for mempool stats endpoint

* chore: restore mempool garbage collect env var after test

* chore: bytea serialization tests

* chore: strict hex string handling for bytea value serializer

* chore: improved mempool_tx indexing

* chore: unit test fix
This commit is contained in:
Matthew Little
2022-08-03 15:54:36 +02:00
committed by GitHub
parent 35797be303
commit 9482238599
19 changed files with 1193 additions and 102 deletions

View File

@@ -0,0 +1,86 @@
{
"tx_type_counts": {
"token_transfer": 130,
"smart_contract": 2,
"contract_call": 310,
"poison_microblock": 0
},
"tx_simple_fee_averages": {
"token_transfer": {
"p25": 3000,
"p50": 3000,
"p75": 6000,
"p95": 401199.9999999995
},
"smart_contract": {
"p25": 837500,
"p50": 925000,
"p75": 1012500,
"p95": 1082500
},
"contract_call": {
"p25": 3000,
"p50": 10368,
"p75": 100000,
"p95": 1000000
},
"poison_microblock": {
"p25": null,
"p50": null,
"p75": null,
"p95": null
}
},
"tx_ages": {
"token_transfer": {
"p25": 167.5,
"p50": 45,
"p75": 1,
"p95": 0
},
"smart_contract": {
"p25": 185.5,
"p50": 129,
"p75": 72.5,
"p95": 27.30000000000001
},
"contract_call": {
"p25": 189,
"p50": 127.5,
"p75": 9.5,
"p95": 0
},
"poison_microblock": {
"p25": null,
"p50": null,
"p75": null,
"p95": null
}
},
"tx_byte_sizes": {
"token_transfer": {
"p25": 180,
"p50": 180,
"p75": 180,
"p95": 180
},
"smart_contract": {
"p25": 706.75,
"p50": 814.5,
"p75": 922.25,
"p95": 1008.45
},
"contract_call": {
"p25": 291,
"p50": 435,
"p75": 462,
"p95": 597
},
"poison_microblock": {
"p25": null,
"p50": null,
"p75": null,
"p95": null
}
}
}

View File

@@ -0,0 +1,414 @@
{
"description": "GET request that returns stats on mempool transactions",
"title": "MempoolTransactionStatsResponse",
"type": "object",
"additionalProperties": false,
"required": [
"tx_type_counts",
"tx_simple_fee_averages",
"tx_ages",
"tx_byte_sizes"
],
"properties": {
"tx_type_counts": {
"type": "object",
"description": "Number of tranasction in the mempool, broken down by transaction type.",
"additionalProperties": false,
"required": [
"token_transfer",
"smart_contract",
"contract_call",
"poison_microblock"
],
"properties": {
"token_transfer": {
"type": "number"
},
"smart_contract": {
"type": "number"
},
"contract_call": {
"type": "number"
},
"poison_microblock": {
"type": "number"
}
}
},
"tx_simple_fee_averages": {
"type": "object",
"description": "The simple mean (average) transaction fee, broken down by transaction type. Note that this does not factor in actual execution costs. The average fee is not a reliable metric for calculating a fee for a new transaction.",
"additionalProperties": false,
"required": [
"token_transfer",
"smart_contract",
"contract_call",
"poison_microblock"
],
"properties": {
"token_transfer": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"smart_contract": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"contract_call": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"poison_microblock": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
}
}
},
"tx_ages": {
"type": "object",
"description": "The average time (in blocks) that transactions have lived in the mempool. The start block height is simply the current chain-tip of when the attached Stacks node receives the transaction. This timing can be different across Stacks nodes / API instances due to propagation timing differences in the p2p network.",
"additionalProperties": false,
"required": [
"token_transfer",
"smart_contract",
"contract_call",
"poison_microblock"
],
"properties": {
"token_transfer": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"smart_contract": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"contract_call": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"poison_microblock": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
}
}
},
"tx_byte_sizes": {
"type": "object",
"description": "The average byte size of transactions in the mempool, broken down by transaction type.",
"additionalProperties": false,
"required": [
"token_transfer",
"smart_contract",
"contract_call",
"poison_microblock"
],
"properties": {
"token_transfer": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"smart_contract": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"contract_call": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
},
"poison_microblock": {
"type": "object",
"additionalProperties": false,
"required": [
"p25",
"p50",
"p75",
"p95"
],
"properties": {
"p25": {
"type": "number",
"nullable": true
},
"p50": {
"type": "number",
"nullable": true
},
"p75": {
"type": "number",
"nullable": true
},
"p95": {
"type": "number",
"nullable": true
}
}
}
}
}
}
}

102
docs/generated.d.ts vendored
View File

@@ -99,6 +99,7 @@ export type SchemaMergeRootStub =
| NonFungibleTokenHoldingsList
| NonFungibleTokenMintList
| NonFungibleTokensMetadataList
| MempoolTransactionStatsResponse
| MempoolTransactionListResponse
| GetRawTransactionResult
| TransactionEventsResponse
@@ -3095,6 +3096,107 @@ export interface NonFungibleTokenMetadata {
*/
sender_address: string;
}
/**
* GET request that returns stats on mempool transactions
*/
export interface MempoolTransactionStatsResponse {
/**
* Number of tranasction in the mempool, broken down by transaction type.
*/
tx_type_counts: {
token_transfer: number;
smart_contract: number;
contract_call: number;
poison_microblock: number;
};
/**
* The simple mean (average) transaction fee, broken down by transaction type. Note that this does not factor in actual execution costs. The average fee is not a reliable metric for calculating a fee for a new transaction.
*/
tx_simple_fee_averages: {
token_transfer: {
p25: number;
p50: number;
p75: number;
p95: number;
};
smart_contract: {
p25: number;
p50: number;
p75: number;
p95: number;
};
contract_call: {
p25: number;
p50: number;
p75: number;
p95: number;
};
poison_microblock: {
p25: number;
p50: number;
p75: number;
p95: number;
};
};
/**
* The average time (in blocks) that transactions have lived in the mempool. The start block height is simply the current chain-tip of when the attached Stacks node receives the transaction. This timing can be different across Stacks nodes / API instances due to propagation timing differences in the p2p network.
*/
tx_ages: {
token_transfer: {
p25: number;
p50: number;
p75: number;
p95: number;
};
smart_contract: {
p25: number;
p50: number;
p75: number;
p95: number;
};
contract_call: {
p25: number;
p50: number;
p75: number;
p95: number;
};
poison_microblock: {
p25: number;
p50: number;
p75: number;
p95: number;
};
};
/**
* The average byte size of transactions in the mempool, broken down by transaction type.
*/
tx_byte_sizes: {
token_transfer: {
p25: number;
p50: number;
p75: number;
p95: number;
};
smart_contract: {
p25: number;
p50: number;
p75: number;
p95: number;
};
contract_call: {
p25: number;
p50: number;
p75: number;
p95: number;
};
poison_microblock: {
p25: number;
p50: number;
p75: number;
p95: number;
};
};
}
/**
* GET request that returns transactions
*/

View File

@@ -326,6 +326,25 @@ paths:
example:
$ref: ./api/transaction/get-mempool-transactions.example.json
/extended/v1/tx/mempool/stats:
get:
summary: Get statistics for mempool transactions
tags:
- Transactions
operationId: get_mempool_transaction_stats
description: |
Queries for transactions counts, age (by block height), fees (simple average), and size.
All results broken down by transaction type and percentiles (p25, p50, p75, p95).
responses:
200:
description: Statistics for mempool transactions
content:
application/json:
schema:
$ref: ./api/transaction/get-mempool-stats.schema.json
example:
$ref: ./api/transaction/get-mempool-stats.example.json
/extended/v1/tx/multiple:
parameters:
- name: tx_id

View File

@@ -138,7 +138,7 @@ export function createTxRouter(db: PgStore): express.Router {
'/mempool',
mempoolCacheHandler,
asyncHandler(async (req, res, next) => {
const limit = parseTxQueryLimit(req.query.limit ?? 96);
const limit = parseMempoolTxQueryLimit(req.query.limit ?? 96);
const offset = parsePagingQueryInput(req.query.offset ?? 0);
let addrParams: (string | undefined)[];
@@ -212,6 +212,16 @@ export function createTxRouter(db: PgStore): express.Router {
})
);
router.get(
'/mempool/stats',
mempoolCacheHandler,
asyncHandler(async (req, res) => {
const queryResult = await db.getMempoolStats({ lastBlockCount: undefined });
setETagCacheHeaders(res, ETagType.mempool);
res.json(queryResult);
})
);
router.get(
'/events',
cacheHandler,

View File

@@ -184,6 +184,37 @@ export interface DbTx extends BaseTx {
execution_cost_write_length: number;
}
export interface DbMempoolStats {
tx_type_counts: Record<string, number>;
tx_simple_fee_averages: Record<
string,
{
p25: number | null;
p50: number | null;
p75: number | null;
p95: number | null;
}
>;
tx_ages: Record<
string,
{
p25: number | null;
p50: number | null;
p75: number | null;
p95: number | null;
}
>;
tx_byte_sizes: Record<
string,
{
p25: number | null;
p50: number | null;
p75: number | null;
p95: number | null;
}
>;
}
export interface DbMempoolTx extends BaseTx {
pruned: boolean;
raw_tx: string;

View File

@@ -21,13 +21,35 @@ const PG_TYPE_MAPPINGS = {
bytea: {
to: 17,
from: [17],
serialize: (x: any) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
x instanceof Uint8Array
? `\\x${Buffer.from(x, x.byteOffset, x.byteLength).toString('hex')}`
: typeof x === 'string' && has0xPrefix(x)
? `\\x${x.slice(2)}`
: x,
serialize: (x: any) => {
if (typeof x === 'string') {
if (/^(0x|0X)[a-fA-F0-9]*$/.test(x)) {
// hex string with "0x" prefix
if (x.length % 2 !== 0) {
throw new Error(`Hex string is an odd number of digits: "${x}"`);
}
return '\\x' + x.slice(2);
} else if (x.length === 0) {
return '\\x';
} else if (/^\\x[a-fA-F0-9]*$/.test(x)) {
// hex string with "\x" prefix (already encoded for postgres)
if (x.length % 2 !== 0) {
throw new Error(`Hex string is an odd number of digits: "${x}"`);
}
return x;
} else {
throw new Error(`String value for bytea column does not have 0x prefix: "${x}"`);
}
} else if (Buffer.isBuffer(x)) {
return '\\x' + x.toString('hex');
} else if (ArrayBuffer.isView(x)) {
return '\\x' + Buffer.from(x.buffer, x.byteOffset, x.byteLength).toString('hex');
} else {
throw new Error(
`Cannot serialize unexpected type "${x.constructor.name}" to bytea hex string`
);
}
},
parse: (x: any) => `0x${x.slice(2)}`,
},
};
@@ -69,7 +91,7 @@ export async function connectPostgres({
const timeElapsed = initTimer.getElapsed();
if (timeElapsed - lastElapsedLog > 2000) {
lastElapsedLog = timeElapsed;
logError('Pg connection failed, retrying..');
logError(`Pg connection failed: ${error}, retrying..`);
}
connectionError = error;
await timeout(100);

View File

@@ -1,4 +1,4 @@
import { hexToBuffer, parseEnum } from '../helpers';
import { hexToBuffer, logError, parseEnum } from '../helpers';
import {
BlockQueryResult,
ContractTxQueryResult,
@@ -8,6 +8,7 @@ import {
DbFaucetRequest,
DbFaucetRequestCurrency,
DbFtEvent,
DbMempoolStats,
DbMempoolTx,
DbMicroblock,
DbNftEvent,
@@ -38,9 +39,11 @@ import {
} from 'stacks-encoding-native-js';
import { getTxSenderAddress } from '../event-stream/reader';
import postgres = require('postgres');
import * as prom from 'prom-client';
import { PgSqlClient } from './connection';
import { NftEvent } from 'docs/generated';
import { getAssetEventTypeString } from '../api/controllers/db-controller';
import { PgStoreEventEmitter } from './pg-store-event-emitter';
export const TX_COLUMNS = [
'tx_id',
@@ -799,3 +802,59 @@ export function createDbTxFromCoreMsg(msg: CoreNodeParsedTxMessage): DbTx {
extractTransactionPayload(parsedTx, dbTx);
return dbTx;
}
export function registerMempoolPromStats(pgEvents: PgStoreEventEmitter) {
const mempoolTxCountGauge = new prom.Gauge({
name: `mempool_tx_count`,
help: 'Number of txs in the mempool, by tx type',
labelNames: ['type'] as const,
});
const mempoolTxFeeAvgGauge = new prom.Gauge({
name: `mempool_tx_fee_average`,
help: 'Simple average of tx fees in the mempool, by tx type',
labelNames: ['type', 'percentile'] as const,
});
const mempoolTxAgeGauge = new prom.Gauge({
name: `mempool_tx_age`,
help: 'Average age (by block) of txs in the mempool, by tx type',
labelNames: ['type', 'percentile'] as const,
});
const mempoolTxSizeGauge = new prom.Gauge({
name: `mempool_tx_byte_size`,
help: 'Average byte size of txs in the mempool, by tx type',
labelNames: ['type', 'percentile'] as const,
});
const updatePromMempoolStats = (mempoolStats: DbMempoolStats) => {
for (const txType in mempoolStats.tx_type_counts) {
const entry = mempoolStats.tx_type_counts[txType];
mempoolTxCountGauge.set({ type: txType }, entry);
}
for (const txType in mempoolStats.tx_simple_fee_averages) {
const entries = mempoolStats.tx_simple_fee_averages[txType];
Object.entries(entries).forEach(([p, num]) => {
mempoolTxFeeAvgGauge.set({ type: txType, percentile: p }, num ?? -1);
});
}
for (const txType in mempoolStats.tx_ages) {
const entries = mempoolStats.tx_ages[txType];
Object.entries(entries).forEach(([p, num]) => {
mempoolTxAgeGauge.set({ type: txType, percentile: p }, num ?? -1);
});
}
for (const txType in mempoolStats.tx_byte_sizes) {
const entries = mempoolStats.tx_byte_sizes[txType];
Object.entries(entries).forEach(([p, num]) => {
mempoolTxSizeGauge.set({ type: txType, percentile: p }, num ?? -1);
});
}
};
pgEvents.addListener('mempoolStatsUpdate', mempoolStats => {
setImmediate(() => {
try {
updatePromMempoolStats(mempoolStats);
} catch (error) {
logError(`Error updating prometheus mempool stats`, error);
}
});
});
}

View File

@@ -1,5 +1,6 @@
import { EventEmitter } from 'events';
import StrictEventEmitter from 'strict-event-emitter-types';
import { DbMempoolStats } from './common';
type DataStoreEventEmitter = StrictEventEmitter<
EventEmitter,
@@ -12,6 +13,7 @@ type DataStoreEventEmitter = StrictEventEmitter<
nameUpdate: (info: string) => void;
tokensUpdate: (contractID: string) => void;
tokenMetadataUpdateQueued: (queueId: number) => void;
mempoolStatsUpdate: (mempoolStats: DbMempoolStats) => void;
}
>;

View File

@@ -4,7 +4,7 @@ import {
TransactionType,
} from '@stacks/stacks-blockchain-api-types';
import { ChainID, ClarityAbi } from '@stacks/transactions';
import { getTxTypeId } from '../api/controllers/db-controller';
import { getTxTypeId, getTxTypeString } from '../api/controllers/db-controller';
import {
assertNotNullish,
FoundOrNot,
@@ -37,6 +37,7 @@ import {
DbGetBlockWithMetadataOpts,
DbGetBlockWithMetadataResponse,
DbInboundStxTransfer,
DbMempoolStats,
DbMempoolTx,
DbMicroblock,
DbMinerReward,
@@ -1056,6 +1057,169 @@ export class PgStore {
});
}
async getMempoolStats({ lastBlockCount }: { lastBlockCount?: number }): Promise<DbMempoolStats> {
return await this.sql.begin(async sql => {
return await this.getMempoolStatsInternal({ sql, lastBlockCount });
});
}
async getMempoolStatsInternal({
sql,
lastBlockCount,
}: {
sql: PgSqlClient;
lastBlockCount?: number;
}): Promise<DbMempoolStats> {
let blockHeightCondition = sql``;
const chainTipHeight = await this.getMaxBlockHeight(sql, { includeUnanchored: true });
if (lastBlockCount) {
const maxBlockHeight = chainTipHeight - lastBlockCount;
blockHeightCondition = sql` AND receipt_block_height >= ${maxBlockHeight} `;
}
const txTypes = [
DbTxTypeId.TokenTransfer,
DbTxTypeId.SmartContract,
DbTxTypeId.ContractCall,
DbTxTypeId.PoisonMicroblock,
];
const txTypeCountsQuery = await sql<{ type_id: DbTxTypeId; count: number }[]>`
SELECT
type_id,
count(*)::integer count
FROM mempool_txs
WHERE pruned = false
${blockHeightCondition}
GROUP BY type_id
`;
const txTypeCounts: Record<string, number> = {};
for (const typeId of txTypes) {
const count = txTypeCountsQuery.find(r => r.type_id === typeId)?.count ?? 0;
txTypeCounts[getTxTypeString(typeId)] = count;
}
const txFeesQuery = await sql<
{ type_id: DbTxTypeId; p25: number; p50: number; p75: number; p95: number }[]
>`
SELECT
type_id,
percentile_cont(0.25) within group (order by fee_rate asc) as p25,
percentile_cont(0.50) within group (order by fee_rate asc) as p50,
percentile_cont(0.75) within group (order by fee_rate asc) as p75,
percentile_cont(0.95) within group (order by fee_rate asc) as p95
FROM mempool_txs
WHERE pruned = false
${blockHeightCondition}
GROUP BY type_id
`;
const txFees: Record<
string,
{ p25: number | null; p50: number | null; p75: number | null; p95: number | null }
> = {};
for (const typeId of txTypes) {
const percentiles = txFeesQuery.find(r => r.type_id === typeId);
txFees[getTxTypeString(typeId)] = {
p25: percentiles?.p25 ?? null,
p50: percentiles?.p50 ?? null,
p75: percentiles?.p75 ?? null,
p95: percentiles?.p95 ?? null,
};
}
const txAgesQuery = await sql<
{
type_id: DbTxTypeId;
p25: number;
p50: number;
p75: number;
p95: number;
}[]
>`
WITH mempool_unpruned AS (
SELECT
type_id,
receipt_block_height
FROM mempool_txs
WHERE pruned = false
${blockHeightCondition}
),
mempool_ages AS (
SELECT
type_id,
${chainTipHeight} - receipt_block_height as age
FROM mempool_unpruned
)
SELECT
type_id,
percentile_cont(0.25) within group (order by age asc) as p25,
percentile_cont(0.50) within group (order by age asc) as p50,
percentile_cont(0.75) within group (order by age asc) as p75,
percentile_cont(0.95) within group (order by age asc) as p95
FROM mempool_ages
GROUP BY type_id
`;
const txAges: Record<
string,
{ p25: number | null; p50: number | null; p75: number | null; p95: number | null }
> = {};
for (const typeId of txTypes) {
const percentiles = txAgesQuery.find(r => r.type_id === typeId);
txAges[getTxTypeString(typeId)] = {
p25: percentiles?.p25 ?? null,
p50: percentiles?.p50 ?? null,
p75: percentiles?.p75 ?? null,
p95: percentiles?.p95 ?? null,
};
}
const txSizesQuery = await sql<
{
type_id: DbTxTypeId;
p25: number;
p50: number;
p75: number;
p95: number;
}[]
>`
WITH mempool_unpruned AS (
SELECT
type_id, tx_size
FROM mempool_txs
WHERE pruned = false
${blockHeightCondition}
)
SELECT
type_id,
percentile_cont(0.25) within group (order by tx_size asc) as p25,
percentile_cont(0.50) within group (order by tx_size asc) as p50,
percentile_cont(0.75) within group (order by tx_size asc) as p75,
percentile_cont(0.95) within group (order by tx_size asc) as p95
FROM mempool_unpruned
GROUP BY type_id
`;
const txSizes: Record<
string,
{ p25: number | null; p50: number | null; p75: number | null; p95: number | null }
> = {};
for (const typeId of txTypes) {
const percentiles = txSizesQuery.find(r => r.type_id === typeId);
txSizes[getTxTypeString(typeId)] = {
p25: percentiles?.p25 ?? null,
p50: percentiles?.p50 ?? null,
p75: percentiles?.p75 ?? null,
p95: percentiles?.p95 ?? null,
};
}
return {
tx_type_counts: txTypeCounts,
tx_simple_fee_averages: txFees,
tx_ages: txAges,
tx_byte_sizes: txSizes,
};
}
async getMempoolTxList({
limit,
offset,

View File

@@ -392,6 +392,11 @@ export class PgWriteStore extends PgStore {
tokenMetadataQueueEntries.push(queueEntry);
}
}
if (!this.isEventReplay) {
const mempoolStats = await this.getMempoolStatsInternal({ sql });
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
}
});
// Skip sending `PgNotifier` updates altogether if we're in the genesis block since this block is the
@@ -644,6 +649,11 @@ export class PgWriteStore extends PgStore {
await this.refreshNftCustody(sql, txs, true);
await this.refreshMaterializedView(sql, 'chain_tip');
if (!this.isEventReplay) {
const mempoolStats = await this.getMempoolStatsInternal({ sql });
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
}
if (this.notifier) {
for (const microblock of dbMicroblocks) {
await this.notifier.sendMicroblock({ microblockHash: microblock.microblock_hash });
@@ -1210,6 +1220,11 @@ export class PgWriteStore extends PgStore {
}
}
await this.refreshMaterializedView(sql, 'mempool_digest');
if (!this.isEventReplay) {
const mempoolStats = await this.getMempoolStatsInternal({ sql });
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
}
});
for (const tx of updatedTxs) {
await this.notifier?.sendTx({ txId: tx.tx_id });

View File

@@ -27,6 +27,7 @@ import { PgStore } from './datastore/pg-store';
import { PgWriteStore } from './datastore/pg-write-store';
import { isFtMetadataEnabled, isNftMetadataEnabled } from './token-metadata/helpers';
import { TokensProcessorQueue } from './token-metadata/tokens-processor-queue';
import { registerMempoolPromStats } from './datastore/helpers';
enum StacksApiMode {
/**
@@ -122,6 +123,8 @@ async function init(): Promise<void> {
skipMigrations: false,
});
registerMempoolPromStats(dbWriteStore.eventEmitter);
if (apiMode === StacksApiMode.default || apiMode === StacksApiMode.writeOnly) {
if (isProdEnv) {
await importV1TokenOfferingData(dbWriteStore);

View File

@@ -0,0 +1,16 @@
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';
export async function up(pgm: MigrationBuilder): Promise<void> {
pgm.addColumn('mempool_txs', {
tx_size: {
type: 'integer',
notNull: true,
expressionGenerated: 'length(raw_tx)'
}
});
pgm.createIndex('mempool_txs', ['type_id', 'receipt_block_height'], { where: 'pruned = false'});
pgm.createIndex('mempool_txs', ['type_id', 'fee_rate'], { where: 'pruned = false'});
pgm.createIndex('mempool_txs', ['type_id', 'tx_size'], { where: 'pruned = false'});
}

View File

@@ -207,7 +207,7 @@ function testTx(args?: TestTxArgs): DataStoreTxEventData {
sponsor_address: undefined,
sender_address: args?.sender_address ?? SENDER_ADDRESS,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
parent_index_block_hash: args?.parent_index_block_hash ?? INDEX_BLOCK_HASH,
parent_block_hash: BLOCK_HASH,
@@ -252,6 +252,8 @@ interface TestMempoolTxArgs {
tx_id?: string;
type_id?: DbTxTypeId;
nonce?: number;
fee_rate?: bigint;
raw_tx?: string;
}
/**
@@ -265,12 +267,12 @@ export function testMempoolTx(args?: TestMempoolTxArgs): DbMempoolTx {
tx_id: args?.tx_id ?? TX_ID,
anchor_mode: 3,
nonce: args?.nonce ?? 0,
raw_tx: 'test-raw-tx',
raw_tx: args?.raw_tx ?? '0x01234567',
type_id: args?.type_id ?? DbTxTypeId.TokenTransfer,
receipt_time: (new Date().getTime() / 1000) | 0,
status: args?.status ?? DbTxStatus.Pending,
post_conditions: '0x01f5',
fee_rate: 1234n,
fee_rate: args?.fee_rate ?? 1234n,
sponsored: false,
sponsor_address: undefined,
origin_hash_mode: 1,

View File

@@ -140,7 +140,7 @@ describe('api tests', () => {
burn_block_time: dbBlock1.burn_block_time,
parent_burn_block_time: 0,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -740,11 +740,11 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
status: DbTxStatus.Pending,
receipt_time: 1594307695,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
post_conditions: '0x01f5',
fee_rate: 1234n,
sponsored: false,
@@ -822,9 +822,9 @@ describe('api tests', () => {
tx_id: '0x8915000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 1000,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
post_conditions: '0x01f5',
fee_rate: 1234n,
sponsored: true,
@@ -903,11 +903,11 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
status: DbTxStatus.Pending,
receipt_time: 1594307695,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
post_conditions: '0x01f5',
fee_rate: 1234n,
sponsored: false,
@@ -947,11 +947,11 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
status: DbTxStatus.Pending,
receipt_time: 1594307695,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
post_conditions: '0x01f5',
fee_rate: 1234n,
sponsored: true,
@@ -992,11 +992,11 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
status: DbTxStatus.Pending,
receipt_time: 1594307695,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
post_conditions: '0x01f5',
fee_rate: 1234n,
sponsored: true,
@@ -1280,10 +1280,10 @@ describe('api tests', () => {
tx_id: `0x891200000000000000000000000000000000000000000000000000000000000${i}`,
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
receipt_time: (new Date(`2020-07-09T15:14:0${i}Z`).getTime() / 1000) | 0,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
post_conditions: '0x01f5',
fee_rate: 1234n,
@@ -1419,7 +1419,7 @@ describe('api tests', () => {
tx_id: `0x89120000000000000000000000000000000000000000000000000000000000${paddedIndex}`,
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: xfer.type_id,
receipt_time: (new Date(`2020-07-09T15:14:${paddedIndex}Z`).getTime() / 1000) | 0,
status: 1,
@@ -1849,7 +1849,7 @@ describe('api tests', () => {
burn_block_time: 2837565,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -1878,10 +1878,10 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
receipt_time: 123456,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
post_conditions: '0x01f5',
fee_rate: 1234n,
@@ -2261,7 +2261,7 @@ describe('api tests', () => {
burn_block_time: 2837565,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -2289,10 +2289,10 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
receipt_time: 123456,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
post_conditions: '0x01f5',
fee_rate: 1234n,
@@ -2591,7 +2591,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: 'none',
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -2642,7 +2642,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: addr2,
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -2911,7 +2911,7 @@ describe('api tests', () => {
tx_id: '0x1111882200000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
receipt_time: 123456,
smart_contract_contract_id: contractAddr2,
smart_contract_source_code: '(some-src)',
@@ -3026,7 +3026,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: 'none',
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -3063,7 +3063,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: addr2,
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -3509,7 +3509,7 @@ describe('api tests', () => {
tx_id: '0x1111882200000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
receipt_time: 123456,
smart_contract_contract_id: contractAddr2,
smart_contract_source_code: '(some-src)',
@@ -3652,7 +3652,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: BigInt(amount),
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: recipient,
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -4420,7 +4420,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: BigInt(amount),
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: recipient,
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -4469,7 +4469,7 @@ describe('api tests', () => {
burn_block_time: block.burn_block_time,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -5606,7 +5606,7 @@ describe('api tests', () => {
sponsor_address: undefined,
origin_hash_mode: 1,
token_transfer_amount: 50n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: contractId,
event_count: 1,
parent_index_block_hash: block2.block.index_block_hash,
@@ -5703,7 +5703,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
@@ -5931,7 +5931,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
@@ -7237,7 +7237,7 @@ describe('api tests', () => {
burn_block_time: 1594647995,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -7987,7 +7987,7 @@ describe('api tests', () => {
tx_id: '0x521234',
anchor_mode: 3,
nonce: 1,
raw_tx: 'test-raw-mempool-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
type_id: DbTxTypeId.Coinbase,
status: 1,
post_conditions: '0x01f5',
@@ -7997,7 +7997,7 @@ describe('api tests', () => {
sender_address: senderAddress,
sponsor_nonce: 3,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
pruned: false,
receipt_time: 1616063078,
};
@@ -8039,7 +8039,7 @@ describe('api tests', () => {
tx_id: '0x52123456',
anchor_mode: 3,
nonce: 1,
raw_tx: 'test-raw-mempool-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
type_id: DbTxTypeId.Coinbase,
status: 1,
post_conditions: '0x01f5',
@@ -8049,7 +8049,7 @@ describe('api tests', () => {
sender_address: senderAddress,
sponsor_nonce: 6,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
pruned: false,
receipt_time: 1616063078,
};
@@ -8617,7 +8617,7 @@ describe('api tests', () => {
tx_index: 0,
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
index_block_hash: block.index_block_hash,
block_hash: block.block_hash,
block_height: block.block_height,
@@ -8638,7 +8638,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
@@ -8670,7 +8670,7 @@ describe('api tests', () => {
tx_id: '0x521234',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-mempool-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
type_id: DbTxTypeId.Coinbase,
status: 1,
post_conditions: '0x01f5',
@@ -8679,7 +8679,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
pruned: false,
receipt_time: 1616063078,
};
@@ -8730,7 +8730,7 @@ describe('api tests', () => {
tx_index: 0,
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
index_block_hash: '0x1234',
block_hash: block.block_hash,
block_height: block.block_height,
@@ -8751,7 +8751,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
@@ -8848,7 +8848,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: 'none',
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -8956,7 +8956,7 @@ describe('api tests', () => {
parent_burn_block_time: 1626124935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: 'none',
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -9073,7 +9073,7 @@ describe('api tests', () => {
burn_block_time: 1594647995,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -9212,7 +9212,7 @@ describe('api tests', () => {
tx_id: '0x521234',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-mempool-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
type_id: DbTxTypeId.Coinbase,
status: 1,
post_conditions: '0x01f5',
@@ -9221,7 +9221,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: senderAddress,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
pruned: false,
receipt_time: 1616063078,
};
@@ -9239,7 +9239,7 @@ describe('api tests', () => {
tx_id: '0x521234',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-mempool-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
type_id: DbTxTypeId.Coinbase,
status: 1,
post_conditions: '0x01f5',
@@ -9248,7 +9248,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: senderAddress,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
pruned: false,
receipt_time: 1616063078,
};
@@ -9284,7 +9284,7 @@ describe('api tests', () => {
tx_id: '0x521234',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-mempool-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-mempool-tx')),
type_id: DbTxTypeId.Coinbase,
status: 1,
post_conditions: '0x01f5',
@@ -9293,7 +9293,7 @@ describe('api tests', () => {
sponsor_address: undefined,
sender_address: senderAddress,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
pruned: false,
receipt_time: 1616063078,
};
@@ -9366,7 +9366,7 @@ describe('api tests', () => {
burn_block_time: block.burn_block_time,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -9455,7 +9455,7 @@ describe('api tests', () => {
burn_block_time: block.burn_block_time,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -9678,9 +9678,9 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
post_conditions: '0x01f5',
fee_rate: 1234n,
sponsored: true,
@@ -9708,9 +9708,9 @@ describe('api tests', () => {
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000001',
anchor_mode: 3,
nonce: 0,
raw_tx: 'test-raw-tx',
raw_tx: bufferToHexPrefixString(Buffer.from('test-raw-tx')),
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
post_conditions: '0x01f5',
fee_rate: 1234n,
sponsored: true,

View File

@@ -4,7 +4,7 @@ import { getBlockFromDataStore } from '../api/controllers/db-controller';
import { DbBlock, DbMicroblockPartial, DbTx, DbTxStatus, DbTxTypeId } from '../datastore/common';
import { startApiServer, ApiServer } from '../api/init';
import { PoolClient } from 'pg';
import { I32_MAX } from '../helpers';
import { bufferToHexPrefixString, I32_MAX } from '../helpers';
import { parseIfNoneMatchHeader } from '../api/controllers/cache-controller';
import { TestBlockBuilder, testMempoolTx } from '../test-utils/test-builders';
import { PgWriteStore } from '../datastore/pg-write-store';
@@ -104,7 +104,7 @@ describe('cache-control tests', () => {
burn_block_time: 1594647995,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: 'coinbase hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('coinbase hi')),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
@@ -248,7 +248,7 @@ describe('cache-control tests', () => {
sponsor_address: undefined,
origin_hash_mode: 1,
token_transfer_amount: 50n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: addr2,
event_count: 1,
parent_index_block_hash: block1.index_block_hash,

View File

@@ -26,7 +26,7 @@ import * as assert from 'assert';
import { PgWriteStore } from '../datastore/pg-write-store';
import { cycleMigrations, runMigrations } from '../datastore/migrations';
import { getPostgres, PgSqlClient } from '../datastore/connection';
import { bnsNameCV, I32_MAX } from '../helpers';
import { bnsNameCV, bufferToHexPrefixString, I32_MAX } from '../helpers';
import { ChainID, intCV, serializeCV } from '@stacks/transactions';
function testEnvVars(
@@ -82,6 +82,78 @@ describe('postgres datastore', () => {
client = db.sql;
});
test('bytea column serialization', async () => {
const vectors = [
{
from: '0x0001',
to: '0x0001',
},
{
from: '0X0002',
to: '0x0002',
},
{
from: '0xFfF3',
to: '0xfff3',
},
{
from: Buffer.from('0004', 'hex'),
to: '0x0004',
},
{
from: new Uint16Array(new Uint8Array([0x00, 0x05]).buffer),
to: '0x0005',
},
{
from: '\\x0006',
to: '0x0006',
},
{
from: '\\xfFf7',
to: '0xfff7',
},
{
from: '\\x',
to: '0x',
},
{
from: '',
to: '0x',
},
{
from: Buffer.alloc(0),
to: '0x',
},
];
await db.sql.begin(async sql => {
await sql`
CREATE TEMPORARY TABLE bytea_testing(
value bytea NOT NULL
) ON COMMIT DROP
`;
for (const v of vectors) {
const query = await sql<{ value: string }[]>`
insert into bytea_testing (value) values (${v.from})
returning value
`;
expect(query[0].value).toBe(v.to);
}
});
const badInputs = ['0x123', '1234', '0xnoop', new Date(), 1234];
for (const input of badInputs) {
const query = async () =>
db.sql.begin(async sql => {
await sql`
CREATE TEMPORARY TABLE bytea_testing(
value bytea NOT NULL
) ON COMMIT DROP
`;
return await sql`insert into bytea_testing (value) values (${input})`;
});
await expect(query()).rejects.toThrow();
}
});
test('postgres uri config', () => {
const uri =
'postgresql://test_user:secret_password@database.server.com:3211/test_db?ssl=true&currentSchema=test_schema&application_name=test-conn-str';
@@ -825,7 +897,7 @@ describe('postgres datastore', () => {
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: BigInt(amount),
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: recipient,
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
@@ -3137,7 +3209,7 @@ describe('postgres datastore', () => {
type_id: DbTxTypeId.TokenTransfer,
receipt_time: 123456,
token_transfer_amount: 1n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: 'stx-recipient-addr',
status: DbTxStatus.Pending,
post_conditions: '0x',
@@ -3406,7 +3478,7 @@ describe('postgres datastore', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 1,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',
@@ -3441,7 +3513,7 @@ describe('postgres datastore', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',
@@ -3631,7 +3703,7 @@ describe('postgres datastore', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 1,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',
@@ -3666,7 +3738,7 @@ describe('postgres datastore', () => {
sender_address: 'sender-addr',
sponsor_address: undefined,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 1,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',
@@ -3888,7 +3960,7 @@ describe('postgres datastore', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',
@@ -4186,7 +4258,7 @@ describe('postgres datastore', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',
@@ -4249,7 +4321,7 @@ describe('postgres datastore', () => {
tx_index: 0,
anchor_mode: 3,
nonce: 0,
raw_tx: 'abc',
raw_tx: bufferToHexPrefixString(Buffer.from('abc')),
index_block_hash: '0x1234',
block_hash: '0x5678',
block_height: block1.block_height,
@@ -4265,7 +4337,7 @@ describe('postgres datastore', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 0,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',
@@ -4343,7 +4415,7 @@ describe('postgres datastore', () => {
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 4,
parent_index_block_hash: '0x00',
parent_block_hash: '0x00',

View File

@@ -1,9 +1,11 @@
import * as supertest from 'supertest';
import { ChainID } from '@stacks/transactions';
import { startApiServer, ApiServer } from '../api/init';
import { PgSqlClient } from '../datastore/connection';
import { TestBlockBuilder, testMempoolTx } from '../test-utils/test-builders';
import { PgWriteStore } from '../datastore/pg-write-store';
import { cycleMigrations, runMigrations } from '../datastore/migrations';
import { DbTxTypeId } from '../datastore/common';
describe('mempool tests', () => {
let db: PgWriteStore;
@@ -23,8 +25,43 @@ describe('mempool tests', () => {
});
test('garbage collection', async () => {
const garbageThresholdOrig = process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD;
process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD = '2';
try {
// Insert 5 blocks with 1 mempool tx each.
for (let block_height = 1; block_height <= 5; block_height++) {
const block = new TestBlockBuilder({
block_height: block_height,
index_block_hash: `0x0${block_height}`,
parent_index_block_hash: `0x0${block_height - 1}`,
})
.addTx({ tx_id: `0x111${block_height}` })
.build();
await db.update(block);
const mempoolTx = testMempoolTx({ tx_id: `0x0${block_height}` });
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
}
// Make sure we only have mempool txs for block_height >= 3
const mempoolTxResult = await db.getMempoolTxList({
limit: 10,
offset: 0,
includeUnanchored: false,
});
const mempoolTxs = mempoolTxResult.results;
expect(mempoolTxs.length).toEqual(3);
const txIds = mempoolTxs.map(e => e.tx_id).sort();
expect(txIds).toEqual(['0x03', '0x04', '0x05']);
} finally {
if (typeof garbageThresholdOrig === 'undefined') {
delete process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD;
} else {
process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD = garbageThresholdOrig;
}
}
});
test('mempool stats', async () => {
// Insert 5 blocks with 1 mempool tx each.
for (let block_height = 1; block_height <= 5; block_height++) {
const block = new TestBlockBuilder({
@@ -35,20 +72,57 @@ describe('mempool tests', () => {
.addTx({ tx_id: `0x111${block_height}` })
.build();
await db.update(block);
const mempoolTx = testMempoolTx({ tx_id: `0x0${block_height}` });
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
const mempoolTx1 = testMempoolTx({
tx_id: `0x0${block_height}`,
type_id: DbTxTypeId.TokenTransfer,
fee_rate: BigInt(100 * block_height),
raw_tx: '0x' + 'ff'.repeat(block_height),
});
const mempoolTx2 = testMempoolTx({
tx_id: `0x1${block_height}`,
type_id: DbTxTypeId.ContractCall,
fee_rate: BigInt(200 * block_height),
raw_tx: '0x' + 'ff'.repeat(block_height + 10),
});
const mempoolTx3 = testMempoolTx({
tx_id: `0x2${block_height}`,
type_id: DbTxTypeId.SmartContract,
fee_rate: BigInt(300 * block_height),
raw_tx: '0x' + 'ff'.repeat(block_height + 20),
});
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx1, mempoolTx2, mempoolTx3] });
}
// Make sure we only have mempool txs for block_height >= 3
const mempoolTxResult = await db.getMempoolTxList({
limit: 10,
offset: 0,
includeUnanchored: false,
});
const mempoolTxs = mempoolTxResult.results;
expect(mempoolTxs.length).toEqual(3);
const txIds = mempoolTxs.map(e => e.tx_id).sort();
expect(txIds).toEqual(['0x03', '0x04', '0x05']);
const result = await supertest(api.server).get(`/extended/v1/tx/mempool/stats`);
expect(result.status).toBe(200);
expect(result.type).toBe('application/json');
const expectedResp1 = {
tx_type_counts: {
token_transfer: 5,
smart_contract: 5,
contract_call: 5,
poison_microblock: 0,
},
tx_simple_fee_averages: {
token_transfer: { p25: 200, p50: 300, p75: 400, p95: 480 },
smart_contract: { p25: 600, p50: 900, p75: 1200, p95: 1440 },
contract_call: { p25: 400, p50: 600, p75: 800, p95: 960 },
poison_microblock: { p25: null, p50: null, p75: null, p95: null },
},
tx_ages: {
token_transfer: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
smart_contract: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
contract_call: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
poison_microblock: { p25: null, p50: null, p75: null, p95: null },
},
tx_byte_sizes: {
token_transfer: { p25: 2, p50: 3, p75: 4, p95: 4.8 },
smart_contract: { p25: 22, p50: 23, p75: 24, p95: 24.8 },
contract_call: { p25: 12, p50: 13, p75: 14, p95: 14.8 },
poison_microblock: { p25: null, p50: null, p75: null, p95: null },
},
};
expect(JSON.parse(result.text)).toEqual(expectedResp1);
});
afterEach(async () => {

View File

@@ -307,7 +307,7 @@ describe('microblock tests', () => {
sponsor_address: undefined,
sender_address: addr1,
origin_hash_mode: 1,
coinbase_payload: 'hi',
coinbase_payload: bufferToHexPrefixString(Buffer.from('hi')),
event_count: 1,
parent_index_block_hash: block1.parent_index_block_hash,
parent_block_hash: block1.parent_block_hash,
@@ -418,7 +418,7 @@ describe('microblock tests', () => {
sponsor_address: undefined,
origin_hash_mode: 1,
token_transfer_amount: 50n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: addr2,
event_count: 1,
parent_index_block_hash: block1.index_block_hash,
@@ -458,7 +458,7 @@ describe('microblock tests', () => {
sponsor_address: undefined,
origin_hash_mode: 1,
token_transfer_amount: 50n,
token_transfer_memo: 'hi',
token_transfer_memo: bufferToHexPrefixString(Buffer.from('hi')),
token_transfer_recipient_address: addr2,
event_count: 1,
parent_index_block_hash: block1.index_block_hash,