Skip to content

Commit 96a9aa6

Browse files
feat(chain): refactor merge_chains
`merge_chains` now returns a tuple of the resultant checkpoint AND changeset. This is arguably a more readable/understandable setup. To do this, we had to create `CheckPoint::apply_changeset` which is kept as a private method. Thank you @ValuedMammal for the suggestion. Co-authored-by: valuedvalued mammal <[email protected]>
1 parent 2f22987 commit 96a9aa6

File tree

1 file changed

+74
-61
lines changed

1 file changed

+74
-61
lines changed

crates/chain/src/local_chain.rs

Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,46 @@ impl CheckPoint {
213213
base.extend(core::iter::once(block_id).chain(tail.into_iter().rev()))
214214
.expect("tail is in order")
215215
}
216+
217+
/// Apply `changeset` to the checkpoint.
218+
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
219+
if let Some(start_height) = changeset.keys().next().cloned() {
220+
// changes after point of agreement
221+
let mut extension = BTreeMap::default();
222+
// point of agreement
223+
let mut base: Option<CheckPoint> = None;
224+
225+
for cp in self.iter() {
226+
if cp.height() >= start_height {
227+
extension.insert(cp.height(), cp.hash());
228+
} else {
229+
base = Some(cp);
230+
break;
231+
}
232+
}
233+
234+
for (&height, &hash) in changeset {
235+
match hash {
236+
Some(hash) => {
237+
extension.insert(height, hash);
238+
}
239+
None => {
240+
extension.remove(&height);
241+
}
242+
};
243+
}
244+
245+
let new_tip = match base {
246+
Some(base) => base
247+
.extend(extension.into_iter().map(BlockId::from))
248+
.expect("extension is strictly greater than base"),
249+
None => LocalChain::from_blocks(extension)?.tip(),
250+
};
251+
self = new_tip;
252+
}
253+
254+
Ok(self)
255+
}
216256
}
217257

