Skip to content

Commit eded1a7

Browse files
evanlinjinLLFourn
andcommitted
feat(chain): introduce CheckPoint::insert
Co-authored-by: LLFourn <[email protected]>
1 parent 519cd75 commit eded1a7

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

crates/chain/src/local_chain.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,43 @@ impl CheckPoint {
187187
core::ops::Bound::Unbounded => true,
188188
})
189189
}
190+
191+
/// Inserts `block_id` at its height within the chain.
192+
///
193+
/// The effect of `insert` depends on whether a height already exists. If it doesn't the
194+
/// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after
195+
/// it. If the height already existed and has a conflicting block hash then it will be purged
196+
/// along with all block followin it. The returned chain will have a tip of the `block_id`
197+
/// passed in. Of course, if the `block_id` was already present then this just returns `self`.
198+
#[must_use]
199+
pub fn insert(self, block_id: BlockId) -> Self {
200+
assert_ne!(block_id.height, 0, "cannot insert the genesis block");
201+
202+
let mut cp = self.clone();
203+
let mut tail = vec![];
204+
let base = loop {
205+
if cp.height() == block_id.height {
206+
if cp.hash() == block_id.hash {
207+
return self;
208+
}
209+
// if we have a conflict we just return the inserted block because the tail is by
210+
// implication invalid.
211+
tail = vec![];
212+
break cp.prev().expect("can't be called on genesis block");
213+
}
214+
215+
if cp.height() < block_id.height {
216+
break cp;
217+
}
218+
219+
tail.push(cp.block_id());
220+
cp = cp.prev().expect("will break before genesis block");
221+
};
222+
223+
base
224+
.extend(core::iter::once(block_id).chain(tail.into_iter().rev()))
225+
.expect("tail is in order")
226+
}
190227
}
191228

192229
/// Iterates over checkpoints backwards.

crates/chain/tests/test_local_chain.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,77 @@ fn checkpoint_query() {
577577
}
578578
}
579579

580+
#[test]
581+
fn checkpoint_insert() {
582+
struct TestCase<'a> {
583+
/// The name of the test.
584+
name: &'a str,
585+
/// The original checkpoint chain to call [`CheckPoint::insert`] on.
586+
chain: &'a [(u32, BlockHash)],
587+
/// The `block_id` to insert.
588+
to_insert: (u32, BlockHash),
589+
/// The expected final checkpoint chain after calling [`CheckPoint::insert`].
590+
exp_final_chain: &'a [(u32, BlockHash)],
591+
}
592+
593+
let test_cases = [
594+
TestCase {
595+
name: "insert_above_tip",
596+
chain: &[(1, h!("a")), (2, h!("b"))],
597+
to_insert: (4, h!("d")),
598+
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (4, h!("d"))],
599+
},
600+
TestCase {
601+
name: "insert_already_exists_expect_no_change",
602+
chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
603+
to_insert: (2, h!("b")),
604+
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
605+
},
606+
TestCase {
607+
name: "insert_in_middle",
608+
chain: &[(2, h!("b")), (4, h!("d")), (5, h!("e"))],
609+
to_insert: (3, h!("c")),
610+
exp_final_chain: &[(2, h!("b")), (3, h!("c")), (4, h!("d")), (5, h!("e"))],
611+
},
612+
TestCase {
613+
name: "replace_one",
614+
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e"))],
615+
to_insert: (5, h!("E")),
616+
exp_final_chain: &[(3, h!("c")), (4, h!("d")), (5, h!("E"))],
617+
},
618+
TestCase {
619+
name: "insert_conflict_should_evict",
620+
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e")), (6, h!("f"))],
621+
to_insert: (4, h!("D")),
622+
exp_final_chain: &[(3, h!("c")), (4, h!("D"))],
623+
},
624+
];
625+
626+
fn genesis_block() -> impl Iterator<Item = BlockId> {
627+
core::iter::once((0, h!("_"))).map(BlockId::from)
628+
}
629+
630+
for (i, t) in test_cases.into_iter().enumerate() {
631+
println!("Running [{}] '{}'", i, t.name);
632+
633+
let chain = CheckPoint::from_block_ids(
634+
genesis_block().chain(t.chain.iter().copied().map(BlockId::from)),
635+
)
636+
.expect("test formed incorrectly, must construct checkpoint chain");
637+
638+
let exp_final_chain = CheckPoint::from_block_ids(
639+
genesis_block().chain(t.exp_final_chain.iter().copied().map(BlockId::from)),
640+
)
641+
.expect("test formed incorrectly, must construct checkpoint chain");
642+
643+
assert_eq!(
644+
chain.insert(t.to_insert.into()),
645+
exp_final_chain,
646+
"unexpected final chain"
647+
);
648+
}
649+
}
650+
580651
#[test]
581652
fn local_chain_apply_header_connected_to() {
582653
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {

0 commit comments

Comments
 (0)