Skip to content

Commit a13c1c7

Browse files
authored
Merge pull request #800 from oasisprotocol/jberci/feature/validators
Add recent uptimes to validator metadata in the client api
2 parents d522dd1 + 00dd266 commit a13c1c7

File tree

10 files changed

+3815
-5
lines changed

10 files changed

+3815
-5
lines changed

.changelog/800.feature.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Track liveness of consensus validator nodes
2+
3+
Consensus validator API now includes uptime statistics for the last
4+
24 hours.

analyzer/consensus_accounts_list/accounts_list.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ const (
1717
analyzerName = "consensus_account_list"
1818

1919
defaultInterval = 2 * time.Minute
20+
vacuumInterval = 2 * time.Hour
2021

21-
vacuumInterval = 2 * time.Hour
22-
23-
accountListViewRefreshQuery = `REFRESH MATERIALIZED VIEW CONCURRENTLY views.accounts_list`
2422
accountsListVacuumQuery = `VACUUM ANALYZE views.accounts_list`
23+
accountListViewRefreshQuery = `REFRESH MATERIALIZED VIEW CONCURRENTLY views.accounts_list`
24+
25+
validatorUptimesVacuumQuery = `VACUUM ANALYZE views.validator_uptimes`
26+
validatorUptimesViewRefreshQuery = `REFRESH MATERIALIZED VIEW CONCURRENTLY views.validator_uptimes`
2527
)
2628

