Skip to content

Commit c4591b3

Browse files
committed
feat(replay): wire latest_processed_slot for RPC getSlot
- Add SlotTree.tip() to get highest slot among fork leaves (bypass mode) - Update latest_processed_slot inside vote handling to match Agave behavior - Update latest_processed_slot in bypassConsensus() from SlotTree.tip() - Change ForkChoiceProcessedSlot to use store() since slot can decrease Note: Bypass mode uses highest fork tip, while consensus mode matches Agave's semantics where processed slot only updates when voting.
1 parent a29f437 commit c4591b3

File tree

3 files changed

+35
-3
lines changed

3 files changed

+35
-3
lines changed

src/replay/consensus/core.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,12 @@ pub const TowerConsensus = struct {
675675
slot_leaders,
676676
vote_sockets,
677677
);
678+
679+
// Update the latest processed slot to the bank being voted on.
680+
// This matches Agave's behavior: the processed slot is updated inside
681+
// handle_votable_bank(), which is only called when vote_bank.is_some().
682+
// See: https://github.com/anza-xyz/agave/blob/5e900421520a10933642d5e9a21e191a70f9b125/core/src/replay_stage.rs#L2683
683+
slot_tracker.latest_processed_slot.set(voted.slot);
678684
}
679685

680686
// Reset onto a fork

src/replay/service.zig

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,6 @@ fn freezeCompletedSlots(state: *ReplayState, results: []const ReplayResult) !boo
609609
slot,
610610
last_entry_hash,
611611
));
612-
state.slot_tracker.latest_confirmed_slot.update(slot);
613612
processed_a_slot = true;
614613
} else {
615614
state.logger.info().logf("partially replayed slot: {}", .{slot});
@@ -622,6 +621,20 @@ fn freezeCompletedSlots(state: *ReplayState, results: []const ReplayResult) !boo
622621

623622
/// bypass the tower bft consensus protocol, simply rooting slots with SlotTree.reRoot
624623
fn bypassConsensus(state: *ReplayState) !void {
624+
// NOTE: Processed slot semantics differ from Agave when Sig is in bypass-consensus mode.
625+
// In bypass mode, `latest_processed_slot` is set to the highest slot among all fork
626+
// leaves (SlotTree.tip()).
627+
//
628+
// This differs from Agave's behavior: the processed slot is only updated
629+
// when `vote_bank.is_some()` (i.e., when the validator has selected a bank
630+
// to vote on after passing all consensus checks like lockout, threshold, and
631+
// switch proof). If the validator is locked out or fails
632+
// threshold checks, the processed slot is NOT updated and can go stale.
633+
// See: https://github.com/anza-xyz/agave/blob/5e900421520a10933642d5e9a21e191a70f9b125/core/src/replay_stage.rs#L2683
634+
//
635+
// TowerConsensus implements Agave's processed slot semantics when consensus is enabled.
636+
state.slot_tracker.latest_processed_slot.set(state.slot_tree.tip());
637+
625638
if (state.slot_tree.reRoot(state.allocator)) |new_root| {
626639
const slot_tracker = &state.slot_tracker;
627640

src/replay/trackers.zig

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ const SlotState = sig.core.SlotState;
1414
pub const ForkChoiceProcessedSlot = struct {
1515
slot: std.atomic.Value(Slot) = .init(0),
1616

17-
pub fn update(self: *@This(), new_slot: Slot) void {
18-
_ = self.slot.fetchMax(new_slot, .monotonic);
17+
/// Set the current processed slot (heaviest fork tip).
18+
/// Uses store() because this can decrease when the fork choice
19+
/// switches to a different fork with a lower slot.
20+
pub fn set(self: *@This(), new_slot: Slot) void {
21+
self.slot.store(new_slot, .monotonic);
1922
}
2023

2124
pub fn get(self: *const @This()) Slot {
@@ -246,6 +249,16 @@ pub const SlotTree = struct {
246249
const List = std.ArrayListUnmanaged;
247250
const min_age = 32;
248251

252+
/// Returns the highest slot among all fork tips (leaves).
253+
/// In bypass mode (without ForkChoice), this represents the "processed" slot.
254+
pub fn tip(self: *const SlotTree) Slot {
255+
var max_slot: Slot = self.root.slot;
256+
for (self.leaves.items) |leaf| {
257+
max_slot = @max(max_slot, leaf.slot);
258+
}
259+
return max_slot;
260+
}
261+
249262
pub fn deinit(const_self: SlotTree, allocator: Allocator) void {
250263
var self = const_self;
251264
self.root.destroyRecursivelyDownstream(allocator);

0 commit comments

Comments
 (0)