218258
/// Iterates over checkpoints backwards.
@@ -348,32 +388,22 @@ impl LocalChain {
348388

349389
/// Applies the given `update` to the chain.
350390
///
351-
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
391+
/// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`.
352392
///
353393
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
354394
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
355395
/// the existing chain and invalidate the block after it (if it exists) by including a block at
356396
/// the same height but with a different hash to explicitly exclude it as a connection point.
357397
///
358-
/// Additionally, a chain with a single block can have it's block invalidated by an update
359-
/// chain with a block at the same height but different hash.
360-
///
361398
/// # Errors
362399
///
363400
/// An error will occur if the update does not correctly connect with `self`.
364401
///
365402
/// [module-level documentation]: crate::local_chain
366403
pub fn apply_update(&mut self, update: CheckPoint) -> Result<ChangeSet, CannotConnectError> {
367-
let (changeset, can_replace) = merge_chains(self.tip.clone(), update.clone())?;
368-
if can_replace {
369-
self.tip = update;
370-
} else {
371-
// `._check_changeset_is_applied` is called in `.apply_changeset`
372-
self.apply_changeset(&changeset)
373-
.map_err(|_| CannotConnectError {
374-
try_include_height: 0,
375-
})?;
376-
}
404+
let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?;
405+
self.tip = new_tip;
406+
self._check_changeset_is_applied(&changeset);
377407
Ok(changeset)
378408
}
379409

@@ -465,43 +495,10 @@ impl LocalChain {
465495

466496
/// Apply the given `changeset`.
467497
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
468-
if let Some(start_height) = changeset.keys().next().cloned() {
469-
// changes after point of agreement
470-
let mut extension = BTreeMap::default();
471-
// point of agreement
472-
let mut base: Option<CheckPoint> = None;
473-
474-
for cp in self.iter_checkpoints() {
475-
if cp.height() >= start_height {
476-
extension.insert(cp.height(), cp.hash());
477-
} else {
478-
base = Some(cp);
479-
break;
480-
}
481-
}
482-
483-
for (&height, &hash) in changeset {
484-
match hash {
485-
Some(hash) => {
486-
extension.insert(height, hash);
487-
}
488-
None => {
489-
extension.remove(&height);
490-
}
491-
};
492-
}
493-
494-
let new_tip = match base {
495-
Some(base) => base
496-
.extend(extension.into_iter().map(BlockId::from))
497-
.expect("extension is strictly greater than base"),
498-
None => LocalChain::from_blocks(extension)?.tip(),
499-
};
500-
self.tip = new_tip;
501-
502-
debug_assert!(self._check_changeset_is_applied(changeset));
503-
}
504-
498+
let old_tip = self.tip.clone();
499+
let new_tip = old_tip.apply_changeset(changeset)?;
500+
self.tip = new_tip;
501+
debug_assert!(self._check_changeset_is_applied(changeset));
505502
Ok(())
506503
}
507504

@@ -731,10 +728,10 @@ impl std::error::Error for ApplyHeaderError {}
731728
fn merge_chains(
732729
original_tip: CheckPoint,
733730
update_tip: CheckPoint,
734-
) -> Result<(ChangeSet, bool), CannotConnectError> {
731+
) -> Result<(CheckPoint, ChangeSet), CannotConnectError> {
735732
let mut changeset = ChangeSet::default();
736-
let mut orig = original_tip.into_iter();
737-
let mut update = update_tip.into_iter();
733+
let mut orig = original_tip.iter();
734+
let mut update = update_tip.iter();
738735
let mut curr_orig = None;
739736
let mut curr_update = None;
740737
let mut prev_orig: Option<CheckPoint> = None;
@@ -743,10 +740,11 @@ fn merge_chains(
743740
let mut prev_orig_was_invalidated = false;
744741
let mut potentially_invalidated_heights = vec![];
745742

746-
// Flag to set if heights are removed from original chain. If no heights are removed, and we
747-
// have a matching node pointer between the two chains, we can conclude that the update tip can
748-
// just replace the original tip.
749-
let mut has_removed_heights = false;
743+
// If we can, we want to return the update tip as the new tip because this allows checkpoints
744+
// in multiple locations to keep the same `Arc` pointers when they are being updated from each
745+
// other using this function. We can do this as long as long as the update contains every
746+
// block's height of the original chain.
747+
let mut is_update_height_superset_of_original = true;
750748

751749
// To find the difference between the new chain and the original we iterate over both of them
752750
// from the tip backwards in tandem. We always dealing with the highest one from either chain
@@ -773,7 +771,7 @@ fn merge_chains(
773771
prev_orig_was_invalidated = false;
774772
prev_orig = curr_orig.take();
775773

776-
has_removed_heights = true;
774+
is_update_height_superset_of_original = false;
777775

778776
// OPTIMIZATION: we have run out of update blocks so we don't need to continue
779777
// iterating because there's no possibility of adding anything to changeset.
@@ -800,7 +798,17 @@ fn merge_chains(
800798
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
801799
// can guarantee that no older blocks are introduced.
802800
if Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
803-
return Ok((changeset, !has_removed_heights));
801+
if is_update_height_superset_of_original {
802+
return Ok((update_tip, changeset));
803+
} else {
804+
let new_tip =
805+
original_tip.apply_changeset(&changeset).map_err(|_| {
806+
CannotConnectError {
807+
try_include_height: 0,
808+
}
809+
})?;
810+
return Ok((new_tip, changeset));
811+
}
804812
}
805813
} else {
806814
// We have an invalidation height so we set the height to the updated hash and
@@ -834,5 +842,10 @@ fn merge_chains(
834842
}
835843
}
836844

837-
Ok((changeset, false))
845+
let new_tip = original_tip
846+
.apply_changeset(&changeset)
847+
.map_err(|_| CannotConnectError {
848+
try_include_height: 0,
849+
})?;
850+
Ok((new_tip, changeset))
838851
}

0 commit comments

Comments
 (0)