|
| 1 | +//! Consumes the per-validator status data produced by the epoch transition |
| 2 | +//! and records validator metrics for the validators registered with `registerLocalValidator`. |
| 3 | + |
| 4 | +const std = @import("std"); |
| 5 | + |
| 6 | +const types = @import("consensus_types"); |
| 7 | +const metrics = @import("metrics.zig"); |
| 8 | +const attester_status = @import("utils/attester_status.zig"); |
| 9 | +const hasMarkers = attester_status.hasMarkers; |
| 10 | + |
| 11 | +const Allocator = std.mem.Allocator; |
| 12 | +const Epoch = types.primitive.Epoch.Type; |
| 13 | +const ValidatorIndex = types.primitive.ValidatorIndex.Type; |
| 14 | + |
| 15 | +pub const ValidatorMonitor = @This(); |
| 16 | + |
| 17 | +allocator: Allocator, |
| 18 | +/// Unordered list of validators that require additional monitoring. |
| 19 | +validators: std.AutoArrayHashMapUnmanaged(ValidatorIndex, void), |
| 20 | +/// Prevents registering statuses for the same epoch twice. |
| 21 | +/// processEpoch() may be run more than once for the same epoch. |
| 22 | +last_registered_status_epoch: ?Epoch, |
| 23 | + |
| 24 | +pub fn init(allocator: Allocator) ValidatorMonitor { |
| 25 | + return .{ |
| 26 | + .allocator = allocator, |
| 27 | + .validators = .empty, |
| 28 | + .last_registered_status_epoch = null, |
| 29 | + }; |
| 30 | +} |
| 31 | + |
| 32 | +pub fn deinit(self: *ValidatorMonitor) void { |
| 33 | + self.validators.deinit(self.allocator); |
| 34 | + self.* = undefined; |
| 35 | +} |
| 36 | + |
| 37 | +/// Adds a validator to the list of monitored validators. |
| 38 | +/// |
| 39 | +/// Registering an already-monitored validator is a no-op. |
| 40 | +pub fn registerLocalValidator(self: *ValidatorMonitor, io: std.Io, index: ValidatorIndex) !void { |
| 41 | + try self.validators.put(self.allocator, index, {}); |
| 42 | +} |
| 43 | + |
| 44 | +/// Prunes a validator from the list of monitored validators. |
| 45 | +/// |
| 46 | +/// Unregistering an unknown validator is a no-op. |
| 47 | +pub fn unregisterLocalValidator(self: *ValidatorMonitor, io: std.Io, index: ValidatorIndex) void { |
| 48 | + _ = self.validators.swapRemove(index); |
| 49 | +} |
| 50 | + |
| 51 | +/// Registers the per-validator statuses produced by one epoch transition and |
| 52 | +/// records metrics for all monitored validators. |
| 53 | +/// |
| 54 | +/// `flags` are the packed attester flags of `EpochTransitionCache` (see `utils/attester_status.zig`). |
| 55 | +/// `balances` is optional; when present the total balance of all monitored |
| 56 | +/// validators is reported. |
| 57 | +/// |
| 58 | +/// NOTE: Gossip-derived metrics (inclusion distance, attester hit/miss, correct head) |
| 59 | +/// are recorded by the lodestar-ts validator monitor, which observes attestations |
| 60 | +/// on the network; the state transition cannot see them post-altair. |
| 61 | +/// TODO(bing): port the rest of validator monitor, this is just a minimal |
| 62 | +/// impl for stf integration |
| 63 | +pub fn registerValidatorStatuses( |
| 64 | + self: *ValidatorMonitor, |
| 65 | + current_epoch: Epoch, |
| 66 | + flags: []const u8, |
| 67 | + balances: ?[]const u64, |
| 68 | +) void { |
| 69 | + |
| 70 | + // Prevent registering statuses for the same epoch twice. |
| 71 | + if (self.last_registered_status_epoch) |last_registered_status_epoch| |
| 72 | + if (current_epoch <= last_registered_status_epoch) return; |
| 73 | + |
| 74 | + self.last_registered_status_epoch = current_epoch; |
| 75 | + |
| 76 | + // There won't be any validator activity in epoch -1. |
| 77 | + if (current_epoch == 0) return; |
| 78 | + |
| 79 | + const vm = &metrics.validator_monitor; |
| 80 | + |
| 81 | + // Track total balance instead of per-validator balance to reduce metric cardinality. |
| 82 | + var total_balance: u64 = 0; |
| 83 | + |
| 84 | + for (self.validators.keys()) |index| { |
| 85 | + // The monitored validator may not be in the state yet. |
| 86 | + if (index >= flags.len) continue; |
| 87 | + |
| 88 | + const flag = flags[index]; |
| 89 | + |
| 90 | + if (hasMarkers(flag, attester_status.FLAG_PREV_SOURCE_ATTESTER)) { |
| 91 | + vm.prev_epoch_on_chain_source_attester_hit.incr(); |
| 92 | + } else { |
| 93 | + vm.prev_epoch_on_chain_source_attester_miss.incr(); |
| 94 | + } |
| 95 | + if (hasMarkers(flag, attester_status.FLAG_PREV_HEAD_ATTESTER)) { |
| 96 | + vm.prev_epoch_on_chain_head_attester_hit.incr(); |
| 97 | + } else { |
| 98 | + vm.prev_epoch_on_chain_head_attester_miss.incr(); |
| 99 | + } |
| 100 | + if (hasMarkers(flag, attester_status.FLAG_PREV_TARGET_ATTESTER)) { |
| 101 | + vm.prev_epoch_on_chain_target_attester_hit.incr(); |
| 102 | + } else { |
| 103 | + vm.prev_epoch_on_chain_target_attester_miss.incr(); |
| 104 | + } |
| 105 | + |
| 106 | + if (balances) |b| { |
| 107 | + if (index < b.len) total_balance += b[index]; |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + if (balances != null) { |
| 112 | + vm.prev_epoch_on_chain_balance.set(total_balance); |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +test "registerValidatorStatuses records metrics" { |
| 117 | + const allocator = std.testing.allocator; |
| 118 | + try metrics.init(allocator, std.testing.io, .{}); |
| 119 | + defer metrics.deinit(); |
| 120 | + |
| 121 | + var monitor = ValidatorMonitor.init(allocator); |
| 122 | + defer monitor.deinit(); |
| 123 | + try monitor.registerLocalValidator(std.testing.io, 0); |
| 124 | + try monitor.registerLocalValidator(std.testing.io, 1); |
| 125 | + |
| 126 | + const flags = [_]u8{ |
| 127 | + attester_status.FLAG_PREV_SOURCE_ATTESTER | |
| 128 | + attester_status.FLAG_PREV_TARGET_ATTESTER | |
| 129 | + attester_status.FLAG_PREV_HEAD_ATTESTER, |
| 130 | + 0, |
| 131 | + }; |
| 132 | + const balances = [_]u64{ 32_000_000_000, 31_000_000_000 }; |
| 133 | + monitor.registerValidatorStatuses(1, &flags, &balances); |
| 134 | + |
| 135 | + var aw: std.Io.Writer.Allocating = .init(allocator); |
| 136 | + var list = _: { |
| 137 | + errdefer aw.deinit(); |
| 138 | + try metrics.write(&aw.writer); |
| 139 | + break :_ aw.toArrayList(); |
| 140 | + }; |
| 141 | + defer list.deinit(allocator); |
| 142 | + |
| 143 | + const expectations = [_][]const u8{ |
| 144 | + "validator_monitor_prev_epoch_on_chain_source_attester_hit_total 1", |
| 145 | + "validator_monitor_prev_epoch_on_chain_source_attester_miss_total 1", |
| 146 | + "validator_monitor_prev_epoch_on_chain_target_attester_hit_total 1", |
| 147 | + "validator_monitor_prev_epoch_on_chain_target_attester_miss_total 1", |
| 148 | + "validator_monitor_prev_epoch_on_chain_head_attester_hit_total 1", |
| 149 | + "validator_monitor_prev_epoch_on_chain_head_attester_miss_total 1", |
| 150 | + "validator_monitor_prev_epoch_on_chain_balance 63000000000", |
| 151 | + }; |
| 152 | + for (expectations) |expected| { |
| 153 | + std.testing.expect(std.mem.indexOf(u8, list.items, expected) != null) catch |err| { |
| 154 | + std.debug.print("expected metric not found: {s}\n", .{expected}); |
| 155 | + return err; |
| 156 | + }; |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +test "registerValidatorStatuses guards" { |
| 161 | + var monitor = ValidatorMonitor.init(std.testing.allocator); |
| 162 | + defer monitor.deinit(); |
| 163 | + |
| 164 | + try monitor.registerLocalValidator(0); |
| 165 | + try monitor.registerLocalValidator(2); |
| 166 | + // registering twice is a no-op |
| 167 | + try monitor.registerLocalValidator(0); |
| 168 | + try std.testing.expectEqual(@as(usize, 2), monitor.validators.count()); |
| 169 | + |
| 170 | + // unregistering removes; unknown index is a no-op |
| 171 | + monitor.unregisterLocalValidator(2); |
| 172 | + monitor.unregisterLocalValidator(99); |
| 173 | + try std.testing.expectEqual(@as(usize, 1), monitor.validators.count()); |
| 174 | + try monitor.registerLocalValidator(2); |
| 175 | + try std.testing.expectEqual(@as(usize, 2), monitor.validators.count()); |
| 176 | + |
| 177 | + const flags = [_]u8{ |
| 178 | + attester_status.FLAG_PREV_SOURCE_ATTESTER | attester_status.FLAG_PREV_TARGET_ATTESTER, |
| 179 | + 0, |
| 180 | + attester_status.FLAG_PREV_HEAD_ATTESTER, |
| 181 | + }; |
| 182 | + const balances = [_]u64{ 32_000_000_000, 31_000_000_000, 33_000_000_000 }; |
| 183 | + |
| 184 | + // epoch 0 is registered but has no previous epoch activity |
| 185 | + monitor.registerValidatorStatuses(std.testing.io, 0, &flags, &balances); |
| 186 | + try std.testing.expectEqual(@as(?Epoch, 0), monitor.last_registered_status_epoch); |
| 187 | + |
| 188 | + monitor.registerValidatorStatuses(std.testing.io, 1, &flags, &balances); |
| 189 | + try std.testing.expectEqual(@as(?Epoch, 1), monitor.last_registered_status_epoch); |
| 190 | + |
| 191 | + // same epoch twice is a no-op |
| 192 | + monitor.registerValidatorStatuses(std.testing.io, 1, &flags, &balances); |
| 193 | + try std.testing.expectEqual(@as(?Epoch, 1), monitor.last_registered_status_epoch); |
| 194 | +} |
0 commit comments