diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index df5a5e9c..6c1c2c3a 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -11,6 +11,7 @@ pub enum ObservedIn { /// The chain data is seen in a block identified by `A`. Block(A), /// The chain data is seen in mempool at this given timestamp. + /// TODO: Call this `Unconfirmed`. Mempool(u64), } diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index b2b206cf..21852150 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -68,7 +68,7 @@ impl TxIndexAdditions for IndexedAdditions< pub struct IndexedTxGraph { graph: TxGraph, - index: I, + index: I, // [TODO] Make public last_height: u32, } @@ -219,6 +219,7 @@ impl IndexedTxGraph { self.last_height } + // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. pub fn try_list_chain_txs<'a, C>( &'a self, chain: C, diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 52844097..9319d4ac 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -26,6 +26,7 @@ mod chain_data; pub use chain_data::*; pub mod indexed_tx_graph; pub mod keychain; +pub mod local_chain; pub mod sparse_chain; mod tx_data_traits; pub mod tx_graph; diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs new file mode 100644 index 00000000..5bcb524f --- /dev/null +++ b/crates/chain/src/local_chain.rs @@ -0,0 +1,140 @@ +use core::convert::Infallible; + +use alloc::{collections::BTreeMap, vec::Vec}; +use bitcoin::BlockHash; + +use crate::{BlockId, ChainOracle}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct LocalChain { + blocks: BTreeMap, +} + +// [TODO] We need a cache/snapshot thing for chain oracle. +// * Minimize calls to remotes. +// * Can we cache it forever? Should we drop stuff? +// * Assume anything deeper than (i.e. 10) blocks won't be reorged. +// * Is this a cache on txs or block? or both? +// [TODO] Parents of children are confirmed if children are confirmed. +impl ChainOracle for LocalChain { + type Error = Infallible; + + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { + Ok(self.blocks.get(&height).cloned()) + } +} + +impl AsRef> for LocalChain { + fn as_ref(&self) -> &BTreeMap { + &self.blocks + } +} + +impl From for BTreeMap { + fn from(value: LocalChain) -> Self { + value.blocks + } +} + +impl LocalChain { + pub fn tip(&self) -> Option { + 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: &U) -> Result + where + U: AsRef>, + { + let update = update.as_ref(); + let update_tip = match update.keys().last().cloned() { + Some(tip) => tip, + None => return Ok(ChangeSet::default()), + }; + + // 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); + + // 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, + }; + + // the first block's height to invalidate in the local chain + let invalidate_from = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h); + + // the first block of height to invalidate (if any) should be represented in the update + if let Some(first_invalid) = invalidate_from { + if !update.contains_key(&first_invalid) { + return Err(UpdateError::NotConnected(first_invalid)); + } + } + + let invalidated_heights = invalidate_from + .into_iter() + .flat_map(|from_height| self.blocks.range(from_height..).map(|(h, _)| h)); + + // invalidated heights must all exist in the update + let mut missing_heights = Vec::::new(); + for invalidated_height in invalidated_heights { + if !update.contains_key(invalidated_height) { + missing_heights.push(*invalidated_height); + } + } + if !missing_heights.is_empty() { + return Err(UpdateError::MissingHeightsInUpdate(missing_heights)); + } + + let mut changeset = BTreeMap::::new(); + for (height, new_hash) in update { + let original_hash = self.blocks.get(height); + if Some(new_hash) != original_hash { + changeset.insert(*height, *new_hash); + } + } + Ok(ChangeSet(changeset)) + } +} + +#[derive(Debug, Default)] +pub struct ChangeSet(pub BTreeMap); + +/// Represents an update failure of [`LocalChain`].j +#[derive(Clone, Debug, PartialEq)] +pub enum UpdateError { + /// 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. + NotConnected(u32), + /// If the update results in displacements of original blocks, the update should include all new + /// block hashes that have displaced the original block hashes. This error case contains the + /// heights of all missing block hashes in the update. + MissingHeightsInUpdate(Vec), +} + +impl core::fmt::Display for UpdateError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + UpdateError::NotConnected(heights) => write!( + f, + "the update cannot connect with the chain, try include blockhash at height {}", + heights + ), + UpdateError::MissingHeightsInUpdate(missing_heights) => write!( + f, + "block hashes of these heights must be included in the update to succeed: {:?}", + missing_heights + ), + } + } +} diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 2ffb9a60..485e3f70 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -58,6 +58,10 @@ impl BlockAnchor for (u32, BlockHash) { } /// Represents a service that tracks the best chain history. +/// TODO: How do we ensure the chain oracle is consistent across a single call? +/// * We need to somehow lock the data! What if the ChainOracle is remote? +/// * Get tip method! And check the tip still exists at the end! And every internal call +/// does not go beyond the initial tip. pub trait ChainOracle { /// Error type. type Error: core::fmt::Debug; @@ -71,6 +75,8 @@ pub trait ChainOracle { } } +// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this? +// Box, Arc ????? I will figure it out impl ChainOracle for &C { type Error = C::Error;