Skip to content

Commit cb3ff68

Browse files
authored
refactor(consensus, forkchoice) Add forkchoice.heaviestSlotOnSameVotedFork method (#729)
This adds the implementation for heaviestSlotOnSameVotedFork. This could not be implemented until now because it depends on Tower.
1 parent cbaa05c commit cb3ff68

File tree

2 files changed

+224
-4
lines changed

2 files changed

+224
-4
lines changed

src/consensus/fork_choice.zig

Lines changed: 222 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Pubkey = sig.core.Pubkey;
1111
const SortedMap = sig.utils.collections.SortedMap;
1212
const SlotAndHash = sig.core.hash.SlotAndHash;
1313
const Slot = sig.core.Slot;
14+
const ReplayTower = sig.consensus.replay_tower.ReplayTower;
1415

1516
const UpdateLabel = enum {
1617
Aggregate,
@@ -102,7 +103,7 @@ pub const ForkInfo = struct {
102103
// clear the latest invalid ancestor
103104
if (latest_duplicate_ancestor <= newly_duplicate_ancestor) {
104105
self.logger.info().logf(
105-
\\ Fork choice for {} clearing latest invalid ancestor
106+
\\ Fork choice for {} clearing latest invalid ancestor
106107
\\ {} because {} was duplicate confirmed
107108
,
108109
.{ my_key, latest_duplicate_ancestor, newly_duplicate_ancestor },
@@ -1074,6 +1075,81 @@ pub const ForkChoice = struct {
10741075
}
10751076
}
10761077

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+
10771153
fn setStakeVotedAt(
10781154
self: *ForkChoice,
10791155
slot_hash_key: *const SlotAndHash,
@@ -1166,6 +1242,8 @@ fn doInsertAggregateOperation(
11661242
return true;
11671243
}
11681244
const test_allocator = std.testing.allocator;
1245+
const createTestReplayTower = sig.consensus.replay_tower.createTestReplayTower;
1246+
const createTestSlotHistory = sig.consensus.replay_tower.createTestSlotHistory;
11691247

11701248
// [Agave] https://github.com/anza-xyz/agave/blob/92b11cd2eef1d3f5434d6af702f7d7a85ffcfca9/core/src/consensus/heaviest_subtree_fork_choice.rs#L3281
11711249
test "HeaviestSubtreeForkChoice.subtreeDiff" {
@@ -1978,6 +2056,148 @@ test "HeaviestSubtreeForkChoice.isStrictAncestor_is_maybe_ancestor" {
19782056
try std.testing.expect(fork_choice.isStrictAncestor(&maybe_ancestor, &key));
19792057
}
19802058

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+
19812201
pub fn forkChoiceForTest(
19822202
allocator: std.mem.Allocator,
19832203
forks: []const TreeNode,
@@ -2008,7 +2228,7 @@ pub fn forkChoiceForTest(
20082228

20092229
const TreeNode = std.meta.Tuple(&.{ SlotAndHash, ?SlotAndHash });
20102230

2011-
const fork_tuples = [_]TreeNode{
2231+
pub const fork_tuples = [_]TreeNode{
20122232
// (0)
20132233
// └── (1)
20142234
// ├── (2)

src/consensus/replay_tower.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,7 +2614,7 @@ test "greatestCommonAncestor" {
26142614
const builtin = @import("builtin");
26152615
const DynamicArrayBitSet = sig.bloom.bit_set.DynamicArrayBitSet;
26162616

2617-
fn createTestReplayTower(
2617+
pub fn createTestReplayTower(
26182618
allocator: std.mem.Allocator,
26192619
threshold_depth: usize,
26202620
threshold_size: f64,
@@ -2709,7 +2709,7 @@ fn voteAndCheckRecent(num_votes: usize) !void {
27092709
);
27102710
}
27112711

2712-
fn createTestSlotHistory(
2712+
pub fn createTestSlotHistory(
27132713
allocator: std.mem.Allocator,
27142714
) !SlotHistory {
27152715
if (!builtin.is_test) {

0 commit comments

Comments
 (0)