2729
type processor struct {
@@ -66,13 +68,19 @@ func (p *processor) GetItems(ctx context.Context, limit uint64) ([]struct{}, err
6668

6769
func (p *processor) ProcessItem(ctx context.Context, batch *storage.QueryBatch, item struct{}) error {
6870
batch.Queue(accountListViewRefreshQuery)
71+
batch.Queue(validatorUptimesViewRefreshQuery)
6972

7073
if time.Since(p.lastVacuum) > vacuumInterval {
7174
_, err := p.target.Exec(ctx, accountsListVacuumQuery)
7275
if err != nil {
7376
p.logger.Error("failed to vacuum accounts list view", "error", err)
7477
return nil
7578
}
79+
_, err = p.target.Exec(ctx, validatorUptimesVacuumQuery)
80+
if err != nil {
81+
p.logger.Error("failed to vacuum validator uptimes view", "error", err)
82+
return nil
83+
}
7684
p.lastVacuum = time.Now()
7785
}
7886

api/spec/v1.yaml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2456,8 +2456,41 @@ components:
24562456
description: An array containing details of the last 100 consensus blocks, indicating whether each block was signed by the validator. Only available when querying a single validator.
24572457
items:
24582458
allOf: [$ref: '#/components/schemas/ValidatorSignedBlock']
2459+
uptime:
2460+
allOf: [$ref: '#/components/schemas/ValidatorUptime']
2461+
description: The validator's uptime statistics for a period of time up to now.
24592462
description: |
2460-
An validator registered at the consensus layer.
2463+
A validator registered at the consensus layer.
2464+
2465+
ValidatorUptime:
2466+
type: object
2467+
required: [window_length, segment_length, window_uptime, segment_uptimes]
2468+
properties:
2469+
window_length:
2470+
type: integer
2471+
format: uint64
2472+
description: |
2473+
The length of the historical window for which this object provides uptime information, in blocks.
2474+
Currently always 14400 blocks, or approximately 24 hours.
2475+
segment_length:
2476+
type: integer
2477+
format: uint64
2478+
description: |
2479+
The length of the window segment, in blocks. We subdivide the window into segments of equal length
2480+
and aggregate the uptime of each segment into `segment_uptimes`.
2481+
Currently always 1200 blocks, which is approximately 2 hours.
2482+
window_uptime:
2483+
type: integer
2484+
format: uint64
2485+
description: The number of blocks signed by the validator out of the last window_length blocks.
2486+
segment_uptimes:
2487+
type: array
2488+
description: |
2489+
An array showing the signed block counts for each sub-segment within window_length.
2490+
The segments are in reverse-chronological order; ie the first element represents the most recent segment of blocks.
2491+
items:
2492+
type: integer
2493+
format: uint64
24612494

24622495
ValidatorSignedBlock:
24632496
type: object

storage/client/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,9 @@ func (c *StorageClient) Validators(ctx context.Context, p apiTypes.GetConsensusV
13801380
}
13811381
var schedule staking.CommissionSchedule
13821382
var logoUrl *string
1383+
1384+
var uptimeWindowLength, uptimeSegmentLength, windowUptime *uint64
1385+
var segmentUptimes []uint64
13831386
if err = res.rows.Scan(
13841387
&v.EntityID,
13851388
&v.EntityAddress,
@@ -1401,6 +1404,10 @@ func (c *StorageClient) Validators(ctx context.Context, p apiTypes.GetConsensusV
14011404
&v.InValidatorSet,
14021405
&v.Media,
14031406
&logoUrl,
1407+
&uptimeWindowLength,
1408+
&uptimeSegmentLength,
1409+
&windowUptime,
1410+
&segmentUptimes,
14041411
); err != nil {
14051412
return nil, wrapError(err)
14061413
}
@@ -1411,6 +1418,17 @@ func (c *StorageClient) Validators(ctx context.Context, p apiTypes.GetConsensusV
14111418
}
14121419
v.Media.LogoUrl = logoUrl
14131420
}
1421+
if uptimeWindowLength != nil && uptimeSegmentLength != nil && windowUptime != nil {
1422+
v.Uptime = &apiTypes.ValidatorUptime{
1423+
WindowLength: *uptimeWindowLength,
1424+
SegmentLength: *uptimeSegmentLength,
1425+
WindowUptime: *windowUptime,
1426+
SegmentUptimes: segmentUptimes,
1427+
}
1428+
} else {
1429+
v.Uptime = nil
1430+
}
1431+
14141432
currentRate := schedule.CurrentRate(beacon.EpochTime(epoch.ID))
14151433
if currentRate != nil {
14161434
v.CurrentRate = currentRate.ToBigInt().Uint64()

storage/client/queries/queries.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,12 +461,17 @@ const (
461461
EXISTS(SELECT NULL FROM chain.nodes WHERE chain.entities.id = chain.nodes.entity_id AND chain.nodes.roles LIKE '%validator%') AS active,
462462
EXISTS(SELECT NULL FROM chain.nodes WHERE chain.entities.id = chain.nodes.entity_id AND voting_power > 0) AS in_validator_set,
463463
chain.entities.meta AS meta,
464-
chain.entities.logo_url as logo_url
464+
chain.entities.logo_url as logo_url,
465+
uptimes.window_length,
466+
uptimes.segment_length,
467+
uptimes.window_signed,
468+
uptimes.segments_signed
465469
FROM chain.entities
466470
JOIN chain.accounts ON chain.entities.address = chain.accounts.address
467471
JOIN chain.blocks ON chain.entities.start_block = chain.blocks.height
468472
LEFT JOIN chain.commissions ON chain.entities.address = chain.commissions.address
469473
LEFT JOIN self_delegations ON chain.entities.address = self_delegations.address
474+
LEFT JOIN views.validator_uptimes as uptimes ON chain.entities.id = uptimes.signer_entity_id
470475
LEFT JOIN history.validators ON chain.entities.id = history.validators.id
471476
-- Find the epoch id from 24 hours ago. Each epoch is ~1hr.
472477
AND history.validators.epoch = (SELECT id - 24 from chain.epochs
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
BEGIN;
2+
3+
CREATE MATERIALIZED VIEW views.validator_uptimes AS
4+
-- With a limit of 14400 blocks, this is the last ~24 hrs of signatures.
5+
WITH last_window_blocks AS (
6+
SELECT height, signer_entity_ids
7+
FROM chain.blocks
8+
ORDER BY height DESC
9+
LIMIT 14400
10+
OFFSET 1 -- Omit the most recent block; signatures for it are obtained in the next block.
11+
),
12+
-- Generate a series of 12 segments representing ~2 hours within the window.
13+
all_segments AS (
14+
SELECT generate_series(0, 11) AS segment_id
15+
),
16+
-- Segments of blocks of ~2 hours within the main window, with expanded signers.
17+
segment_blocks AS (
18+
SELECT
19+
height,
20+
UNNEST(signer_entity_ids) AS signer_entity_id,
21+
(ROW_NUMBER() OVER (ORDER BY height DESC) - 1) / 1200 AS segment_id
22+
FROM last_window_blocks
23+
),
24+
-- Count signed blocks in each segment.
25+
segment_counts AS (
26+
SELECT
27+
signer_entity_id,
28+
segment_id,
29+
COUNT(height) AS signed_blocks_count
30+
FROM
31+
segment_blocks
32+
-- Compute this for all validators; the client can select from the view if needed.
33+
GROUP BY
34+
signer_entity_id, segment_id
35+
)
36+
-- Group windows per signer and calculate overall percentage.
37+
SELECT
38+
signers.signer_entity_id AS signer_entity_id,
39+
COALESCE(SUM(signed_blocks_count), 0) AS window_signed,
40+
ARRAY_AGG(COALESCE(segment_counts.signed_blocks_count, 0) ORDER BY segment_counts.segment_id) AS segments_signed,
41+
14400 AS window_length, -- 14400 blocks per window.
42+
1200 AS segment_length -- 1200 blocks per segment.
43+
FROM
44+
-- Ensure we have all windows for each signer, even if they didn't sign in a particular window.
45+
(SELECT DISTINCT signer_entity_id FROM segment_counts) AS signers
46+
CROSS JOIN all_segments
47+
LEFT JOIN segment_counts ON signers.signer_entity_id = segment_counts.signer_entity_id AND all_segments.segment_id = segment_counts.segment_id
48+
GROUP BY
49+
signers.signer_entity_id;
50+
51+
CREATE UNIQUE INDEX ix_views_validator_uptimes_signer_entity_id ON views.validator_uptimes (signer_entity_id); -- A unique index is required for CONCURRENTLY refreshing the view.
52+
53+
-- Grant others read-only use. This does NOT apply to future tables in the schema.
54+
GRANT SELECT ON ALL TABLES IN SCHEMA views TO PUBLIC;
55+
56+
END;

tests/e2e_regression/damask/expected/validator.body

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,25 @@
441441
}
442442
],
443443
"start_date": "2022-04-11T09:30:00Z",
444+
"uptime": {
445+
"segment_length": 1200,
446+
"segment_uptimes": [
447+
884,
448+
0,
449+
0,
450+
0,
451+
0,
452+
0,
453+
0,
454+
0,
455+
0,
456+
0,
457+
0,
458+
0
459+
],
460+
"window_length": 14400,
461+
"window_uptime": 884
462+
},
444463
"voting_power": 34834350461211,
445464
"voting_power_cumulative": 34834350461211
446465
}

0 commit comments

Comments
 (0)