Skip to content

Commit

Permalink
feat: ingest signer_signature from /new_block event and expose in…
Browse files Browse the repository at this point in the history
… new endpoint (#2125)

* feat: ingest `signer_signature` from `/new_block` event and expose in new endpoint

* chore: lint fix

* chore: fix tests

* chore: signer-signature -> signer-signatures
  • Loading branch information
zone117x authored Oct 18, 2024
1 parent 7d2f8df commit c389154
Show file tree
Hide file tree
Showing 24 changed files with 307 additions and 4 deletions.
14 changes: 14 additions & 0 deletions migrations/1729262745699_stacks_block_signer-signatures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable camelcase */

/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
exports.up = pgm => {

pgm.addColumn('blocks', {
signer_signatures: {
type: 'bytea[]',
}
});

pgm.createIndex('blocks', 'signer_signatures', { method: 'gin' });

};
5 changes: 5 additions & 0 deletions src/api/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum ResourceType {
Signer,
PoxCycle,
TokenHolders,
BlockSignerSignature,
}

export const pagingQueryLimits: Record<ResourceType, { defaultLimit: number; maxLimit: number }> = {
Expand Down Expand Up @@ -94,6 +95,10 @@ export const pagingQueryLimits: Record<ResourceType, { defaultLimit: number; max
defaultLimit: 100,
maxLimit: 200,
},
[ResourceType.BlockSignerSignature]: {
defaultLimit: 500,
maxLimit: 1000,
},
};

export function getPagingQueryLimit(
Expand Down
59 changes: 57 additions & 2 deletions src/api/routes/v2/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import { Server } from 'node:http';
import { CursorOffsetParam, LimitParam, OffsetParam } from '../../schemas/params';
import { getPagingQueryLimit, pagingQueryLimits, ResourceType } from '../../pagination';
import { PaginatedResponse } from '../../schemas/util';
import { NakamotoBlock, NakamotoBlockSchema } from '../../schemas/entities/block';
import {
NakamotoBlock,
NakamotoBlockSchema,
SignerSignatureSchema,
} from '../../schemas/entities/block';
import { TransactionSchema } from '../../schemas/entities/transactions';
import { BlockListV2ResponseSchema } from '../../schemas/responses/responses';
import {
BlockListV2ResponseSchema,
BlockSignerSignatureResponseSchema,
} from '../../schemas/responses/responses';

export const BlockRoutesV2: FastifyPluginAsync<
Record<never, never>,
Expand Down Expand Up @@ -174,5 +181,53 @@ export const BlockRoutesV2: FastifyPluginAsync<
}
);

fastify.get(
'/:height_or_hash/signer-signatures',
{
preHandler: handleBlockCache,
preValidation: (req, _reply, done) => {
cleanBlockHeightOrHashParam(req.params);
done();
},
schema: {
operationId: 'get_signer_signatures_for_block',
summary: 'Get signer signatures for block',
description: `Retrieves the signer signatures (an array of signature byte strings) in a single block`,
tags: ['Blocks'],
params: BlockParamsSchema,
querystring: Type.Object({
limit: LimitParam(ResourceType.BlockSignerSignature),
offset: OffsetParam(),
}),
response: {
200: BlockSignerSignatureResponseSchema,
},
},
},
async (req, reply) => {
const params = parseBlockParam(req.params.height_or_hash);
const query = req.query;

try {
const { limit, offset, results, total } = await fastify.db.v2.getBlockSignerSignature({
blockId: params,
...query,
});
const response = {
limit,
offset,
total,
results: results,
};
await reply.send(response);
} catch (error) {
if (error instanceof InvalidRequestError) {
throw new NotFoundError('Block not found');
}
throw error;
}
}
);

await Promise.resolve();
};
8 changes: 8 additions & 0 deletions src/api/routes/v2/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export const TransactionLimitParamSchema = Type.Integer({
description: 'Transactions per page',
});

