@@ -18,6 +18,7 @@ const ParseOptions = std.json.ParseOptions;
1818const Pubkey = sig .core .Pubkey ;
1919const Signature = sig .core .Signature ;
2020const Slot = sig .core .Slot ;
21+ const Commitment = common .Commitment ;
2122
2223pub 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