Skip to content

Commit 772e129

Browse files
michaelsproulWoodpile37
authored andcommitted
Use peeking_take_while in BlockReplayer (sigp#4803)
## Issue Addressed While reviewing sigp#4801 I noticed that our use of `take_while` in the block replayer means that if a state root iterator _with gaps_ is provided, some additonal state roots will be dropped unnecessarily. In practice the impact is small, because once there's _one_ state root miss, the whole tree hash cache needs to be built anyway, and subsequent misses are less costly. However this was still a little inefficient, so I figured it's better to fix it. ## Proposed Changes Use [`peeking_take_while`](https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.peeking_take_while) to avoid consuming the next element when checking whether it satisfies the slot predicate. ## Additional Info There's a gist here that shows the basic dynamics in isolation: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=40b623cc0febf9ed51705d476ab140c5. Changing the `peeking_take_while` to a `take_while` causes the assert to fail. Similarly I've added a new test `block_replayer_peeking_state_roots` which fails if the same change is applied inside `get_state_root`.
1 parent b4b512e commit 772e129

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

consensus/state_processing/src/block_replayer.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use crate::{
33
BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError,
44
VerifyBlockRoot,
55
};
6+
use itertools::Itertools;
7+
use std::iter::Peekable;
68
use std::marker::PhantomData;
79
use types::{BeaconState, BlindedPayload, ChainSpec, EthSpec, Hash256, SignedBeaconBlock, Slot};
810

@@ -25,7 +27,7 @@ pub struct BlockReplayer<
2527
'a,
2628
Spec: EthSpec,
2729
Error = BlockReplayError,
28-
StateRootIter = StateRootIterDefault<Error>,
30+
StateRootIter: Iterator<Item = Result<(Hash256, Slot), Error>> = StateRootIterDefault<Error>,
2931
> {
3032
state: BeaconState<Spec>,
3133
spec: &'a ChainSpec,
@@ -36,7 +38,7 @@ pub struct BlockReplayer<
3638
post_block_hook: Option<PostBlockHook<'a, Spec, Error>>,
3739
pre_slot_hook: Option<PreSlotHook<'a, Spec, Error>>,
3840
post_slot_hook: Option<PostSlotHook<'a, Spec, Error>>,
39-
state_root_iter: Option<StateRootIter>,
41+
pub(crate) state_root_iter: Option<Peekable<StateRootIter>>,
4042
state_root_miss: bool,
4143
_phantom: PhantomData<Error>,
4244
}
@@ -138,7 +140,7 @@ where
138140
/// `self.state.slot` to the `target_slot` supplied to `apply_blocks` (inclusive of both
139141
/// endpoints).
140142
pub fn state_root_iter(mut self, iter: StateRootIter) -> Self {
141-
self.state_root_iter = Some(iter);
143+
self.state_root_iter = Some(iter.peekable());
142144
self
143145
}
144146

@@ -192,7 +194,7 @@ where
192194
// If a state root iterator is configured, use it to find the root.
193195
if let Some(ref mut state_root_iter) = self.state_root_iter {
194196
let opt_root = state_root_iter
195-
.take_while(|res| res.as_ref().map_or(true, |(_, s)| *s <= slot))
197+
.peeking_take_while(|res| res.as_ref().map_or(true, |(_, s)| *s <= slot))
196198
.find(|res| res.as_ref().map_or(true, |(_, s)| *s == slot))
197199
.transpose()?;
198200

consensus/state_processing/src/per_block_processing/tests.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::per_block_processing::errors::{
55
DepositInvalid, HeaderInvalid, IndexedAttestationInvalid, IntoWithIndex,
66
ProposerSlashingInvalid,
77
};
8-
use crate::{per_block_processing, StateProcessingStrategy};
8+
use crate::{per_block_processing, BlockReplayError, BlockReplayer, StateProcessingStrategy};
99
use crate::{
1010
per_block_processing::{process_operations, verify_exit::verify_exit},
1111
BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures,
@@ -1035,3 +1035,51 @@ async fn fork_spanning_exit() {
10351035
)
10361036
.expect_err("phase0 exit does not verify against bellatrix state");
10371037
}
1038+
1039+
/// Check that the block replayer does not consume state roots unnecessarily.
1040+
#[tokio::test]
1041+
async fn block_replayer_peeking_state_roots() {
1042+
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
1043+
1044+
let target_state = harness.get_current_state();
1045+
let target_block_root = harness.head_block_root();
1046+
let target_block = harness
1047+
.chain
1048+
.get_blinded_block(&target_block_root)
1049+
.unwrap()
1050+
.unwrap();
1051+
1052+
let parent_block_root = target_block.parent_root();
1053+
let parent_block = harness
1054+
.chain
1055+
.get_blinded_block(&parent_block_root)
1056+
.unwrap()
1057+
.unwrap();
1058+
let parent_state = harness
1059+
.chain
1060+
.get_state(&parent_block.state_root(), Some(parent_block.slot()))
1061+
.unwrap()
1062+
.unwrap();
1063+
1064+
// Omit the state root for `target_state` but provide a dummy state root at the *next* slot.
1065+
// If the block replayer is peeking at the state roots rather than consuming them, then the
1066+
// dummy state should still be there after block replay completes.
1067+
let dummy_state_root = Hash256::repeat_byte(0xff);
1068+
let dummy_slot = target_state.slot() + 1;
1069+
let state_root_iter = vec![Ok::<_, BlockReplayError>((dummy_state_root, dummy_slot))];
1070+
let block_replayer = BlockReplayer::new(parent_state, &harness.chain.spec)
1071+
.state_root_iter(state_root_iter.into_iter())
1072+
.no_signature_verification()
1073+
.apply_blocks(vec![target_block], None)
1074+
.unwrap();
1075+
1076+
assert_eq!(
1077+
block_replayer
1078+
.state_root_iter
1079+
.unwrap()
1080+
.next()
1081+
.unwrap()
1082+
.unwrap(),
1083+
(dummy_state_root, dummy_slot)
1084+
);
1085+
}

0 commit comments

Comments
 (0)