export const BlockSignerSignatureLimitParamSchema = Type.Integer({
minimum: 1,
maximum: pagingQueryLimits[ResourceType.BlockSignerSignature].maxLimit,
default: pagingQueryLimits[ResourceType.BlockSignerSignature].defaultLimit,
title: 'Block signer signature limit',
description: 'Block signer signatures per page',
});

export const PoxCycleLimitParamSchema = Type.Integer({
minimum: 1,
maximum: pagingQueryLimits[ResourceType.PoxCycle].maxLimit,
Expand Down
4 changes: 4 additions & 0 deletions src/api/schemas/entities/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,7 @@ export const NakamotoBlockSchema = Type.Object({
execution_cost_write_length: Type.Integer({ description: 'Execution cost write length.' }),
});
export type NakamotoBlock = Static<typeof NakamotoBlockSchema>;

export const SignerSignatureSchema = Type.String({
description: "Array of hex strings representing the block's signer signature",
});
5 changes: 4 additions & 1 deletion src/api/schemas/responses/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
BurnchainRewardSchema,
BurnchainRewardSlotHolderSchema,
} from '../entities/burnchain-rewards';
import { NakamotoBlockSchema } from '../entities/block';
import { NakamotoBlockSchema, SignerSignatureSchema } from '../entities/block';

export const ErrorResponseSchema = Type.Object(
{
Expand Down Expand Up @@ -182,3 +182,6 @@ export type RunFaucetResponse = Static<typeof RunFaucetResponseSchema>;

export const BlockListV2ResponseSchema = PaginatedCursorResponse(NakamotoBlockSchema);
export type BlockListV2Response = Static<typeof BlockListV2ResponseSchema>;

export const BlockSignerSignatureResponseSchema = PaginatedResponse(SignerSignatureSchema);
export type BlockSignerSignatureResponse = Static<typeof BlockSignerSignatureResponseSchema>;
2 changes: 2 additions & 0 deletions src/datastore/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface DbBlock {
tx_count: number;
block_time: number;
signer_bitvec: string | null;
signer_signatures: string[] | null;
}

/** An interface representing the microblock data that can be constructed _only_ from the /new_microblocks payload */
Expand Down Expand Up @@ -1286,6 +1287,7 @@ export interface BlockInsertValues {
execution_cost_write_length: number;
tx_count: number;
signer_bitvec: string | null;
signer_signatures: PgBytea[] | null;
}

export interface MicroblockInsertValues {
Expand Down
1 change: 1 addition & 0 deletions src/datastore/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ export function parseBlockQueryResult(row: BlockQueryResult): DbBlock {
execution_cost_write_length: Number.parseInt(row.execution_cost_write_length),
tx_count: row.tx_count,
signer_bitvec: row.signer_bitvec,
signer_signatures: null, // this field is not queried from db by default due to size constraints
};
return block;
}
Expand Down
43 changes: 43 additions & 0 deletions src/datastore/pg-store-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
PoxSignerPaginationQueryParams,
PoxSignerLimitParamSchema,
BlockIdParam,
BlockSignerSignatureLimitParamSchema,
} from '../api/routes/v2/schemas';
import { InvalidRequestError, InvalidRequestErrorType } from '../errors';
import { normalizeHashString } from '../helpers';
Expand Down Expand Up @@ -226,6 +227,48 @@ export class PgStoreV2 extends BasePgStoreModule {
});
}

