Skip to content

Commit 47bc7a6

Browse files
committed
feat(rpc): implement getVoteAccounts
- Add getVoteAccounts handler to RpcHookContext with full parameter support - Wire epoch_tracker to RPC hooks for epoch vote account lookups - Limit epoch credits to last 5 entries (matches Agave MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY) - Support commitment levels, votePubkey filter, keepUnstakedDelinquents, and delinquentSlotDistance - Partition validators into current/delinquent based on last vote slot - Rename SlotHookContext to RpcHookContext for broader RPC method support - Fix typo: delinquintSlotDistance -> delinquentSlotDistance
1 parent ccfde8d commit 47bc7a6

File tree

2 files changed

+126
-29
lines changed

2 files changed

+126
-29
lines changed

src/cmd.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1333,8 +1333,9 @@ fn validator(
13331333
});
13341334
defer replay_service_state.deinit(allocator);
13351335

1336-
try app_base.rpc_hooks.set(allocator, sig.rpc.methods.SlotHookContext{
1336+
try app_base.rpc_hooks.set(allocator, sig.rpc.methods.RpcHookContext{
13371337
.slot_tracker = &replay_service_state.replay_state.slot_tracker,
1338+
.epoch_tracker = &replay_service_state.replay_state.epoch_tracker,
13381339
});
13391340

13401341
const replay_thread = try replay_service_state.spawnService(

src/rpc/methods.zig

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const ParseOptions = std.json.ParseOptions;
1818
const Pubkey = sig.core.Pubkey;
1919
const Signature = sig.core.Signature;
2020
const Slot = sig.core.Slot;
21+
const Commitment = common.Commitment;
2122

2223
pub fn Result(comptime method: MethodAndParams.Tag) type {
2324
return union(enum) {
@@ -582,7 +583,7 @@ pub const GetVoteAccounts = struct {
582583
commitment: ?common.Commitment = null,
583584
votePubkey: ?Pubkey = null,
584585
keepUnstakedDelinquents: ?bool = null,
585-
delinquintSlotDistance: ?u64 = null,
586+
delinquentSlotDistance: ?u64 = null,
586587
};
587588

588589
pub const Response = struct {
@@ -645,7 +646,7 @@ pub const common = struct {
645646

646647
/// Used to configure several RPC method requests
647648
pub const CommitmentSlotConfig = struct {
648-
commitment: ?Commitment = null,
649+
commitment: ?common.Commitment = null,
649650
minContextSlot: ?sig.core.Slot = null,
650651
};
651652

@@ -687,30 +688,27 @@ pub const common = struct {
687688
};
688689
};
689690

690-
pub const SlotHookContext = struct {
691+
pub const RpcHookContext = struct {
691692
slot_tracker: *const sig.replay.trackers.SlotTracker,
693+
epoch_tracker: *const sig.replay.trackers.EpochTracker,
692694

693-
fn getLatestProcessedSlot(self: @This()) !Slot {
694-
const slot = self.slot_tracker.latest_processed_slot.get();
695-
if (slot == 0) {
696-
return error.RpcNoProcessedSlot;
697-
}
698-
return slot;
699-
}
695+
// Limit the length of the `epoch_credits` array for each validator in a `get_vote_accounts`
696+
// response.
697+
// See: https://github.com/anza-xyz/agave/blob/cd00ceb1fdf43f694caf7af23cb87987922fce2c/rpc-client-types/src/request.rs#L159
698+
const MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY: usize = 5;
700699

701-
fn getLatestConfirmedSlot(self: @This()) !Slot {
702-
const slot = self.slot_tracker.latest_confirmed_slot.get();
703-
if (slot == 0) {
704-
return error.RpcNoConfirmedSlot;
705-
}
706-
return slot;
707-
}
700+
// Validators that are this number of slots behind are considered delinquent.
701+
// See: https://github.com/anza-xyz/agave/blob/cd00ceb1fdf43f694caf7af23cb87987922fce2c/rpc-client-types/src/request.rs#L162
702+
const DELINQUENT_VALIDATOR_SLOT_DISTANCE: u64 = 128;
708703

709-
fn getLatestFinalizedSlot(self: @This()) !Slot {
710-
const slot = self.slot_tracker.root.load(.monotonic);
711-
if (slot == 0) {
712-
return error.RpcNoFinalizedSlot;
713-
}
704+
fn getSlotForCommitment(self: @This(), commitment: Commitment) !Slot {
705+
const slot = switch (commitment) {
706+
.processed => self.slot_tracker.latest_processed_slot.get(),
707+
.confirmed => self.slot_tracker.latest_confirmed_slot.get(),
708+
.finalized => self.slot_tracker.root.load(.monotonic),
709+
};
710+
711+
if (slot == 0) return error.RpcNoSlotForCommitment;
714712
return slot;
715713
}
716714

@@ -720,11 +718,7 @@ pub const SlotHookContext = struct {
720718
) !Slot {
721719
const commitment = config.commitment orelse .finalized;
722720

723-
const slot = switch (commitment) {
724-
.processed => try self.getLatestProcessedSlot(),
725-
.confirmed => try self.getLatestConfirmedSlot(),
726-
.finalized => try self.getLatestFinalizedSlot(),
727-
};
721+
const slot = try self.getSlotForCommitment(commitment);
728722

729723
if (config.minContextSlot) |min_slot| {
730724
if (slot < min_slot) {
@@ -737,7 +731,109 @@ pub const SlotHookContext = struct {
737731

738732
pub fn getSlot(self: @This(), _: std.mem.Allocator, params: GetSlot) !GetSlot.Response {
739733
const config = params.config orelse common.CommitmentSlotConfig{};
740-
741734
return self.getSlotImpl(config);
742735
}
736+
737+
pub fn getVoteAccounts(
738+
self: @This(),
739+
allocator: std.mem.Allocator,
740+
params: GetVoteAccounts,
741+
) !GetVoteAccounts.Response {
742+
const config: GetVoteAccounts.Config = params.config orelse .{};
743+
744+
// get slot for requested commitment.
745+
// TODO: double check if finalized is default if unspecified.
746+
const slot = try self.getSlotForCommitment(config.commitment orelse Commitment.finalized);
747+
748+
// Get the root slot's state which contains stakes_cache.
749+
const root_ref = self.slot_tracker.getRoot();
750+
751+
// Setup config consts for the request.
752+
// TODO: document default values such that they match Agave.
753+
const delinquent_distance = config.delinquentSlotDistance orelse
754+
DELINQUENT_VALIDATOR_SLOT_DISTANCE;
755+
const keep_unstaked = config.keepUnstakedDelinquents orelse false;
756+
const filter_pk = config.votePubkey;
757+
758+
// Get epoch info for epochVoteAccounts check
759+
// const epoch = self.epoch_tracker.schedule.getEpoch(slot);
760+
// TODO: make more idiomatic??
761+
const epoch_constants = self.epoch_tracker.getPtrForSlot(slot);
762+
const epoch_vote_accounts = if (epoch_constants) |ec|
763+
&ec.stakes.stakes.vote_accounts
764+
else
765+
null;
766+
767+
var current_list = std.ArrayList(GetVoteAccounts.VoteAccount).init(allocator);
768+
errdefer current_list.deinit();
769+
var delinqt_list = std.ArrayList(GetVoteAccounts.VoteAccount).init(allocator);
770+
errdefer delinqt_list.deinit();
771+
772+
// Access stakes cache (takes read lock).
773+
const stakes, var stakes_guard = root_ref.state.stakes_cache.stakes.readWithLock();
774+
const vote_accounts_map = &stakes.vote_accounts.vote_accounts;
775+
for (vote_accounts_map.keys(), vote_accounts_map.values()) |vote_pk, stake_and_vote| {
776+
// Apply filter if specified.
777+
if (filter_pk) |f| {
778+
if (!vote_pk.equals(&f)) continue;
779+
}
780+
781+
const vote_state = stake_and_vote.account.state;
782+
const activated_stake = stake_and_vote.stake;
783+
784+
// Get the slot this vote account last voted on.
785+
// TODO: is this correct?
786+
const last_vote_slot = vote_state.lastVotedSlot() orelse 0;
787+
788+
// Check if vote account is active in current epoch.
789+
const is_epoch_vote_account = if (epoch_vote_accounts) |eva|
790+
eva.vote_accounts.contains(vote_pk)
791+
else
792+
activated_stake > 0;
793+
794+
// Convert epoch credits to [3]u64 format
795+
const all_credits = vote_state.epoch_credits.items;
796+
const num_credits_to_return = @min(
797+
all_credits.len,
798+
MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY,
799+
);
800+
const epoch_credits = all_credits[all_credits.len - num_credits_to_return ..];
801+
var credits = try allocator.alloc([3]u64, num_credits_to_return);
802+
for (epoch_credits, 0..) |ec, i| {
803+
credits[i] = .{ ec.epoch, ec.credits, ec.prev_credits };
804+
}
805+
806+
const info = GetVoteAccounts.VoteAccount{
807+
.votePubkey = vote_pk,
808+
.nodePubkey = vote_state.node_pubkey,
809+
.activatedStake = activated_stake,
810+
.epochVoteAccount = is_epoch_vote_account,
811+
.commission = vote_state.commission,
812+
.lastVote = last_vote_slot,
813+
.epochCredits = credits,
814+
.rootSlot = vote_state.root_slot orelse 0, // TODO: is this correct?
815+
};
816+
817+
// Partition by delinquent status. current is set when last_vote_slot > slot - delinquent_distance.
818+
const is_current = if (slot >= delinquent_distance)
819+
last_vote_slot > slot - delinquent_distance
820+
else
821+
last_vote_slot > 0; // TODO: Should always be true?
822+
823+
if (is_current) {
824+
try current_list.append(info);
825+
} else {
826+
// TODO: is this check correct?
827+
if (keep_unstaked or activated_stake > 0) {
828+
try delinqt_list.append(info);
829+
}
830+
}
831+
}
832+
stakes_guard.unlock();
833+
834+
return .{
835+
.current = try current_list.toOwnedSlice(),
836+
.delinquent = try delinqt_list.toOwnedSlice(),
837+
};
838+
}
743839
};

0 commit comments

Comments
 (0)