Skip to content

Commit 82dc8b4

Browse files
authored
Ensure payload envelope streamer always serves canonical envelopes after the split slot (#9085)
Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>
1 parent cfc7483 commit 82dc8b4

9 files changed

Lines changed: 533 additions & 36 deletions

File tree

beacon_node/beacon_chain/src/canonical_head.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,24 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
383383
Ok((head, execution_status))
384384
}
385385

386-
// TODO(gloas) just a stub for now, implement this once we have fork choice.
387-
/// Returns true if the payload for this block is canonical according to fork choice
388-
/// Returns an error if the block root doesn't exist in fork choice.
389-
pub fn block_has_canonical_payload(&self, _root: &Hash256) -> Result<bool, Error> {
390-
Ok(true)
386+
/// Returns `true` if the payload for this block is canonical (Full) according to fork choice.
387+
pub fn block_has_canonical_payload(
388+
&self,
389+
root: &Hash256,
390+
spec: &ChainSpec,
391+
) -> Result<bool, Error> {
392+
let cached_head = self.cached_head();
393+
let head_root = cached_head.head_block_root();
394+
let head_payload_status = cached_head.head_payload_status();
395+
396+
if *root == head_root {
397+
return Ok(head_payload_status == PayloadStatus::Full);
398+
}
399+
400+
self.fork_choice_read_lock()
401+
.get_canonical_payload_status(root, spec)
402+
.map(|status| status == PayloadStatus::Full)
403+
.map_err(Error::ForkChoiceError)
391404
}
392405

393406
/// Returns a clone of `self.cached_head`.

beacon_node/beacon_chain/src/payload_envelope_streamer/beacon_chain_adapter.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ impl<T: BeaconChainTypes> EnvelopeStreamerBeaconAdapter<T> {
3737
&self,
3838
root: &Hash256,
3939
) -> Result<bool, BeaconChainError> {
40-
self.chain.canonical_head.block_has_canonical_payload(root)
40+
self.chain
41+
.canonical_head
42+
.block_has_canonical_payload(root, &self.chain.spec)
4143
}
4244
}