async getBlockSignerSignature(args: {
blockId: BlockIdParam;
limit?: number;
offset?: number;
}): Promise<DbPaginatedResult<string>> {
return await this.sqlTransaction(async sql => {
const limit = args.limit ?? BlockSignerSignatureLimitParamSchema.default;
const offset = args.offset ?? 0;
const blockId = args.blockId;
const filter =
blockId.type === 'latest'
? sql`index_block_hash = (SELECT index_block_hash FROM blocks WHERE canonical = TRUE ORDER BY block_height DESC LIMIT 1)`
: blockId.type === 'hash'
? sql`(
block_hash = ${normalizeHashString(blockId.hash)}
OR index_block_hash = ${normalizeHashString(blockId.hash)}
)`
: sql`block_height = ${blockId.height}`;
const blockQuery = await sql<{ signer_signatures: string[]; total: number }[]>`
SELECT
signer_signatures[${offset + 1}:${offset + limit}] as signer_signatures,
array_length(signer_signatures, 1)::integer AS total
FROM blocks
WHERE canonical = true AND ${filter}
LIMIT 1
`;
if (blockQuery.count === 0)
return {
limit,
offset,
results: [],
total: 0,
};
return {
limit,
offset,
results: blockQuery[0].signer_signatures,
total: blockQuery[0].total,
};
});
}

async getAverageBlockTimes(): Promise<{
last_1h: number;
last_24h: number;
Expand Down
2 changes: 2 additions & 0 deletions src/datastore/pg-write-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ export class PgWriteStore extends PgStore {
execution_cost_write_length: block.execution_cost_write_length,
tx_count: block.tx_count,
signer_bitvec: block.signer_bitvec,
signer_signatures: block.signer_signatures,
};
const result = await sql`
INSERT INTO blocks ${sql(values)}
Expand Down Expand Up @@ -3384,6 +3385,7 @@ export class PgWriteStore extends PgStore {
execution_cost_write_length: block.execution_cost_write_length,
tx_count: block.tx_count,
signer_bitvec: block.signer_bitvec,
signer_signatures: block.signer_signatures,
}));
await sql`
INSERT INTO blocks ${sql(values)}
Expand Down
1 change: 1 addition & 0 deletions src/event-stream/core-node-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export interface CoreNodeBlockMessage {
};
block_time: number;
signer_bitvec?: string | null;
signer_signature?: string[];
}

export interface CoreNodeParsedTxMessage {
Expand Down
6 changes: 6 additions & 0 deletions src/event-stream/event-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ async function handleBlockMessage(
? BitVec.consensusDeserializeToString(msg.signer_bitvec)
: null;

// Stacks-core does not include the '0x' prefix in the signer signature hex strings
const signerSignatures =
msg.signer_signature?.map(s => (s.startsWith('0x') ? s : '0x' + s)) ?? null;

const dbBlock: DbBlock = {
canonical: true,
block_hash: msg.block_hash,
Expand All @@ -319,6 +323,7 @@ async function handleBlockMessage(
tx_count: msg.transactions.length,
block_time: blockData.block_time,
signer_bitvec: signerBitvec,
signer_signatures: signerSignatures,
};

logger.debug(`Received block ${msg.block_hash} (${msg.block_height}) from node`, dbBlock);
Expand Down Expand Up @@ -1158,6 +1163,7 @@ export function parseNewBlockMessage(chainId: ChainID, msg: CoreNodeBlockMessage
execution_cost_write_length: totalCost.execution_cost_write_length,
tx_count: msg.transactions.length,
signer_bitvec: msg.signer_bitvec ?? null,
signer_signatures: msg.signer_signature ?? null,
};

const dbMinerRewards: DbMinerReward[] = [];
Expand Down
3 changes: 3 additions & 0 deletions tests/api/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ describe('address tests', () => {
execution_cost_write_length: 0,
tx_count: 1,
signer_bitvec: null,
signer_signatures: null,
};
let indexIdIndex = 0;
const createStxTx = (
Expand Down Expand Up @@ -1169,6 +1170,7 @@ describe('address tests', () => {
execution_cost_write_length: 0,
tx_count: 1,
signer_bitvec: null,
signer_signatures: null,
};

let indexIdIndex = 0;
Expand Down Expand Up @@ -2386,6 +2388,7 @@ describe('address tests', () => {
execution_cost_write_length: 0,
tx_count: 1,
signer_bitvec: null,
signer_signatures: null,
};
const txBuilder = await makeContractCall({
contractAddress: 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y',
Expand Down
Loading

0 comments on commit c389154

Please sign in to comment.