Implement linked-list LocalChain and update chain-src crates/examples
This commit changes the `LocalChain` implementation to have blocks stored as a linked-list. This allows the data-src thread to hold a shared ref to a single checkpoint and have access to the whole history of checkpoints without cloning or keeping a lock on `LocalChain`. The APIs of `bdk::Wallet`, `esplora` and `electrum` are also updated to reflect these changes. Note that the `esplora` crate is rewritten to anchor txs in the confirmation block (using the esplora API's tx status block_hash). This guarantees 100% consistency between anchor blocks and their transactions (instead of anchoring txs to the latest tip). `ExploraExt` now has separate methods for updating the `TxGraph` and `LocalChain`. A new method `TxGraph::missing_blocks` is introduced for finding "floating anchors" of a `TxGraph` update (given a chain). Additional changes: * `test_local_chain.rs` is refactored to make test cases easier to write. Additional tests are also added. * Examples are updated. * Fix `tempfile` dev dependency of `bdk_file_store` to work with MSRV Co-authored-by: LLFourn <lloyd.fourn@gmail.com>
This commit is contained in:
@@ -11,10 +11,7 @@
|
||||
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
|
||||
|
||||
use crate::{
|
||||
collections::BTreeMap,
|
||||
indexed_tx_graph::IndexedAdditions,
|
||||
local_chain::{self, LocalChain},
|
||||
tx_graph::TxGraph,
|
||||
collections::BTreeMap, indexed_tx_graph::IndexedAdditions, local_chain, tx_graph::TxGraph,
|
||||
Anchor, Append,
|
||||
};
|
||||
|
||||
@@ -89,24 +86,32 @@ impl<K> AsRef<BTreeMap<K, u32>> for DerivationAdditions<K> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure to update [`KeychainTxOutIndex`], [`TxGraph`] and [`LocalChain`]
|
||||
/// atomically.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// A structure to update [`KeychainTxOutIndex`], [`TxGraph`] and [`LocalChain`] atomically.
|
||||
///
|
||||
/// [`LocalChain`]: local_chain::LocalChain
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalUpdate<K, A> {
|
||||
/// Last active derivation index per keychain (`K`).
|
||||
pub keychain: BTreeMap<K, u32>,
|
||||
|
||||
/// Update for the [`TxGraph`].
|
||||
pub graph: TxGraph<A>,
|
||||
|
||||
/// Update for the [`LocalChain`].
|
||||
pub chain: LocalChain,
|
||||
///
|
||||
/// [`LocalChain`]: local_chain::LocalChain
|
||||
pub chain: local_chain::Update,
|
||||
}
|
||||
|
||||
impl<K, A> Default for LocalUpdate<K, A> {
|
||||
fn default() -> Self {
|
||||
impl<K, A> LocalUpdate<K, A> {
|
||||
/// Construct a [`LocalUpdate`] with a given [`CheckPoint`] tip.
|
||||
///
|
||||
/// [`CheckPoint`]: local_chain::CheckPoint
|
||||
pub fn new(chain_update: local_chain::Update) -> Self {
|
||||
Self {
|
||||
keychain: Default::default(),
|
||||
graph: Default::default(),
|
||||
chain: Default::default(),
|
||||
keychain: BTreeMap::new(),
|
||||
graph: TxGraph::default(),
|
||||
chain: chain_update,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +131,8 @@ impl<K, A> Default for LocalUpdate<K, A> {
|
||||
)]
|
||||
pub struct LocalChangeSet<K, A> {
|
||||
/// Changes to the [`LocalChain`].
|
||||
///
|
||||
/// [`LocalChain`]: local_chain::LocalChain
|
||||
pub chain_changeset: local_chain::ChangeSet,
|
||||
|
||||
/// Additions to [`IndexedTxGraph`].
|
||||
|
||||
@@ -2,15 +2,160 @@
|
||||
|
||||
use core::convert::Infallible;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::{BlockId, ChainOracle};
|
||||
use alloc::sync::Arc;
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
use crate::{BlockId, ChainOracle};
|
||||
/// A structure that represents changes to [`LocalChain`].
|
||||
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||
|
||||
/// A blockchain of [`LocalChain`].
|
||||
///
|
||||
/// The in a linked-list with newer blocks pointing to older ones.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CheckPoint(Arc<CPInner>);
|
||||
|
||||
/// The internal contents of [`CheckPoint`].
|
||||
#[derive(Debug, Clone)]
|
||||
struct CPInner {
|
||||
/// Block id (hash and height).
|
||||
block: BlockId,
|
||||
/// Previous checkpoint (if any).
|
||||
prev: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl CheckPoint {
|
||||
/// Construct a new base block at the front of a linked list.
|
||||
pub fn new(block: BlockId) -> Self {
|
||||
Self(Arc::new(CPInner { block, prev: None }))
|
||||
}
|
||||
|
||||
/// Puts another checkpoint onto the linked list representing the blockchain.
|
||||
///
|
||||
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
|
||||
/// are pushing on to.
|
||||
pub fn push(self, block: BlockId) -> Result<Self, Self> {
|
||||
if self.height() < block.height {
|
||||
Ok(Self(Arc::new(CPInner {
|
||||
block,
|
||||
prev: Some(self.0),
|
||||
})))
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the checkpoint linked list by a iterator of block ids.
|
||||
///
|
||||
/// Returns an `Err(self)` if there is block which does not have a greater height than the
|
||||
/// previous one.
|
||||
pub fn extend_with_blocks(
|
||||
self,
|
||||
blocks: impl IntoIterator<Item = BlockId>,
|
||||
) -> Result<Self, Self> {
|
||||
let mut curr = self.clone();
|
||||
for block in blocks {
|
||||
curr = curr.push(block).map_err(|_| self.clone())?;
|
||||
}
|
||||
Ok(curr)
|
||||
}
|
||||
|
||||
/// Get the [`BlockId`] of the checkpoint.
|
||||
pub fn block_id(&self) -> BlockId {
|
||||
self.0.block
|
||||
}
|
||||
|
||||
/// Get the height of the checkpoint.
|
||||
pub fn height(&self) -> u32 {
|
||||
self.0.block.height
|
||||
}
|
||||
|
||||
/// Get the block hash of the checkpoint.
|
||||
pub fn hash(&self) -> BlockHash {
|
||||
self.0.block.hash
|
||||
}
|
||||
|
||||
/// Get the previous checkpoint in the chain
|
||||
pub fn prev(&self) -> Option<CheckPoint> {
|
||||
self.0.prev.clone().map(CheckPoint)
|
||||
}
|
||||
|
||||
/// Iterate from this checkpoint in descending height.
|
||||
pub fn iter(&self) -> CheckPointIter {
|
||||
self.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure that iterates over checkpoints backwards.
|
||||
pub struct CheckPointIter {
|
||||
current: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl Iterator for CheckPointIter {
|
||||
type Item = CheckPoint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current = self.current.clone()?;
|
||||
self.current = current.prev.clone();
|
||||
Some(CheckPoint(current))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for CheckPoint {
|
||||
type Item = CheckPoint;
|
||||
type IntoIter = CheckPointIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
CheckPointIter {
|
||||
current: Some(self.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an update to [`LocalChain`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Update {
|
||||
/// The update's new [`CheckPoint`] tip.
|
||||
pub tip: CheckPoint,
|
||||
|
||||
/// Whether the update allows for introducing older blocks.
|
||||
///
|
||||
/// Refer to [`LocalChain::apply_update`] for more.
|
||||
///
|
||||
/// [`LocalChain::apply_update`]: crate::local_chain::LocalChain::apply_update
|
||||
pub introduce_older_blocks: bool,
|
||||
}
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LocalChain {
|
||||
blocks: BTreeMap<u32, BlockHash>,
|
||||
tip: Option<CheckPoint>,
|
||||
index: BTreeMap<u32, BlockHash>,
|
||||
}
|
||||
|
||||
impl PartialEq for LocalChain {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
fn from(value: LocalChain) -> Self {
|
||||
value.index
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangeSet> for LocalChain {
|
||||
fn from(value: ChangeSet) -> Self {
|
||||
Self::from_changeset(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
|
||||
Self::from_blocks(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
@@ -19,215 +164,271 @@ impl ChainOracle for LocalChain {
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
static_block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error> {
|
||||
if block.height > static_block.height {
|
||||
if block.height > chain_tip.height {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(
|
||||
match (
|
||||
self.blocks.get(&block.height),
|
||||
self.blocks.get(&static_block.height),
|
||||
self.index.get(&block.height),
|
||||
self.index.get(&chain_tip.height),
|
||||
) {
|
||||
(Some(&hash), Some(&static_hash)) => {
|
||||
Some(hash == block.hash && static_hash == static_block.hash)
|
||||
}
|
||||
(Some(cp), Some(tip_cp)) => Some(*cp == block.hash && *tip_cp == chain_tip.hash),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> {
|
||||
Ok(self.tip())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn as_ref(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.blocks
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
fn from(value: LocalChain) -> Self {
|
||||
value.blocks
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
|
||||
Self { blocks: value }
|
||||
Ok(self.tip.as_ref().map(|tip| tip.block_id()))
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalChain {
|
||||
/// Contruct a [`LocalChain`] from a list of [`BlockId`]s.
|
||||
pub fn from_blocks<B>(blocks: B) -> Self
|
||||
where
|
||||
B: IntoIterator<Item = BlockId>,
|
||||
{
|
||||
Self {
|
||||
blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(),
|
||||
}
|
||||
/// Construct a [`LocalChain`] from an initial `changeset`.
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Self {
|
||||
let mut chain = Self::default();
|
||||
chain.apply_changeset(&changeset);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
chain._check_consistency(Some(&changeset));
|
||||
|
||||
chain
|
||||
}
|
||||
|
||||
/// Get a reference to a map of block height to hash.
|
||||
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.blocks
|
||||
}
|
||||
|
||||
/// Get the chain tip.
|
||||
pub fn tip(&self) -> Option<BlockId> {
|
||||
self.blocks
|
||||
.iter()
|
||||
.last()
|
||||
.map(|(&height, &hash)| BlockId { height, hash })
|
||||
}
|
||||
|
||||
/// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights
|
||||
/// are to be re-filled.
|
||||
pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet, UpdateNotConnectedError> {
|
||||
let update = update.as_ref();
|
||||
let update_tip = match update.keys().last().cloned() {
|
||||
Some(tip) => tip,
|
||||
None => return Ok(ChangeSet::default()),
|
||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||
pub fn from_tip(tip: CheckPoint) -> Self {
|
||||
let mut _self = Self {
|
||||
tip: Some(tip),
|
||||
..Default::default()
|
||||
};
|
||||
_self.reindex(0);
|
||||
|
||||
// this is the latest height where both the update and local chain has the same block hash
|
||||
let agreement_height = update
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|&(u_height, u_hash)| self.blocks.get(u_height) == Some(u_hash))
|
||||
.map(|(&height, _)| height);
|
||||
#[cfg(debug_assertions)]
|
||||
_self._check_consistency(None);
|
||||
|
||||
// the lower bound of the range to invalidate
|
||||
let invalidate_lb = match agreement_height {
|
||||
Some(height) if height == update_tip => u32::MAX,
|
||||
Some(height) => height + 1,
|
||||
None => 0,
|
||||
};
|
||||
_self
|
||||
}
|
||||
|
||||
// the first block's height to invalidate in the local chain
|
||||
let invalidate_from_height = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h);
|
||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||
///
|
||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||
/// all of the same chain.
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Self {
|
||||
let mut tip: Option<CheckPoint> = None;
|
||||
|
||||
// the first block of height to invalidate (if any) should be represented in the update
|
||||
if let Some(first_invalid_height) = invalidate_from_height {
|
||||
if !update.contains_key(&first_invalid_height) {
|
||||
return Err(UpdateNotConnectedError(first_invalid_height));
|
||||
}
|
||||
}
|
||||
|
||||
let mut changeset: BTreeMap<u32, Option<BlockHash>> = match invalidate_from_height {
|
||||
Some(first_invalid_height) => {
|
||||
// the first block of height to invalidate should be represented in the update
|
||||
if !update.contains_key(&first_invalid_height) {
|
||||
return Err(UpdateNotConnectedError(first_invalid_height));
|
||||
for block in &blocks {
|
||||
match tip {
|
||||
Some(curr) => {
|
||||
tip = Some(
|
||||
curr.push(BlockId::from(block))
|
||||
.expect("BTreeMap is ordered"),
|
||||
)
|
||||
}
|
||||
self.blocks
|
||||
.range(first_invalid_height..)
|
||||
.map(|(height, _)| (*height, None))
|
||||
.collect()
|
||||
}
|
||||
None => BTreeMap::new(),
|
||||
};
|
||||
for (height, update_hash) in update {
|
||||
let original_hash = self.blocks.get(height);
|
||||
if Some(update_hash) != original_hash {
|
||||
changeset.insert(*height, Some(*update_hash));
|
||||
None => tip = Some(CheckPoint::new(BlockId::from(block))),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
let chain = Self { index: blocks, tip };
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
chain._check_consistency(None);
|
||||
|
||||
chain
|
||||
}
|
||||
|
||||
/// Applies the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
|
||||
for (height, blockhash) in changeset {
|
||||
match blockhash {
|
||||
Some(blockhash) => self.blocks.insert(height, blockhash),
|
||||
None => self.blocks.remove(&height),
|
||||
/// Get the highest checkpoint.
|
||||
pub fn tip(&self) -> Option<CheckPoint> {
|
||||
self.tip.clone()
|
||||
}
|
||||
|
||||
/// Returns whether the [`LocalChain`] is empty (has no checkpoints).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tip.is_none()
|
||||
}
|
||||
|
||||
/// Updates [`Self`] with the given `update_tip`.
|
||||
///
|
||||
/// `introduce_older_blocks` specifies whether the `update_tip`'s history can introduce blocks
|
||||
/// below the original chain's tip without invalidating blocks. Block-by-block syncing
|
||||
/// mechanisms would typically create updates that builds upon the previous tip. In this case,
|
||||
/// this paramater would be false. Script-pubkey based syncing mechanisms may not introduce
|
||||
/// transactions in a chronological order so some updates require introducing older blocks (to
|
||||
/// anchor older transactions). For script-pubkey based syncing, this parameter would typically
|
||||
/// be true.
|
||||
///
|
||||
/// The method returns [`ChangeSet`] on success. This represents the applied changes to
|
||||
/// [`Self`].
|
||||
///
|
||||
/// To update, the `update_tip` must *connect* with `self`. If `self` and `update_tip` has a
|
||||
/// mutual checkpoint (same height and hash), it can connect if:
|
||||
/// * The mutual checkpoint is the tip of `self`.
|
||||
/// * An ancestor of `update_tip` has a height which is of the checkpoint one higher than the
|
||||
/// mutual checkpoint from `self`.
|
||||
///
|
||||
/// Additionally:
|
||||
/// * If `self` is empty, `update_tip` will always connect.
|
||||
/// * If `self` only has one checkpoint, `update_tip` must have an ancestor checkpoint with the
|
||||
/// same height as it.
|
||||
///
|
||||
/// To invalidate from a given checkpoint, `update_tip` must contain an ancestor checkpoint with
|
||||
/// the same height but different hash.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error will occur if the update does not correctly connect with `self`.
|
||||
///
|
||||
/// Refer to [module-level documentation] for more.
|
||||
///
|
||||
/// [module-level documentation]: crate::local_chain
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
|
||||
match self.tip() {
|
||||
Some(original_tip) => {
|
||||
let changeset = merge_chains(
|
||||
original_tip,
|
||||
update.tip.clone(),
|
||||
update.introduce_older_blocks,
|
||||
)?;
|
||||
self.apply_changeset(&changeset);
|
||||
|
||||
// return early as `apply_changeset` already calls `check_consistency`
|
||||
Ok(changeset)
|
||||
}
|
||||
None => {
|
||||
*self = Self::from_tip(update.tip);
|
||||
let changeset = self.initial_changeset();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
self._check_consistency(Some(&changeset));
|
||||
Ok(changeset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
let mut extension = BTreeMap::default();
|
||||
let mut base: Option<CheckPoint> = None;
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() >= start_height {
|
||||
extension.insert(cp.height(), cp.hash());
|
||||
} else {
|
||||
base = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (&height, &hash) in changeset {
|
||||
match hash {
|
||||
Some(hash) => {
|
||||
extension.insert(height, hash);
|
||||
}
|
||||
None => {
|
||||
extension.remove(&height);
|
||||
}
|
||||
};
|
||||
}
|
||||
let new_tip = match base {
|
||||
Some(base) => Some(
|
||||
base.extend_with_blocks(extension.into_iter().map(BlockId::from))
|
||||
.expect("extension is strictly greater than base"),
|
||||
),
|
||||
None => LocalChain::from_blocks(extension).tip(),
|
||||
};
|
||||
self.tip = new_tip;
|
||||
self.reindex(start_height);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
self._check_consistency(Some(changeset));
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates [`LocalChain`] with an update [`LocalChain`].
|
||||
/// Insert a [`BlockId`].
|
||||
///
|
||||
/// This is equivalent to calling [`determine_changeset`] and [`apply_changeset`] in sequence.
|
||||
/// # Errors
|
||||
///
|
||||
/// [`determine_changeset`]: Self::determine_changeset
|
||||
/// [`apply_changeset`]: Self::apply_changeset
|
||||
pub fn apply_update(&mut self, update: Self) -> Result<ChangeSet, UpdateNotConnectedError> {
|
||||
let changeset = self.determine_changeset(&update)?;
|
||||
self.apply_changeset(changeset.clone());
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Derives a [`ChangeSet`] that assumes that there are no preceding changesets.
|
||||
///
|
||||
/// The changeset returned will record additions of all blocks included in [`Self`].
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.blocks
|
||||
.iter()
|
||||
.map(|(&height, &hash)| (height, Some(hash)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Insert a block of [`BlockId`] into the [`LocalChain`].
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
/// If the insertion height already contains a block, and the block has a different blockhash,
|
||||
/// this will result in an [`InsertBlockNotMatchingError`].
|
||||
pub fn insert_block(
|
||||
&mut self,
|
||||
block_id: BlockId,
|
||||
) -> Result<ChangeSet, InsertBlockNotMatchingError> {
|
||||
let mut update = Self::from_blocks(self.tip());
|
||||
|
||||
if let Some(original_hash) = update.blocks.insert(block_id.height, block_id.hash) {
|
||||
/// Replacing the block hash of an existing checkpoint will result in an error.
|
||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, InsertBlockError> {
|
||||
if let Some(&original_hash) = self.index.get(&block_id.height) {
|
||||
if original_hash != block_id.hash {
|
||||
return Err(InsertBlockNotMatchingError {
|
||||
return Err(InsertBlockError {
|
||||
height: block_id.height,
|
||||
original_hash,
|
||||
update_hash: block_id.hash,
|
||||
});
|
||||
} else {
|
||||
return Ok(ChangeSet::default());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.apply_update(update).expect("should always connect"))
|
||||
let mut changeset = ChangeSet::default();
|
||||
changeset.insert(block_id.height, Some(block_id.hash));
|
||||
self.apply_changeset(&changeset);
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Reindex the heights in the chain from (and including) `from` height
|
||||
fn reindex(&mut self, from: u32) {
|
||||
let _ = self.index.split_off(&from);
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() < from {
|
||||
break;
|
||||
}
|
||||
self.index.insert(cp.height(), cp.hash());
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
||||
/// recover the current chain.
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.index.iter().map(|(k, v)| (*k, Some(*v))).collect()
|
||||
}
|
||||
|
||||
/// Iterate over checkpoints in decending height order.
|
||||
pub fn iter_checkpoints(&self) -> CheckPointIter {
|
||||
CheckPointIter {
|
||||
current: self.tip.as_ref().map(|tip| tip.0.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the internal index mapping the height to block hash
|
||||
pub fn heights(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.index
|
||||
}
|
||||
|
||||
/// Checkpoints that exist under `self.tip` and blocks indexed in `self.index` should be equal.
|
||||
/// Additionally, if a `changeset` is provided, the changes specified in the `changeset` should
|
||||
/// be reflected in `self.index`.
|
||||
#[cfg(debug_assertions)]
|
||||
fn _check_consistency(&self, changeset: Option<&ChangeSet>) {
|
||||
debug_assert_eq!(
|
||||
self.tip
|
||||
.iter()
|
||||
.flat_map(CheckPoint::iter)
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
self.index,
|
||||
"checkpoint history and index must be consistent"
|
||||
);
|
||||
|
||||
if let Some(changeset) = changeset {
|
||||
for (height, exp_hash) in changeset {
|
||||
let hash = self.index.get(height);
|
||||
assert_eq!(
|
||||
hash,
|
||||
exp_hash.as_ref(),
|
||||
"changeset changes should be reflected in the internal index"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
|
||||
///
|
||||
/// [`determine_changeset`]: LocalChain::determine_changeset
|
||||
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||
|
||||
/// Represents an update failure of [`LocalChain`] due to the update not connecting to the original
|
||||
/// chain.
|
||||
///
|
||||
/// The update cannot be applied to the chain because the chain suffix it represents did not
|
||||
/// connect to the existing chain. This error case contains the checkpoint height to include so
|
||||
/// that the chains can connect.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UpdateNotConnectedError(pub u32);
|
||||
|
||||
impl core::fmt::Display for UpdateNotConnectedError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"the update cannot connect with the chain, try include block at height {}",
|
||||
self.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for UpdateNotConnectedError {}
|
||||
|
||||
/// Represents a failure when trying to insert a checkpoint into [`LocalChain`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct InsertBlockNotMatchingError {
|
||||
pub struct InsertBlockError {
|
||||
/// The checkpoints' height.
|
||||
pub height: u32,
|
||||
/// Original checkpoint's block hash.
|
||||
@@ -236,7 +437,7 @@ pub struct InsertBlockNotMatchingError {
|
||||
pub update_hash: BlockHash,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for InsertBlockNotMatchingError {
|
||||
impl core::fmt::Display for InsertBlockError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
@@ -247,4 +448,129 @@ impl core::fmt::Display for InsertBlockNotMatchingError {
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InsertBlockNotMatchingError {}
|
||||
impl std::error::Error for InsertBlockError {}
|
||||
|
||||
/// Occurs when an update does not have a common checkpoint with the original chain.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CannotConnectError {
|
||||
/// The suggested checkpoint to include to connect the two chains.
|
||||
pub try_include_height: u32,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CannotConnectError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"introduced chain cannot connect with the original chain, try include height {}",
|
||||
self.try_include_height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for CannotConnectError {}
|
||||
|
||||
fn merge_chains(
|
||||
original_tip: CheckPoint,
|
||||
update_tip: CheckPoint,
|
||||
introduce_older_blocks: bool,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
let mut changeset = ChangeSet::default();
|
||||
let mut orig = original_tip.into_iter();
|
||||
let mut update = update_tip.into_iter();
|
||||
let mut curr_orig = None;
|
||||
let mut curr_update = None;
|
||||
let mut prev_orig: Option<CheckPoint> = None;
|
||||
let mut prev_update: Option<CheckPoint> = None;
|
||||
let mut point_of_agreement_found = false;
|
||||
let mut prev_orig_was_invalidated = false;
|
||||
let mut potentially_invalidated_heights = vec![];
|
||||
|
||||
// To find the difference between the new chain and the original we iterate over both of them
|
||||
// from the tip backwards in tandem. We always dealing with the highest one from either chain
|
||||
// first and move to the next highest. The crucial logic is applied when they have blocks at the
|
||||
// same height.
|
||||
loop {
|
||||
if curr_orig.is_none() {
|
||||
curr_orig = orig.next();
|
||||
}
|
||||
if curr_update.is_none() {
|
||||
curr_update = update.next();
|
||||
}
|
||||
|
||||
match (curr_orig.as_ref(), curr_update.as_ref()) {
|
||||
// Update block that doesn't exist in the original chain
|
||||
(o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
prev_update = curr_update.take();
|
||||
}
|
||||
// Original block that isn't in the update
|
||||
(Some(o), u) if Some(o.height()) > u.map(|u| u.height()) => {
|
||||
// this block might be gone if an earlier block gets invalidated
|
||||
potentially_invalidated_heights.push(o.height());
|
||||
prev_orig_was_invalidated = false;
|
||||
prev_orig = curr_orig.take();
|
||||
|
||||
// OPTIMIZATION: we have run out of update blocks so we don't need to continue
|
||||
// iterating becuase there's no possibility of adding anything to changeset.
|
||||
if u.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(o), Some(u)) => {
|
||||
if o.hash() == u.hash() {
|
||||
// We have found our point of agreement 🎉 -- we require that the previous (i.e.
|
||||
// higher because we are iterating backwards) block in the original chain was
|
||||
// invalidated (if it exists). This ensures that there is an unambigious point of
|
||||
// connection to the original chain from the update chain (i.e. we know the
|
||||
// precisely which original blocks are invalid).
|
||||
if !prev_orig_was_invalidated && !point_of_agreement_found {
|
||||
if let (Some(prev_orig), Some(_prev_update)) = (&prev_orig, &prev_update) {
|
||||
return Err(CannotConnectError {
|
||||
try_include_height: prev_orig.height(),
|
||||
});
|
||||
}
|
||||
}
|
||||
point_of_agreement_found = true;
|
||||
prev_orig_was_invalidated = false;
|
||||
// OPTIMIZATION 1 -- If we know that older blocks cannot be introduced without
|
||||
// invalidation, we can break after finding the point of agreement.
|
||||
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
|
||||
// can guarantee that no older blocks are introduced.
|
||||
if !introduce_older_blocks || Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
|
||||
return Ok(changeset);
|
||||
}
|
||||
} else {
|
||||
// We have an invalidation height so we set the height to the updated hash and
|
||||
// also purge all the original chain block hashes above this block.
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
for invalidated_height in potentially_invalidated_heights.drain(..) {
|
||||
changeset.insert(invalidated_height, None);
|
||||
}
|
||||
prev_orig_was_invalidated = true;
|
||||
}
|
||||
prev_update = curr_update.take();
|
||||
prev_orig = curr_orig.take();
|
||||
}
|
||||
(None, None) => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
unreachable!("compiler cannot tell that everything has been covered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When we don't have a point of agreement you can imagine it is implicitly the
|
||||
// genesis block so we need to do the final connectivity check which in this case
|
||||
// just means making sure the entire original chain was invalidated.
|
||||
if !prev_orig_was_invalidated && !point_of_agreement_found {
|
||||
if let Some(prev_orig) = prev_orig {
|
||||
return Err(CannotConnectError {
|
||||
try_include_height: prev_orig.height(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
|
||||
ForEachTxOut, FullTxOut,
|
||||
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
|
||||
ChainOracle, ChainPosition, ForEachTxOut, FullTxOut,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||
@@ -598,6 +598,31 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
}
|
||||
|
||||
impl<A: Anchor> TxGraph<A> {
|
||||
/// Find missing block heights of `chain`.
|
||||
///
|
||||
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
|
||||
/// exists in the [`LocalChain`].
|
||||
pub fn missing_blocks<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
|
||||
self.anchors
|
||||
.iter()
|
||||
.map(|(a, _)| a.anchor_block())
|
||||
.filter({
|
||||
let mut last_block = Option::<BlockId>::None;
|
||||
move |block| {
|
||||
if last_block.as_ref() == Some(block) {
|
||||
false
|
||||
} else {
|
||||
last_block = Some(*block);
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter_map(|block| match chain.heights().get(&block.height) {
|
||||
Some(chain_hash) if *chain_hash == block.hash => None,
|
||||
_ => Some(block.height),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
||||
///
|
||||
/// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is
|
||||
|
||||
Reference in New Issue
Block a user