Skip to content

Commit 406e392

Browse files
committed
Use forwards iterator for state root lookups (#2422)
## Issue Addressed #2377 ## Proposed Changes Implement the same code used for block root lookups (from #2376) to state root lookups in order to improve performance and reduce associated memory spikes (e.g. from certain HTTP API requests). ## Additional Changes - Tests using `rev_iter_state_roots` and `rev_iter_block_roots` have been refactored to use their `forwards` versions instead. - The `rev_iter_state_roots` and `rev_iter_block_roots` functions are now unused and have been removed. - The `state_at_slot` function has been changed to use the `forwards` iterator. ## Additional Info - Some tests still need to be refactored to use their `forwards_iter` versions. These tests start their iteration from a specific beacon state and thus use the `rev_iter_state_roots_from` and `rev_iter_block_roots_from` functions. If they can be refactored, those functions can also be removed.
1 parent 73d002e commit 406e392

File tree

6 files changed

+283
-84
lines changed

6 files changed

+283
-84
lines changed

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -390,29 +390,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
390390
.map(|slot| slot.epoch(T::EthSpec::slots_per_epoch()))
391391
}
392392

393-
/// Iterates across all `(block_root, slot)` pairs from the head of the chain (inclusive) to
394-
/// the earliest reachable ancestor (may or may not be genesis).
393+
/// Iterates across all `(block_root, slot)` pairs from `start_slot`
394+
/// to the head of the chain (inclusive).
395395
///
396396
/// ## Notes
397397
///
398-
/// `slot` always decreases by `1`.
398+
/// - `slot` always increases by `1`.
399399
/// - Skipped slots contain the root of the closest prior
400-
/// non-skipped slot (identical to the way they are stored in `state.block_roots`) .
400+
/// non-skipped slot (identical to the way they are stored in `state.block_roots`).
401401
/// - Iterator returns `(Hash256, Slot)`.
402-
/// - As this iterator starts at the `head` of the chain (viz., the best block), the first slot
403-
/// returned may be earlier than the wall-clock slot.
404-
pub fn rev_iter_block_roots(
405-
&self,
406-
) -> Result<impl Iterator<Item = Result<(Hash256, Slot), Error>>, Error> {
407-
let head = self.head()?;
408-
let iter = BlockRootsIterator::owned(self.store.clone(), head.beacon_state);
409-
Ok(
410-
std::iter::once(Ok((head.beacon_block_root, head.beacon_block.slot())))
411-
.chain(iter)
412-
.map(|result| result.map_err(|e| e.into())),
413-
)
414-
}
415-
416402
pub fn forwards_iter_block_roots(
417403
&self,
418404
start_slot: Slot,
@@ -434,7 +420,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
434420
///
435421
/// ## Notes
436422
///
437-
/// `slot` always decreases by `1`.
423+
/// - `slot` always decreases by `1`.
438424
/// - Skipped slots contain the root of the closest prior
439425
/// non-skipped slot (identical to the way they are stored in `state.block_roots`) .
440426
/// - Iterator returns `(Hash256, Slot)`.
@@ -526,29 +512,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
526512
})
527513
}
528514

529-
/// Iterates across all `(state_root, slot)` pairs from the head of the chain (inclusive) to
530-
/// the earliest reachable ancestor (may or may not be genesis).
515+
/// Iterates backwards across all `(state_root, slot)` pairs starting from
516+
/// an arbitrary `BeaconState` to the earliest reachable ancestor (may or may not be genesis).
531517
///
532518
/// ## Notes
533519
///
534-
/// `slot` always decreases by `1`.
520+
/// - `slot` always decreases by `1`.
535521
/// - Iterator returns `(Hash256, Slot)`.
536522
/// - As this iterator starts at the `head` of the chain (viz., the best block), the first slot
537523
/// returned may be earlier than the wall-clock slot.
538-
pub fn rev_iter_state_roots(
539-
&self,
540-
) -> Result<impl Iterator<Item = Result<(Hash256, Slot), Error>>, Error> {
541-
let head = self.head()?;
542-
let head_slot = head.beacon_state.slot;
543-
let head_state_root = head.beacon_state_root();
544-
let iter = StateRootsIterator::owned(self.store.clone(), head.beacon_state);
545-
let iter = std::iter::once(Ok((head_state_root, head_slot)))
546-
.chain(iter)
547-
.map(|result| result.map_err(Into::into));
548-
Ok(iter)
549-
}
550-
551-
/// As for `rev_iter_state_roots` but starting from an arbitrary `BeaconState`.
552524
pub fn rev_iter_state_roots_from<'a>(
553525
&self,
554526
state_root: Hash256,
@@ -559,6 +531,30 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
559531
.map(|result| result.map_err(Into::into))
560532
}
561533