beacon_node/beacon_chain/src/payload_envelope_streamer/mod.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,8 @@ impl<T: BeaconChainTypes> PayloadEnvelopeStreamer<T> {
132132
results.push((*root, Ok(None)));
133133
}
134134
}
135-
Err(_) => {
136-
results.push((
137-
*root,
138-
Err(BeaconChainError::EnvelopeStreamerError(
139-
Error::BlockMissingFromForkChoice,
140-
)),
141-
));
135+
Err(e) => {
136+
results.push((*root, Err(e)));
142137
}
143138
}
144139
} else {

beacon_node/beacon_chain/src/payload_envelope_streamer/tests.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use crate::beacon_chain::ForkChoiceError;
23
use crate::payload_envelope_streamer::beacon_chain_adapter::MockEnvelopeStreamerBeaconAdapter;
34
use crate::test_utils::EphemeralHarnessType;
45
use bls::{FixedBytesExtended, Signature};
@@ -279,15 +280,18 @@ async fn stream_envelopes_by_root() {
279280
}
280281

281282
/// When `block_has_canonical_payload` returns an error, the streamer should
282-
/// yield `Err(EnvelopeStreamerError(BlockMissingFromForkChoice))` for those roots.
283+
/// propagate that error for those roots.
283284
#[tokio::test]
284285
async fn stream_envelopes_error() {
285286
let chain = build_chain(4, &[], &[], &[]);
286287
let (mut mock, _runtime) = mock_adapter();
287288
mock.expect_get_split_slot().return_const(Slot::new(0));
288289
mock_envelopes(&mut mock, &chain);
289-
mock.expect_block_has_canonical_payload()
290-
.returning(|_| Err(BeaconChainError::CanonicalHeadLockTimeout));
290+
mock.expect_block_has_canonical_payload().returning(|_| {
291+
Err(BeaconChainError::ForkChoiceError(
292+
ForkChoiceError::DoesNotDescendFromFinalizedCheckpoint,
293+
))
294+
});
291295

292296
let streamer = PayloadEnvelopeStreamer::new(mock, EnvelopeRequestSource::ByRange);
293297
let mut stream = streamer.launch_stream(roots(&chain));
@@ -299,13 +303,8 @@ async fn stream_envelopes_error() {
299303
.unwrap_or_else(|| panic!("stream ended early at index {i}"));
300304
assert_eq!(root, entry.block_root, "root mismatch at index {i}");
301305
assert!(
302-
matches!(
303-
result.as_ref(),
304-
Err(BeaconChainError::EnvelopeStreamerError(
305-
Error::BlockMissingFromForkChoice
306-
))
307-
),
308-
"expected BlockMissingFromForkChoice error at index {i}, got {:?}",
306+
result.as_ref().is_err(),
307+
"expected error at index {i}, got {:?}",
309308
result
310309
);
311310
}

consensus/fork_choice/src/fork_choice.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ pub enum Error<T> {
7878
UnrealizedVoteProcessing(state_processing::EpochProcessingError),
7979
ValidatorStatuses(BeaconStateError),
8080
ChainSpecError(String),
81+
DoesNotDescendFromFinalizedCheckpoint,
8182
}
8283

8384
impl<T> From<InvalidAttestation> for Error<T> {
@@ -1523,6 +1524,29 @@ where
15231524
}
15241525
}
15251526

1527+
/// Returns the canonical payload status of a block. See
1528+
/// `ProtoArrayForkChoice::get_canonical_payload_status`.
1529+
pub fn get_canonical_payload_status(
1530+
&self,
1531+
block_root: &Hash256,
1532+
spec: &ChainSpec,
1533+
) -> Result<PayloadStatus, Error<T::Error>> {
1534+
if self.is_finalized_checkpoint_or_descendant(*block_root) {
1535+
let current_slot = self.fc_store.get_current_slot();
1536+
let proposer_boost_root = self.fc_store.proposer_boost_root();
1537+
self.proto_array
1538+
.get_canonical_payload_status::<E>(
1539+
block_root,
1540+
current_slot,
1541+
proposer_boost_root,
1542+
spec,
1543+
)
1544+
.map_err(Error::ProtoArrayError)
1545+
} else {
1546+
Err(Error::DoesNotDescendFromFinalizedCheckpoint)
1547+
}
1548+
}
1549+
15261550
/// Returns the weight for the given block root.
15271551
pub fn get_block_weight(&self, block_root: &Hash256) -> Option<u64> {
15281552
self.proto_array.get_weight(block_root)

consensus/proto_array/src/fork_choice_test_definition.rs

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod gloas_payload;
44
mod no_votes;
55
mod votes;
66

7+
use crate::error::Error;
78
use crate::proto_array_fork_choice::{Block, ExecutionStatus, PayloadStatus, ProtoArrayForkChoice};
89
use crate::{InvalidationOperation, JustifiedBalances};
910
use fixed_bytes::FixedBytesExtended;
@@ -30,6 +31,8 @@ pub enum Operation {
3031
justified_state_balances: Vec<u64>,
3132
expected_head: Hash256,
3233
current_slot: Slot,
34+
// TODO(gloas): Make this non-optional. `find_head` always returns a `PayloadStatus`
35+
// (Empty for pre-GLOAS), so every test should assert on it explicitly.
3336
#[serde(default)]
3437
expected_payload_status: Option<PayloadStatus>,
3538
},
@@ -61,6 +64,12 @@ pub enum Operation {
6164
block_root: Hash256,
6265
attestation_slot: Slot,
6366
},
67+
ProcessGloasAttestation {
68+
validator_index: usize,
69+
block_root: Hash256,
70+
attestation_slot: Slot,
71+
payload_present: bool,
72+
},
6473
ProcessPayloadAttestation {
6574
validator_index: usize,
6675
block_root: Hash256,
@@ -105,6 +114,16 @@ pub enum Operation {
105114
block_root: Hash256,
106115
expected: bool,
107116
},
117+
AssertPayloadStatusByWeight {
118+
block_root: Hash256,
119+
expected_status: PayloadStatus,
120+
/// Override `current_slot`. Defaults to the `current_slot` of the last `FindHead`.
121+
#[serde(default)]
122+
current_slot: Option<Slot>,
123+
/// Override the proposer boost root. Defaults to `Hash256::zero()`.
124+
#[serde(default)]
125+
proposer_boost_root: Option<Hash256>,
126+
},
108127
}
109128

110129
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -149,6 +168,7 @@ impl ForkChoiceTestDefinition {
149168
)
150169
.expect("should create fork choice struct");
151170
let equivocating_indices = BTreeSet::new();
171+
let mut last_current_slot = Slot::new(0);
152172

153173
for (op_index, op) in self.operations.into_iter().enumerate() {
154174
match op.clone() {
@@ -189,6 +209,16 @@ impl ForkChoiceTestDefinition {
189209
op_index, op
190210
);
191211
}
212+
assert_canonical_payload_status_matches_find_head(
213+
&fork_choice,
214+
&head,
215+
current_slot,
216+
Hash256::zero(),
217+
&spec,
218+
payload_status,
219+
op_index,
220+
);
221+
last_current_slot = current_slot;
192222
check_bytes_round_trip(&fork_choice);
193223
}
194224
Operation::ProposerBoostFindHead {
@@ -201,7 +231,7 @@ impl ForkChoiceTestDefinition {
201231
let justified_balances =
202232
JustifiedBalances::from_effective_balances(justified_state_balances)
203233
.unwrap();
204-
let (head, _payload_status) = fork_choice
234+
let (head, payload_status) = fork_choice
205235
.find_head::<MainnetEthSpec>(
206236
justified_checkpoint,
207237
finalized_checkpoint,
@@ -220,6 +250,15 @@ impl ForkChoiceTestDefinition {
220250
"Operation at index {} failed head check. Operation: {:?}",
221251
op_index, op
222252
);
253+
assert_canonical_payload_status_matches_find_head(
254+
&fork_choice,
255+
&head,
256+
Slot::new(0),
257+
proposer_boost_root,
258+
&spec,
259+
payload_status,
260+
op_index,
261+
);
223262
check_bytes_round_trip(&fork_choice);
224263
}
225264
Operation::InvalidFindHead {
@@ -308,6 +347,27 @@ impl ForkChoiceTestDefinition {
308347
});
309348
check_bytes_round_trip(&fork_choice);
310349
}
350+
Operation::ProcessGloasAttestation {
351+
validator_index,
352+
block_root,
353+
attestation_slot,
354+
payload_present,
355+
} => {
356+
fork_choice
357+
.process_attestation(
358+
validator_index,
359+
block_root,
360+
attestation_slot,
361+
payload_present,
362+
)
363+
.unwrap_or_else(|_| {
364+
panic!(
365+
"process_attestation op at index {} returned error",
366+
op_index
367+
)
368+
});
369+
check_bytes_round_trip(&fork_choice);
370+
}
311371
Operation::ProcessPayloadAttestation {
312372
validator_index,
313373
block_root,
@@ -522,6 +582,26 @@ impl ForkChoiceTestDefinition {
522582
op_index
523583
);
524584
}
585+
Operation::AssertPayloadStatusByWeight {
586+
block_root,
587+
expected_status,
588+
current_slot,
589+
proposer_boost_root,
590+
} => {
591+
let actual = fork_choice
592+
.get_canonical_payload_status::<MainnetEthSpec>(
593+
&block_root,
594+
current_slot.unwrap_or(last_current_slot),
595+
proposer_boost_root.unwrap_or_else(Hash256::zero),
596+
&spec,
597+
)
598+
.unwrap();
599+
assert_eq!(
600+
actual, expected_status,
601+
"canonical payload status mismatch at op index {}",
602+
op_index
603+
);
604+
}
525605
}
526606
}
527607
}
@@ -546,6 +626,37 @@ fn get_checkpoint(i: u64) -> Checkpoint {
546626
}
547627
}
548628

