Skip to content

Commit

Permalink
feat: add signer last_seen and version fields
Browse files Browse the repository at this point in the history
* feat: add signer last_seen field

* chore: pretty print json responses

* chore: schema desc fix

* feat: include signer version
  • Loading branch information
zone117x authored Nov 5, 2024
1 parent ba87566 commit bcc0b16
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 13 deletions.
9 changes: 9 additions & 0 deletions src/api/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export async function buildApiServer(args: { db: PgStore }) {
await fastify.register(FastifyCors);
await fastify.register(Api, { prefix: '/signer-metrics' });

fastify.addHook('onSend', async (_req, reply, payload) => {
if ((reply.getHeader('Content-Type') as string).startsWith('application/json')) {
// Pretty-print with indentation
return JSON.stringify(JSON.parse(payload as string), null, 2);
} else {
return payload;
}
});

return fastify;
}

Expand Down
18 changes: 14 additions & 4 deletions src/api/routes/cycle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import { CycleSignerResponseSchema, CycleSignersResponseSchema } from '../schemas';
import {
CycleSigner,
CycleSignerResponse,
CycleSignerResponseSchema,
CycleSignersResponseSchema,
} from '../schemas';
import { parseTime } from '../../helpers';
import { InvalidRequestError } from '../errors';

Expand Down Expand Up @@ -68,7 +73,7 @@ export const CycleRoutes: FastifyPluginCallback<
});

const formatted = results.map(result => {
return {
const cycleSinger: CycleSigner = {
signer_key: result.signer_key,
weight: result.weight,
weight_percentage: result.weight_percentage,
Expand All @@ -79,7 +84,10 @@ export const CycleRoutes: FastifyPluginCallback<
proposals_rejected_count: result.proposals_rejected_count,
proposals_missed_count: result.proposals_missed_count,
average_response_time_ms: result.average_response_time_ms,
last_seen: result.last_block_response_time?.toISOString() ?? null,
version: result.last_metadata_server_version ?? null,
};
return cycleSinger;
});

return {
Expand Down Expand Up @@ -125,8 +133,7 @@ export const CycleRoutes: FastifyPluginCallback<
error: 'Signer not found',
});
}