534+
/// Iterates across all `(state_root, slot)` pairs from `start_slot`
535+
/// to the head of the chain (inclusive).
536+
///
537+
/// ## Notes
538+
///
539+
/// - `slot` always increases by `1`.
540+
/// - Iterator returns `(Hash256, Slot)`.
541+
pub fn forwards_iter_state_roots(
542+
&self,
543+
start_slot: Slot,
544+
) -> Result<impl Iterator<Item = Result<(Hash256, Slot), Error>>, Error> {
545+
let local_head = self.head()?;
546+
547+
let iter = HotColdDB::forwards_state_roots_iterator(
548+
self.store.clone(),
549+
start_slot,
550+
local_head.beacon_state_root(),
551+
local_head.beacon_state,
552+
&self.spec,
553+
)?;
554+
555+
Ok(iter.map(|result| result.map_err(Into::into)))
556+
}
557+
562558
/// Returns the block at the given slot, if any. Only returns blocks in the canonical chain.
563559
///
564560
/// Use the `skips` parameter to define the behaviour when `request_slot` is a skipped slot.
@@ -580,16 +576,48 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
580576
}
581577
}
582578

583-
/// Returns the block at the given slot, if any. Only returns blocks in the canonical chain.
579+
/// Returns the state root at the given slot, if any. Only returns state roots in the canonical chain.
584580
///
585581
/// ## Errors
586582
///
587583
/// May return a database error.
588-
pub fn state_root_at_slot(&self, slot: Slot) -> Result<Option<Hash256>, Error> {
589-
process_results(self.rev_iter_state_roots()?, |mut iter| {
590-
iter.find(|(_, this_slot)| *this_slot == slot)
591-
.map(|(root, _)| root)
592-
})
584+
pub fn state_root_at_slot(&self, request_slot: Slot) -> Result<Option<Hash256>, Error> {
585+
if request_slot > self.slot()? {
586+
return Ok(None);
587+
} else if request_slot == self.spec.genesis_slot {
588+
return Ok(Some(self.genesis_state_root));
589+
}
590+
591+
// Try an optimized path of reading the root directly from the head state.
592+
let fast_lookup: Option<Hash256> = self.with_head(|head| {
593+
if head.beacon_block.slot() <= request_slot {
594+
// Return the head state root if all slots between the request and the head are skipped.
595+
Ok(Some(head.beacon_state_root()))
596+
} else if let Ok(root) = head.beacon_state.get_state_root(request_slot) {
597+
// Return the root if it's easily accessible from the head state.
598+
Ok(Some(*root))
599+
} else {
600+
// Fast lookup is not possible.
601+
Ok::<_, Error>(None)
602+
}
603+
})?;
604+
605+
if let Some(root) = fast_lookup {
606+
return Ok(Some(root));
607+
}
608+
609+
process_results(self.forwards_iter_state_roots(request_slot)?, |mut iter| {
610+
if let Some((root, slot)) = iter.next() {
611+
if slot == request_slot {
612+
Ok(Some(root))
613+
} else {
614+
// Sanity check.
615+
Err(Error::InconsistentForwardsIter { request_slot, slot })
616+
}
617+
} else {
618+
Ok(None)
619+
}
620+
})?
593621
}
594622

595623
/// Returns the block root at the given slot, if any. Only returns roots in the canonical chain.
@@ -896,7 +924,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
896924
Ok(state)
897925
}
898926
Ordering::Less => {
899-
let state_root = process_results(self.rev_iter_state_roots()?, |iter| {
927+
let state_root = process_results(self.forwards_iter_state_roots(slot)?, |iter| {
900928
iter.take_while(|(_, current_slot)| *current_slot >= slot)
901929
.find(|(_, current_slot)| *current_slot == slot)
902930
.map(|(root, _slot)| root)

beacon_node/beacon_chain/tests/store_tests.rs

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ fn epoch_boundary_state_attestation_processing() {
309309
let finalized_epoch = harness
310310
.chain
311311
.head_info()
312-
.expect("head ok")
312+
.expect("should get head")
313313
.finalized_checkpoint
314314
.epoch;
315315

@@ -444,8 +444,8 @@ fn delete_blocks_and_states() {
444444
let split_slot = store.get_split_slot();
445445
let finalized_states = harness
446446
.chain
447-
.rev_iter_state_roots()
448-
.expect("rev iter ok")
447+
.forwards_iter_state_roots(Slot::new(0))
448+
.expect("should get iter")
449449
.map(Result::unwrap);
450450

451451
for (state_root, slot) in finalized_states {
@@ -706,7 +706,7 @@ fn check_shuffling_compatible(
706706
{
707707
let (block_root, slot) = maybe_tuple.unwrap();
708708
// Shuffling is compatible targeting the current epoch,
709-
// iff slot is greater than or equal to the current epoch pivot block
709+
// if slot is greater than or equal to the current epoch pivot block.
710710
assert_eq!(
711711
harness.chain.shuffling_is_compatible(
712712
&block_root,
@@ -1671,7 +1671,7 @@ fn pruning_test(
16711671

16721672
let all_canonical_states = harness
16731673
.chain
1674-
.rev_iter_state_roots()
1674+
.forwards_iter_state_roots(Slot::new(0))
16751675
.unwrap()
16761676
.map(Result::unwrap)
16771677
.map(|(state_root, _)| state_root.into())
@@ -1799,17 +1799,12 @@ fn check_chain_dump(harness: &TestHarness, expected_len: u64) {
17991799
.map(|checkpoint| (checkpoint.beacon_block_root, checkpoint.beacon_block.slot()))
18001800
.collect::<Vec<_>>();
18011801

1802-
let head = harness.chain.head().expect("should get head");
1803-
let mut forward_block_roots = HotColdDB::forwards_block_roots_iterator(
1804-
harness.chain.store.clone(),
1805-
Slot::new(0),
1806-
head.beacon_state,
1807-
head.beacon_block_root,
1808-
&harness.spec,
1809-
)
1810-
.unwrap()
1811-
.map(Result::unwrap)
1812-
.collect::<Vec<_>>();
1802+
let mut forward_block_roots = harness
1803+
.chain
1804+
.forwards_iter_block_roots(Slot::new(0))
1805+
.expect("should get iter")
1806+
.map(Result::unwrap)
1807+
.collect::<Vec<_>>();
18131808

18141809
// Drop the block roots for skipped slots.
18151810
forward_block_roots.dedup_by_key(|(block_root, _)| *block_root);
@@ -1827,10 +1822,10 @@ fn check_chain_dump(harness: &TestHarness, expected_len: u64) {
18271822
/// Check that every state from the canonical chain is in the database, and that the
18281823
/// reverse state and block root iterators reach genesis.
18291824
fn check_iterators(harness: &TestHarness) {
1830-
let mut min_slot = None;
1825+
let mut max_slot = None;
18311826
for (state_root, slot) in harness
18321827
.chain
1833-
.rev_iter_state_roots()
1828+
.forwards_iter_state_roots(Slot::new(0))
18341829
.expect("should get iter")
18351830
.map(Result::unwrap)
18361831
{
@@ -1844,20 +1839,23 @@ fn check_iterators(harness: &TestHarness) {
18441839
"state {:?} from canonical chain should be in DB",
18451840
state_root
18461841
);
1847-
min_slot = Some(slot);
1842+
max_slot = Some(slot);
18481843
}
1849-
// Assert that we reached genesis.
1850-
assert_eq!(min_slot, Some(Slot::new(0)));
1851-
// Assert that the block root iterator reaches genesis.
1844+
// Assert that we reached the head.
1845+
assert_eq!(
1846+
max_slot,
1847+
Some(harness.chain.head_info().expect("should get head").slot)
1848+
);
1849+
// Assert that the block root iterator reaches the head.
18521850
assert_eq!(
18531851
harness
18541852
.chain
1855-
.rev_iter_block_roots()
1853+
.forwards_iter_block_roots(Slot::new(0))
18561854
.expect("should get iter")
18571855
.last()
18581856
.map(Result::unwrap)
18591857
.map(|(_, slot)| slot),
1860-
Some(Slot::new(0))
1858+
Some(harness.chain.head_info().expect("should get head").slot)
18611859
);
18621860
}
18631861

beacon_node/beacon_chain/tests/tests.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ fn iterators() {
7777

7878
let block_roots: Vec<(Hash256, Slot)> = harness
7979
.chain
80-
.rev_iter_block_roots()
80+
.forwards_iter_block_roots(Slot::new(0))
8181
.expect("should get iter")
8282
.map(Result::unwrap)
8383
.collect();
8484
let state_roots: Vec<(Hash256, Slot)> = harness
8585
.chain
86-
.rev_iter_state_roots()
86+
.forwards_iter_state_roots(Slot::new(0))
8787
.expect("should get iter")
8888
.map(Result::unwrap)
8989
.collect();
@@ -112,30 +112,30 @@ fn iterators() {
112112
block_roots.windows(2).for_each(|x| {
113113
assert_eq!(
114114
x[1].1,
115-
x[0].1 - 1,
116-
"block root slots should be decreasing by one"
115+
x[0].1 + 1,
116+
"block root slots should be increasing by one"
117117
)
118118
});
119119
state_roots.windows(2).for_each(|x| {
120120
assert_eq!(
121121
x[1].1,
122-
x[0].1 - 1,
123-
"state root slots should be decreasing by one"
122+
x[0].1 + 1,
123+
"state root slots should be increasing by one"
124124
)
125125
});
126126

127127
let head = &harness.chain.head().expect("should get head");
128128

129129
assert_eq!(
130-
*block_roots.first().expect("should have some block roots"),
130+
*block_roots.last().expect("should have some block roots"),
131131
(head.beacon_block_root, head.beacon_block.slot()),
132-
"first block root and slot should be for the head block"
132+
"last block root and slot should be for the head block"
133133
);
134134

135135
assert_eq!(
136-
*state_roots.first().expect("should have some state roots"),
136+
*state_roots.last().expect("should have some state roots"),
137137
(head.beacon_state_root(), head.beacon_state.slot),
138-
"first state root and slot should be for the head state"
138+
"last state root and slot should be for the head state"
139139
);
140140
}
141141

beacon_node/http_api/tests/tests.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -861,13 +861,10 @@ impl ApiTester {
861861
pub async fn test_beacon_headers_all_parents(self) -> Self {
862862
let mut roots = self
863863
.chain
864-
.rev_iter_block_roots()
864+
.forwards_iter_block_roots(Slot::new(0))
865865
.unwrap()
866866
.map(Result::unwrap)
867867
.map(|(root, _slot)| root)
868-
.collect::<Vec<_>>()
869-
.into_iter()
870-
.rev()
871868
.collect::<Vec<_>>();
872869

873870
// The iterator natively returns duplicate roots for skipped slots.

0 commit comments

Comments
 (0)