629+
/// Checks that `get_canonical_payload_status` agrees with the `payload_status`
630+
/// returned by `find_head` for the head block.
631+
fn assert_canonical_payload_status_matches_find_head(
632+
fork_choice: &ProtoArrayForkChoice,
633+
head: &Hash256,
634+
current_slot: Slot,
635+
proposer_boost_root: Hash256,
636+
spec: &ChainSpec,
637+
expected: PayloadStatus,
638+
op_index: usize,
639+
) {
640+
match fork_choice.get_canonical_payload_status::<MainnetEthSpec>(
641+
head,
642+
current_slot,
643+
proposer_boost_root,
644+
spec,
645+
) {
646+
Ok(actual) => assert_eq!(
647+
actual, expected,
648+
"get_canonical_payload_status disagreed with find_head for head {:?} at op index {}",
649+
head, op_index
650+
),
651+
// Skip the check for pre-gloas nodes
652+
Err(Error::InvalidNodeVariant { .. }) => {}
653+
Err(e) => panic!(
654+
"get_canonical_payload_status failed at op index {}: {:?}",
655+
op_index, e
656+
),
657+
}
658+
}
659+
549660
fn check_bytes_round_trip(original: &ProtoArrayForkChoice) {
550661
let bytes = original.as_bytes();
551662
let decoded = ProtoArrayForkChoice::from_bytes(&bytes, original.balances.clone())

0 commit comments

Comments
 (0)