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>
This commit is contained in:
Eitan Seri-Levi
2026-04-23 20:32:26 +09:00
committed by GitHub
parent cfc748309f
commit 82dc8b4edc
9 changed files with 533 additions and 36 deletions

View File

@@ -4,6 +4,7 @@ mod gloas_payload;
mod no_votes;
mod votes;
use crate::error::Error;
use crate::proto_array_fork_choice::{Block, ExecutionStatus, PayloadStatus, ProtoArrayForkChoice};
use crate::{InvalidationOperation, JustifiedBalances};
use fixed_bytes::FixedBytesExtended;
@@ -30,6 +31,8 @@ pub enum Operation {
justified_state_balances: Vec<u64>,
expected_head: Hash256,
current_slot: Slot,
// TODO(gloas): Make this non-optional. `find_head` always returns a `PayloadStatus`
// (Empty for pre-GLOAS), so every test should assert on it explicitly.
#[serde(default)]
expected_payload_status: Option<PayloadStatus>,
},
@@ -61,6 +64,12 @@ pub enum Operation {
block_root: Hash256,
attestation_slot: Slot,
},
ProcessGloasAttestation {
validator_index: usize,
block_root: Hash256,
attestation_slot: Slot,
payload_present: bool,
},
ProcessPayloadAttestation {
validator_index: usize,
block_root: Hash256,
@@ -105,6 +114,16 @@ pub enum Operation {
block_root: Hash256,
expected: bool,
},
AssertPayloadStatusByWeight {
block_root: Hash256,
expected_status: PayloadStatus,
/// Override `current_slot`. Defaults to the `current_slot` of the last `FindHead`.
#[serde(default)]
current_slot: Option<Slot>,
/// Override the proposer boost root. Defaults to `Hash256::zero()`.
#[serde(default)]
proposer_boost_root: Option<Hash256>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -149,6 +168,7 @@ impl ForkChoiceTestDefinition {
)
.expect("should create fork choice struct");
let equivocating_indices = BTreeSet::new();
let mut last_current_slot = Slot::new(0);
for (op_index, op) in self.operations.into_iter().enumerate() {
match op.clone() {
@@ -189,6 +209,16 @@ impl ForkChoiceTestDefinition {
op_index, op
);
}
assert_canonical_payload_status_matches_find_head(
&fork_choice,
&head,
current_slot,
Hash256::zero(),
&spec,
payload_status,
op_index,
);
last_current_slot = current_slot;
check_bytes_round_trip(&fork_choice);
}
Operation::ProposerBoostFindHead {
@@ -201,7 +231,7 @@ impl ForkChoiceTestDefinition {
let justified_balances =
JustifiedBalances::from_effective_balances(justified_state_balances)
.unwrap();
let (head, _payload_status) = fork_choice
let (head, payload_status) = fork_choice
.find_head::<MainnetEthSpec>(
justified_checkpoint,
finalized_checkpoint,
@@ -220,6 +250,15 @@ impl ForkChoiceTestDefinition {
"Operation at index {} failed head check. Operation: {:?}",
op_index, op
);
assert_canonical_payload_status_matches_find_head(
&fork_choice,
&head,
Slot::new(0),
proposer_boost_root,
&spec,
payload_status,
op_index,
);
check_bytes_round_trip(&fork_choice);
}
Operation::InvalidFindHead {
@@ -308,6 +347,27 @@ impl ForkChoiceTestDefinition {
});
check_bytes_round_trip(&fork_choice);
}
Operation::ProcessGloasAttestation {
validator_index,
block_root,
attestation_slot,
payload_present,
} => {
fork_choice
.process_attestation(
validator_index,
block_root,
attestation_slot,
payload_present,
)
.unwrap_or_else(|_| {
panic!(
"process_attestation op at index {} returned error",
op_index
)
});
check_bytes_round_trip(&fork_choice);
}
Operation::ProcessPayloadAttestation {
validator_index,
block_root,
@@ -522,6 +582,26 @@ impl ForkChoiceTestDefinition {
op_index
);
}
Operation::AssertPayloadStatusByWeight {
block_root,
expected_status,
current_slot,
proposer_boost_root,
} => {
let actual = fork_choice
.get_canonical_payload_status::<MainnetEthSpec>(
&block_root,
current_slot.unwrap_or(last_current_slot),
proposer_boost_root.unwrap_or_else(Hash256::zero),
&spec,
)
.unwrap();
assert_eq!(
actual, expected_status,
"canonical payload status mismatch at op index {}",
op_index
);
}
}
}
}
@@ -546,6 +626,37 @@ fn get_checkpoint(i: u64) -> Checkpoint {
}
}
/// Checks that `get_canonical_payload_status` agrees with the `payload_status`
/// returned by `find_head` for the head block.
fn assert_canonical_payload_status_matches_find_head(
fork_choice: &ProtoArrayForkChoice,
head: &Hash256,
current_slot: Slot,
proposer_boost_root: Hash256,
spec: &ChainSpec,
expected: PayloadStatus,
op_index: usize,
) {
match fork_choice.get_canonical_payload_status::<MainnetEthSpec>(
head,
current_slot,
proposer_boost_root,
spec,
) {
Ok(actual) => assert_eq!(
actual, expected,
"get_canonical_payload_status disagreed with find_head for head {:?} at op index {}",
head, op_index
),
// Skip the check for pre-gloas nodes
Err(Error::InvalidNodeVariant { .. }) => {}
Err(e) => panic!(
"get_canonical_payload_status failed at op index {}: {:?}",
op_index, e
),
}
}
fn check_bytes_round_trip(original: &ProtoArrayForkChoice) {
let bytes = original.as_bytes();
let decoded = ProtoArrayForkChoice::from_bytes(&bytes, original.balances.clone())