Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/signer last seen #38

Merged
merged 4 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to figure out how to optimize this using fast-json-stringify from fastify, but it doesn't look like it's built-in and would be a bit more complicated to maintain, to avoid the second stringify, as well as the parse.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. I'm just leaving it here for now until we have a better view than the raw json in a web browser :D

} else {
return payload;
}

Check warning on line 41 in src/api/init.ts

View check run for this annotation

Codecov / codecov/patch

src/api/init.ts#L40-L41

Added lines #L40 - L41 were not covered by tests
});

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);
});
});
Loading