return {
const cycleSigner: CycleSignerResponse = {
signer_key: signer.signer_key,
weight: signer.weight,
weight_percentage: signer.weight_percentage,
Expand All @@ -137,7 +144,10 @@ export const CycleRoutes: FastifyPluginCallback<
proposals_rejected_count: signer.proposals_rejected_count,
proposals_missed_count: signer.proposals_missed_count,
average_response_time_ms: signer.average_response_time_ms,
last_seen: signer.last_block_response_time?.toISOString() ?? null,
version: signer.last_metadata_server_version ?? null,
};
return cycleSigner;
});
await reply.send(result);
}
Expand Down
6 changes: 6 additions & 0 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ export const CycleSignerSchema = Type.Object({
description:
'Time duration (in milliseconds) taken to submit responses to block proposals (tracked best effort)',
}),
last_seen: Type.Union([Type.String(), Type.Null()], {
description: 'ISO timestamp of the last time a message from this signer was seen',
}),
version: Type.Union([Type.String(), Type.Null()], {
description: 'The last seen signer binary version reported by this signer',
}),
// TODO: implement these nice-to-have fields
/*
mined_blocks_accepted_included_count: Type.Integer({
Expand Down
37 changes: 35 additions & 2 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ export class PgStore extends BasePgStore {
proposals_rejected_count: number;
proposals_missed_count: number;
average_response_time_ms: number;
last_block_response_time: Date | null;
last_metadata_server_version: string | null;
}[]
>`
WITH signer_data AS (
Expand Down Expand Up @@ -329,10 +331,20 @@ export class PgStore extends BasePgStore {
br.signer_sighash,
br.accepted,
br.received_at,
br.metadata_server_version,
br.id
FROM block_responses br
JOIN proposal_data pd ON br.signer_sighash = pd.block_hash -- Only responses linked to selected proposals
),
latest_response_data AS (
-- Find the latest response time and corresponding metadata_server_version for each signer
SELECT DISTINCT ON (signer_key)
signer_key,
received_at AS last_block_response_time,
metadata_server_version
FROM response_data
ORDER BY signer_key, received_at DESC
),
signer_proposal_data AS (
-- Cross join signers with proposals and left join filtered responses
SELECT
Expand Down Expand Up @@ -377,12 +389,16 @@ export class PgStore extends BasePgStore {
ad.proposals_accepted_count,
ad.proposals_rejected_count,
ad.proposals_missed_count,
COALESCE(ad.average_response_time_ms, 0) AS average_response_time_ms
COALESCE(ad.average_response_time_ms, 0) AS average_response_time_ms,
COALESCE(lrd.last_block_response_time, NULL) AS last_block_response_time,
COALESCE(lrd.metadata_server_version, NULL) AS last_metadata_server_version
FROM signer_data sd
LEFT JOIN aggregated_data ad
ON sd.signer_key = ad.signer_key
LEFT JOIN signer_rank sr
ON sd.signer_key = sr.signer_key
LEFT JOIN latest_response_data lrd
ON sd.signer_key = lrd.signer_key -- Join the latest response time and metadata server version data
ORDER BY sd.signer_stacked_amount DESC, sd.signer_key ASC
`;
return dbRewardSetSigners;
Expand All @@ -401,6 +417,8 @@ export class PgStore extends BasePgStore {
proposals_rejected_count: number;
proposals_missed_count: number;
average_response_time_ms: number;
last_block_response_time: Date | null;
last_metadata_server_version: string | null;
}[]
>`
WITH signer_data AS (
Expand Down Expand Up @@ -429,11 +447,21 @@ export class PgStore extends BasePgStore {
br.signer_sighash,
br.accepted,
br.received_at,
br.metadata_server_version,
br.id
FROM block_responses br
JOIN proposal_data pd ON br.signer_sighash = pd.block_hash
WHERE br.signer_key = ${normalizeHexString(signerId)} -- Filter for the specific signer
),
latest_response_data AS (
-- Find the latest response time and corresponding metadata_server_version for the specific signer
SELECT DISTINCT ON (signer_key)
signer_key,
received_at AS last_block_response_time,
metadata_server_version
FROM response_data
ORDER BY signer_key, received_at DESC
),
signer_proposal_data AS (
-- Cross join the specific signer with proposals and left join filtered responses
SELECT
Expand All @@ -442,6 +470,7 @@ export class PgStore extends BasePgStore {
pd.proposal_received_at,
rd.accepted,
rd.received_at AS response_received_at,
rd.metadata_server_version,
EXTRACT(MILLISECOND FROM (rd.received_at - pd.proposal_received_at)) AS response_time_ms
FROM signer_data sd
CROSS JOIN proposal_data pd
Expand Down Expand Up @@ -478,12 +507,16 @@ export class PgStore extends BasePgStore {
ad.proposals_accepted_count,
ad.proposals_rejected_count,
ad.proposals_missed_count,
COALESCE(ad.average_response_time_ms, 0) AS average_response_time_ms
COALESCE(ad.average_response_time_ms, 0) AS average_response_time_ms,
COALESCE(lrd.last_block_response_time, NULL) AS last_block_response_time,
COALESCE(lrd.metadata_server_version, NULL) AS last_metadata_server_version
FROM signer_data sd
LEFT JOIN aggregated_data ad
ON sd.signer_key = ad.signer_key
LEFT JOIN signer_rank sr
ON sd.signer_key = sr.signer_key
LEFT JOIN latest_response_data lrd
ON sd.signer_key = lrd.signer_key
`;
return dbRewardSetSigner[0];
}
Expand Down
64 changes: 57 additions & 7 deletions tests/db/endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { PoxInfo, RpcStackerSetResponse } from '../../src/stacks-core-rpc/stacks-core-rpc-client';
import { rpcStackerSetToDbRewardSetSigners } from '../../src/stacks-core-rpc/stacker-set-updater';

describe('Postgres ingestion tests', () => {
describe('Endpoint tests', () => {
let db: PgStore;
let apiServer: FastifyInstance;

Expand Down Expand Up @@ -87,10 +87,6 @@ describe('Postgres ingestion tests', () => {
.expect(200);
const body: BlocksResponse = responseTest.body;

const firstBlockTime = new Date(body.results[0].block_time * 1000).toISOString();
const lastBlockTime = new Date((body.results.at(-1)?.block_time ?? 0) * 1000).toISOString();
console.log(`First block time: ${firstBlockTime}, Last block time: ${lastBlockTime}`);

// block 112274 has all signer states (missing, rejected, accepted, accepted_excluded)
const testBlock = body.results.find(r => r.block_height === 112274);
assert.ok(testBlock);
Expand Down Expand Up @@ -143,8 +139,30 @@ describe('Postgres ingestion tests', () => {
proposals_rejected_count: 12,
proposals_missed_count: 3,
average_response_time_ms: 26273.979,
last_seen: '2024-11-02T13:33:21.831Z',
version:
'stacks-signer signer-3.0.0.0.0.1 (release/signer-3.0.0.0.0.1:b26f406, release build, linux [x86_64])',
};
expect(testSigner).toEqual(expectedSignerData);

// this signer has missed all block_proposal (no block_response has been seen)
const miaSignerKey = '0x0399649284ed10a00405f032f8567b5e5463838aaa00af8d6bc9da71dda4e19c9c';
const miaSigner = body.results.find(r => r.signer_key === miaSignerKey);
const expectedMiaSignerData: CycleSigner = {
signer_key: '0x0399649284ed10a00405f032f8567b5e5463838aaa00af8d6bc9da71dda4e19c9c',
weight: 1,
weight_percentage: 2,
stacked_amount: '7700000000000',
stacked_amount_percent: 2.283,
stacked_amount_rank: 5,
proposals_accepted_count: 0,
proposals_rejected_count: 0,
proposals_missed_count: 99,
average_response_time_ms: 0,
last_seen: null,
version: null,
};
expect(miaSigner).toEqual(expectedMiaSignerData);
});

test('get signers for cycle with time range', async () => {
Expand Down Expand Up @@ -181,6 +199,9 @@ describe('Postgres ingestion tests', () => {
proposals_rejected_count: 0,
proposals_missed_count: 1,
average_response_time_ms: 28515,
last_seen: '2024-11-02T13:33:21.831Z',
version:
'stacks-signer signer-3.0.0.0.0.1 (release/signer-3.0.0.0.0.1:b26f406, release build, linux [x86_64])',
};
expect(testSigner1).toEqual(expectedSignerData1);

Expand All @@ -204,7 +225,7 @@ describe('Postgres ingestion tests', () => {
const signersBody3: CycleSignersResponse = signersResp3.body;
const testSigner3 = signersBody3.results.find(r => r.signer_key === testSignerKey1);
// should return data for the oldest block
expect(testSigner3).toEqual({
const expected3: CycleSigner = {
signer_key: '0x02e8620935d58ebffa23c260f6917cbd0915ea17d7a46df17e131540237d335504',
weight: 38,
weight_percentage: 76,
Expand All @@ -215,7 +236,11 @@ describe('Postgres ingestion tests', () => {
proposals_rejected_count: 0,
proposals_missed_count: 0,
average_response_time_ms: 29020,
});
last_seen: '2024-11-02T13:30:43.731Z',
version:
'stacks-signer signer-3.0.0.0.0.1 (release/signer-3.0.0.0.0.1:b26f406, release build, linux [x86_64])',
};
expect(testSigner3).toEqual(expected3);
});

test('get signer for cycle', async () => {
Expand All @@ -236,7 +261,32 @@ describe('Postgres ingestion tests', () => {
proposals_rejected_count: 12,
proposals_missed_count: 3,
average_response_time_ms: 26273.979,
last_seen: '2024-11-02T13:33:21.831Z',
version:
'stacks-signer signer-3.0.0.0.0.1 (release/signer-3.0.0.0.0.1:b26f406, release build, linux [x86_64])',
};
expect(body).toEqual(expectedSignerData);

// this signer has missed all block_proposal (no block_response has been seen)
const miaSignerKey = '0x0399649284ed10a00405f032f8567b5e5463838aaa00af8d6bc9da71dda4e19c9c';
const responseTest2 = await supertest(apiServer.server)
.get(`/signer-metrics/v1/cycles/72/signers/${miaSignerKey}`)
.expect(200);
const miaSigner: CycleSignerResponse = responseTest2.body;
const expectedMiaSignerData: CycleSigner = {
signer_key: '0x0399649284ed10a00405f032f8567b5e5463838aaa00af8d6bc9da71dda4e19c9c',
weight: 1,
weight_percentage: 2,
stacked_amount: '7700000000000',
stacked_amount_percent: 2.283,
stacked_amount_rank: 5,
proposals_accepted_count: 0,
proposals_rejected_count: 0,
proposals_missed_count: 99,
average_response_time_ms: 0,
last_seen: null,
version: null,
};
expect(miaSigner).toEqual(expectedMiaSignerData);
});
});

0 comments on commit bcc0b16

Please sign in to comment.