@@ -11,6 +11,7 @@ const Pubkey = sig.core.Pubkey;
11
11
const SortedMap = sig .utils .collections .SortedMap ;
12
12
const SlotAndHash = sig .core .hash .SlotAndHash ;
13
13
const Slot = sig .core .Slot ;
14
+ const ReplayTower = sig .consensus .replay_tower .ReplayTower ;
14
15
15
16
const UpdateLabel = enum {
16
17
Aggregate ,
@@ -102,7 +103,7 @@ pub const ForkInfo = struct {
102
103
// clear the latest invalid ancestor
103
104
if (latest_duplicate_ancestor <= newly_duplicate_ancestor ) {
104
105
self .logger .info ().logf (
105
- \\ Fork choice for {} clearing latest invalid ancestor
106
+ \\ Fork choice for {} clearing latest invalid ancestor
106
107
\\ {} because {} was duplicate confirmed
107
108
,
108
109
.{ my_key , latest_duplicate_ancestor , newly_duplicate_ancestor },
@@ -1074,6 +1075,81 @@ pub const ForkChoice = struct {
1074
1075
}
1075
1076
}
1076
1077
1078
+ /// [Agave] https://github.com/anza-xyz/agave/blob/9dbfe93720019942a3d70e0d609b654a57c42555/core/src/consensus/heaviest_subtree_fork_choice.rs#L1133
1079
+ fn heaviestSlotOnSameVotedFork (
1080
+ self : * const ForkChoice ,
1081
+ replay_tower : * const ReplayTower ,
1082
+ ) ! ? SlotAndHash {
1083
+ if (replay_tower .lastVotedSlotHash ()) | last_voted_slot_hash | {
1084
+ if (self .isCandidate (& last_voted_slot_hash )) | is_candidate | {
1085
+ if (is_candidate ) {
1086
+ return self .heaviestSlot (last_voted_slot_hash );
1087
+ } else {
1088
+ // In this case our last voted fork has been marked invalid because
1089
+ // it contains a duplicate block. It is critical that we continue to
1090
+ // build on it as long as there exists at least 1 non duplicate fork.
1091
+ // This is because there is a chance that this fork is actually duplicate
1092
+ // confirmed but not observed because there is no block containing the
1093
+ // required votes.
1094
+ //
1095
+ // Scenario 1:
1096
+ // Slot 0 - Slot 1 (90%)
1097
+ // |
1098
+ // - Slot 1'
1099
+ // |
1100
+ // - Slot 2 (10%)
1101
+ //
1102
+ // Imagine that 90% of validators voted for Slot 1, but because of the existence
1103
+ // of Slot 1', Slot 1 is marked as invalid in fork choice. It is impossible to reach
1104
+ // the required switch threshold for these validators to switch off of Slot 1 to Slot 2.
1105
+ // In this case it is important for someone to build a Slot 3 off of Slot 1 that contains
1106
+ // the votes for Slot 1. At this point they will see that the fork off of Slot 1 is duplicate
1107
+ // confirmed, and the rest of the network can repair Slot 1, and mark it is a valid candidate
1108
+ // allowing fork choice to converge.
1109
+ //
1110
+ // This will only occur after Slot 2 has been created, in order to resolve the following
1111
+ // scenario:
1112
+ //
1113
+ // Scenario 2:
1114
+ // Slot 0 - Slot 1 (30%)
1115
+ // |
1116
+ // - Slot 1' (30%)
1117
+ //
1118
+ // In this scenario only 60% of the network has voted before the duplicate proof for Slot 1 and 1'
1119
+ // was viewed. Neither version of the slot will reach the duplicate confirmed threshold, so it is
1120
+ // critical that a new fork Slot 2 from Slot 0 is created to allow the validators on Slot 1 and
1121
+ // Slot 1' to switch. Since the `best_slot` is an ancestor of the last vote (Slot 0 is ancestor of last
1122
+ // vote Slot 1 or Slot 1'), we will trigger `SwitchForkDecision::FailedSwitchDuplicateRollback`, which
1123
+ // will create an alternate fork off of Slot 0. Once this alternate fork is created, the `best_slot`
1124
+ // will be Slot 2, at which point we will be in Scenario 1 and continue building off of Slot 1 or Slot 1'.
1125
+ //
1126
+ // For more details see the case for
1127
+ // `SwitchForkDecision::FailedSwitchDuplicateRollback` in `ReplayStage::select_vote_and_reset_forks`.
1128
+ return self .deepestSlot (& last_voted_slot_hash );
1129
+ }
1130
+ } else {
1131
+ if (! replay_tower .isStrayLastVote ()) {
1132
+ // Unless last vote is stray and stale, self.is_candidate(last_voted_slot_hash) must return
1133
+ // Some(_), justifying to panic! here.
1134
+ // Also, adjust_lockouts_after_replay() correctly makes last_voted_slot None,
1135
+ // if all saved votes are ancestors of replayed_root_slot. So this code shouldn't be
1136
+ // touched in that case as well.
1137
+ // In other words, except being stray, all other slots have been voted on while this
1138
+ // validator has been running, so we must be able to fetch best_slots for all of
1139
+ // them.
1140
+ return error .MissingCandidate ;
1141
+ } else {
1142
+ // fork_infos doesn't have corresponding data for the stale stray last vote,
1143
+ // meaning some inconsistency between saved tower and ledger.
1144
+ // (newer snapshot, or only a saved tower is moved over to new setup?)
1145
+ return null ;
1146
+ }
1147
+ }
1148
+ } else {
1149
+ return null ;
1150
+ }
1151
+ }
1152
+
1077
1153
fn setStakeVotedAt (
1078
1154
self : * ForkChoice ,
1079
1155
slot_hash_key : * const SlotAndHash ,
@@ -1166,6 +1242,8 @@ fn doInsertAggregateOperation(
1166
1242
return true ;
1167
1243
}
1168
1244
const test_allocator = std .testing .allocator ;
1245
+ const createTestReplayTower = sig .consensus .replay_tower .createTestReplayTower ;
1246
+ const createTestSlotHistory = sig .consensus .replay_tower .createTestSlotHistory ;
1169
1247
1170
1248
// [Agave] https://github.com/anza-xyz/agave/blob/92b11cd2eef1d3f5434d6af702f7d7a85ffcfca9/core/src/consensus/heaviest_subtree_fork_choice.rs#L3281
1171
1249
test "HeaviestSubtreeForkChoice.subtreeDiff" {
@@ -1978,6 +2056,148 @@ test "HeaviestSubtreeForkChoice.isStrictAncestor_is_maybe_ancestor" {
1978
2056
try std .testing .expect (fork_choice .isStrictAncestor (& maybe_ancestor , & key ));
1979
2057
}
1980
2058
2059
+ test "HeaviestSubtreeForkChoice.heaviestSlotOnSameVotedFork_stray_restored_slot" {
2060
+ const tree = [_ ]TreeNode {
2061
+ //
2062
+ // (0)
2063
+ // └── (1)
2064
+ // ├── (2)
2065
+ //
2066
+ .{
2067
+ SlotAndHash { .slot = 1 , .hash = Hash .ZEROES },
2068
+ SlotAndHash { .slot = 0 , .hash = Hash .ZEROES },
2069
+ },
2070
+ .{
2071
+ SlotAndHash { .slot = 2 , .hash = Hash .ZEROES },
2072
+ SlotAndHash { .slot = 1 , .hash = Hash .ZEROES },
2073
+ },
2074
+ };
2075
+ var fork_choice = try forkChoiceForTest (test_allocator , tree [0.. ]);
2076
+ defer fork_choice .deinit ();
2077
+
2078
+ var replay_tower = try createTestReplayTower (test_allocator , 10 , 0.9 );
2079
+ defer replay_tower .deinit (test_allocator );
2080
+ _ = try replay_tower .recordBankVote (test_allocator , 1 , Hash .ZEROES );
2081
+
2082
+ try std .testing .expect (! replay_tower .isStrayLastVote ());
2083
+ try std .testing .expectEqualDeep (
2084
+ SlotAndHash { .slot = @as (Slot , 2 ), .hash = Hash .ZEROES },
2085
+ (try fork_choice .heaviestSlotOnSameVotedFork (& replay_tower )).? ,
2086
+ );
2087
+
2088
+ // Make slot 1 (existing in bank_forks) a restored stray slot
2089
+ var slot_history = try createTestSlotHistory (test_allocator );
2090
+ defer slot_history .deinit (test_allocator );
2091
+
2092
+ slot_history .add (0 );
2093
+ // Work around TooOldSlotHistory
2094
+ slot_history .add (999 );
2095
+
2096
+ try replay_tower .adjustLockoutsAfterReplay (test_allocator , 0 , & slot_history );
2097
+
2098
+ try std .testing .expect (replay_tower .isStrayLastVote ());
2099
+ try std .testing .expectEqual (
2100
+ SlotAndHash { .slot = @as (Slot , 2 ), .hash = Hash .ZEROES },
2101
+ (try fork_choice .heaviestSlotOnSameVotedFork (& replay_tower )).? ,
2102
+ );
2103
+
2104
+ // Make slot 3 (NOT existing in bank_forks) a restored stray slot
2105
+ _ = try replay_tower .recordBankVote (test_allocator , 3 , Hash .ZEROES );
2106
+ try replay_tower .adjustLockoutsAfterReplay (test_allocator , 0 , & slot_history );
2107
+
2108
+ try std .testing .expect (replay_tower .isStrayLastVote ());
2109
+ try std .testing .expectEqual (
2110
+ null ,
2111
+ try fork_choice .heaviestSlotOnSameVotedFork (& replay_tower ),
2112
+ );
2113
+ }
2114
+
2115
+ test "HeaviestSubtreeForkChoice.heaviestSlotOnSameVotedFork_last_voted_not_found" {
2116
+ var fork_choice = try forkChoiceForTest (test_allocator , fork_tuples [0.. ]);
2117
+ defer fork_choice .deinit ();
2118
+
2119
+ var replay_tower = try createTestReplayTower (test_allocator , 10 , 0.9 );
2120
+ defer replay_tower .deinit (test_allocator );
2121
+
2122
+ try std .testing .expectEqualDeep (
2123
+ null ,
2124
+ (try fork_choice .heaviestSlotOnSameVotedFork (& replay_tower )),
2125
+ );
2126
+ }
2127
+
2128
+ test "HeaviestSubtreeForkChoice.heaviestSlotOnSameVotedFork_use_deepest_slot" {
2129
+ const tree = [_ ]TreeNode {
2130
+ //
2131
+ // (0)
2132
+ // └── (1)
2133
+ // ├── (2)
2134
+ //
2135
+ .{
2136
+ SlotAndHash { .slot = 1 , .hash = Hash .ZEROES },
2137
+ SlotAndHash { .slot = 0 , .hash = Hash .ZEROES },
2138
+ },
2139
+ .{
2140
+ SlotAndHash { .slot = 2 , .hash = Hash .ZEROES },
2141
+ SlotAndHash { .slot = 1 , .hash = Hash .ZEROES },
2142
+ },
2143
+ };
2144
+ var fork_choice = try forkChoiceForTest (test_allocator , & tree );
2145
+ defer fork_choice .deinit ();
2146
+
2147
+ // Create a tower that voted on slot 1.
2148
+ var replay_tower = try createTestReplayTower (test_allocator , 10 , 0.9 );
2149
+ defer replay_tower .deinit (test_allocator );
2150
+ _ = try replay_tower .recordBankVote (test_allocator , 1 , Hash .ZEROES );
2151
+
2152
+ // Initially, slot 1 is valid so we get the heaviest slot (which would be 2)
2153
+ try std .testing .expectEqualDeep (
2154
+ SlotAndHash { .slot = @as (Slot , 2 ), .hash = Hash .ZEROES },
2155
+ (try fork_choice .heaviestSlotOnSameVotedFork (& replay_tower )).? ,
2156
+ );
2157
+
2158
+ // Now mark slot 1 as invalid
2159
+ try fork_choice .markForkInvalidCandidate (
2160
+ & SlotAndHash { .slot = 1 , .hash = Hash .ZEROES },
2161
+ );
2162
+ try std .testing .expect (
2163
+ ! fork_choice .isCandidate (& SlotAndHash { .slot = 1 , .hash = Hash .ZEROES }).? ,
2164
+ );
2165
+
2166
+ // Now heaviestSlotOnSameVotedFork should return the deepest slot (2)
2167
+ // even though the fork is invalid
2168
+ try std .testing .expectEqualDeep (
2169
+ SlotAndHash { .slot = @as (Slot , 2 ), .hash = Hash .ZEROES },
2170
+ (try fork_choice .heaviestSlotOnSameVotedFork (& replay_tower )).? ,
2171
+ );
2172
+ }
2173
+
2174
+ test "HeaviestSubtreeForkChoice.heaviestSlotOnSameVotedFork_missing_candidate" {
2175
+ const tree = [_ ]TreeNode {
2176
+ //
2177
+ // (0)
2178
+ // └── (1)
2179
+ //
2180
+ .{
2181
+ SlotAndHash { .slot = 1 , .hash = Hash .ZEROES },
2182
+ SlotAndHash { .slot = 0 , .hash = Hash .ZEROES },
2183
+ },
2184
+ };
2185
+ var fork_choice = try forkChoiceForTest (test_allocator , & tree );
2186
+ defer fork_choice .deinit ();
2187
+
2188
+ // Create a tower that voted on slot 2 which doesn't exist in the fork choice.
2189
+ var replay_tower = try createTestReplayTower (test_allocator , 10 , 0.9 );
2190
+ defer replay_tower .deinit (test_allocator );
2191
+ _ = try replay_tower .recordBankVote (test_allocator , 2 , Hash .ZEROES );
2192
+
2193
+ try std .testing .expect (! replay_tower .isStrayLastVote ());
2194
+
2195
+ try std .testing .expectError (
2196
+ error .MissingCandidate ,
2197
+ fork_choice .heaviestSlotOnSameVotedFork (& replay_tower ),
2198
+ );
2199
+ }
2200
+
1981
2201
pub fn forkChoiceForTest (
1982
2202
allocator : std.mem.Allocator ,
1983
2203
forks : []const TreeNode ,
@@ -2008,7 +2228,7 @@ pub fn forkChoiceForTest(
2008
2228
2009
2229
const TreeNode = std .meta .Tuple (&.{ SlotAndHash , ? SlotAndHash });
2010
2230
2011
- const fork_tuples = [_ ]TreeNode {
2231
+ pub const fork_tuples = [_ ]TreeNode {
2012
2232
// (0)
2013
2233
// └── (1)
2014
2234
// ├── (2)
0 commit comments