Skip to content

Commit 38fdaf7

Browse files
Fix proposer shuffling decision slot at boundary (#8128)
Follow-up to the bug fixed in: - #8121 This fixes the root cause of that bug, which was introduced by me in: - #8101 Lion identified the issue here: - #8101 (comment) In the methods that compute the proposer shuffling decision root, ensure we don't use lookahead for the Fulu fork epoch itself. This is accomplished by checking if Fulu is enabled at `epoch - 1`, i.e. if `epoch > fulu_fork_epoch`. I haven't updated the methods that _compute_ shufflings to use these new corrected bounds (e.g. `BeaconState::compute_proposer_indices`), although we could make this change in future. The `get_beacon_proposer_indices` method already gracefully handles the Fulu boundary case by using the `proposer_lookahead` field (if initialised). Co-Authored-By: Michael Sproul <[email protected]>
1 parent edcfee6 commit 38fdaf7

File tree

3 files changed

+93
-8
lines changed

3 files changed

+93
-8
lines changed

beacon_node/beacon_chain/tests/store_tests.rs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,14 +1195,18 @@ fn check_shuffling_compatible(
11951195
///
11961196
/// - ProtoBlock::proposer_shuffling_root_for_child_block, and
11971197
/// - BeaconState::proposer_shuffling_decision_root{_at_epoch}
1198-
async fn proposer_shuffling_root_consistency_test(parent_slot: u64, child_slot: u64) {
1198+
async fn proposer_shuffling_root_consistency_test(
1199+
spec: ChainSpec,
1200+
parent_slot: u64,
1201+
child_slot: u64,
1202+
) {
11991203
let child_slot = Slot::new(child_slot);
12001204
let db_path = tempdir().unwrap();
1201-
let store = get_store(&db_path);
1205+
let store = get_store_generic(&db_path, Default::default(), spec.clone());
12021206
let validators_keypairs =
12031207
types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT);
12041208
let harness = TestHarness::builder(MinimalEthSpec)
1205-
.default_spec()
1209+
.spec(spec.into())
12061210
.keypairs(validators_keypairs)
12071211
.fresh_disk_store(store)
12081212
.mock_execution_layer()
@@ -1268,17 +1272,58 @@ async fn proposer_shuffling_root_consistency_test(parent_slot: u64, child_slot:
12681272

12691273
#[tokio::test]
12701274
async fn proposer_shuffling_root_consistency_same_epoch() {
1271-
proposer_shuffling_root_consistency_test(32, 39).await;
1275+
let spec = test_spec::<E>();
1276+
proposer_shuffling_root_consistency_test(spec, 32, 39).await;
12721277
}
12731278

12741279
#[tokio::test]
12751280
async fn proposer_shuffling_root_consistency_next_epoch() {
1276-
proposer_shuffling_root_consistency_test(32, 47).await;
1281+
let spec = test_spec::<E>();
1282+
proposer_shuffling_root_consistency_test(spec, 32, 47).await;
12771283
}
12781284

12791285
#[tokio::test]
12801286
async fn proposer_shuffling_root_consistency_two_epochs() {
1281-
proposer_shuffling_root_consistency_test(32, 55).await;
1287+
let spec = test_spec::<E>();
1288+
proposer_shuffling_root_consistency_test(spec, 32, 55).await;
1289+
}
1290+
1291+
#[tokio::test]
1292+
async fn proposer_shuffling_root_consistency_at_fork_boundary() {
1293+
let mut spec = ForkName::Electra.make_genesis_spec(E::default_spec());
1294+
spec.fulu_fork_epoch = Some(Epoch::new(4));
1295+
1296+
// Parent block in epoch prior to Fulu fork epoch, child block in Fulu fork epoch.
1297+
proposer_shuffling_root_consistency_test(
1298+
spec.clone(),
1299+
3 * E::slots_per_epoch(),
1300+
4 * E::slots_per_epoch(),
1301+
)
1302+
.await;
1303+
1304+
// Parent block and child block in Fulu fork epoch.
1305+
proposer_shuffling_root_consistency_test(
1306+
spec.clone(),
1307+
4 * E::slots_per_epoch(),
1308+
4 * E::slots_per_epoch() + 1,
1309+
)
1310+
.await;
1311+
1312+
// Parent block in Fulu fork epoch and child block in epoch after.
1313+
proposer_shuffling_root_consistency_test(
1314+
spec.clone(),
1315+
4 * E::slots_per_epoch(),
1316+
5 * E::slots_per_epoch(),
1317+
)
1318+
.await;
1319+
1320+
// Parent block in epoch prior and child block in epoch after.
1321+
proposer_shuffling_root_consistency_test(
1322+
spec,
1323+
3 * E::slots_per_epoch(),
1324+
5 * E::slots_per_epoch(),
1325+
)
1326+
.await;
12821327
}
12831328

12841329
#[tokio::test]

consensus/proto_array/src/proto_array_fork_choice.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,13 @@ impl Block {
172172
) -> Hash256 {
173173
let block_epoch = self.current_epoch_shuffling_id.shuffling_epoch;
174174

175-
if !spec.fork_name_at_epoch(child_block_epoch).fulu_enabled() {
175+
// For child blocks in the Fulu fork epoch itself, we want to use the old logic. There is no
176+
// lookahead in the first Fulu epoch. So we check whether Fulu is enabled at
177+
// `child_block_epoch - 1`, i.e. whether `child_block_epoch > fulu_fork_epoch`.
178+
if !spec
179+
.fork_name_at_epoch(child_block_epoch.saturating_sub(1_u64))
180+
.fulu_enabled()
181+
{
176182
// Prior to Fulu the proposer shuffling decision root for the current epoch is the same
177183
// as the attestation shuffling for the *next* epoch, i.e. it is determined at the start
178184
// of the current epoch.

consensus/types/src/chain_spec.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,13 @@ impl ChainSpec {
869869
///
870870
/// The block root at this slot can be used to key the proposer shuffling for the given epoch.
871871
pub fn proposer_shuffling_decision_slot<E: EthSpec>(&self, epoch: Epoch) -> Slot {
872-
if self.fork_name_at_epoch(epoch).fulu_enabled() {
872+
// At the Fulu fork epoch itself, the shuffling is computed "the old way" with no lookahead.
873+
// Therefore for `epoch == fulu_fork_epoch` we must take the `else` branch. Checking if Fulu
874+
// is enabled at `epoch - 1` accomplishes this neatly.
875+
if self
876+
.fork_name_at_epoch(epoch.saturating_sub(1_u64))
877+
.fulu_enabled()
878+
{
873879
// Post-Fulu the proposer shuffling decision slot for epoch N is the slot at the end
874880
// of epoch N - 2 (note: min_seed_lookahead=1 in all current configs).
875881
epoch
@@ -2999,4 +3005,32 @@ mod yaml_tests {
29993005
spec.min_epoch_data_availability_boundary(current_epoch)
30003006
);
30013007
}
3008+
3009+
#[test]
3010+
fn proposer_shuffling_decision_root_around_epoch_boundary() {
3011+
type E = MainnetEthSpec;
3012+
let fulu_fork_epoch = 5;
3013+
let spec = {
3014+
let mut spec = ForkName::Electra.make_genesis_spec(E::default_spec());
3015+
spec.fulu_fork_epoch = Some(Epoch::new(fulu_fork_epoch));
3016+
Arc::new(spec)
3017+
};
3018+
3019+
// For epochs prior to AND including the Fulu fork epoch, the decision slot is the end
3020+
// of the previous epoch (i.e. only 1 slot lookahead).
3021+
for epoch in (0..=fulu_fork_epoch).map(Epoch::new) {
3022+
assert_eq!(
3023+
spec.proposer_shuffling_decision_slot::<E>(epoch),
3024+
epoch.start_slot(E::slots_per_epoch()) - 1
3025+
);
3026+
}
3027+
3028+
// For epochs after Fulu, the decision slot is the end of the epoch two epochs prior.
3029+
for epoch in ((fulu_fork_epoch + 1)..(fulu_fork_epoch + 10)).map(Epoch::new) {
3030+
assert_eq!(
3031+
spec.proposer_shuffling_decision_slot::<E>(epoch),
3032+
(epoch - 1).start_slot(E::slots_per_epoch()) - 1
3033+
);
3034+
}
3035+
}
30023036
}

0 commit comments

Comments
 (0)