]> Untitled Git - bdk/commitdiff
feat!: `LocalChain` with hardwired genesis checkpoint
author志宇 <hello@evanlinjin.me>
Thu, 12 Oct 2023 08:55:32 +0000 (16:55 +0800)
committer志宇 <hello@evanlinjin.me>
Wed, 15 Nov 2023 22:41:18 +0000 (06:41 +0800)
This ensures that `LocalChain` will always have a tip. The `ChainOracle`
trait's `get_chain_tip` method no longer needs to return an option.

19 files changed:
crates/bdk/src/error.rs
crates/bdk/src/wallet/mod.rs
crates/bdk/tests/wallet.rs
crates/bitcoind_rpc/src/lib.rs
crates/bitcoind_rpc/tests/test_emitter.rs
crates/chain/src/chain_oracle.rs
crates/chain/src/local_chain.rs
crates/chain/tests/common/mod.rs
crates/chain/tests/test_indexed_tx_graph.rs
crates/chain/tests/test_local_chain.rs
crates/chain/tests/test_tx_graph.rs
crates/chain/tests/test_tx_graph_conflicts.rs
crates/electrum/src/electrum_ext.rs
crates/esplora/src/async_ext.rs
crates/esplora/src/blocking_ext.rs
example-crates/example_bitcoind_rpc_polling/src/main.rs
example-crates/example_cli/src/lib.rs
example-crates/example_electrum/src/main.rs
example-crates/example_esplora/src/main.rs

index fcb5a6f7b17ba159e15cedb46c3ae38e9aee18f8..3bd3dc6d32169a77c73368687be7bf78a3b9d4fe 100644 (file)
@@ -199,3 +199,12 @@ impl_error!(miniscript::Error, Miniscript);
 impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
 impl_error!(bitcoin::bip32::Error, Bip32);
 impl_error!(bitcoin::psbt::Error, Psbt);
+
+impl From<crate::wallet::NewNoPersistError> for Error {
+    fn from(e: crate::wallet::NewNoPersistError) -> Self {
+        match e {
+            wallet::NewNoPersistError::Descriptor(e) => Error::Descriptor(e),
+            unknown_network_err => Error::Generic(format!("{}", unknown_network_err)),
+        }
+    }
+}
index bc5093ce3b4b1b77535ead5c35ea13762f1c29ef..275487a581ff513cd4f96cccaead4bfb806bb7b0 100644 (file)
@@ -28,14 +28,14 @@ use bdk_chain::{
     Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
     IndexedTxGraph, Persist, PersistBackend,
 };
-use bitcoin::consensus::encode::serialize;
-use bitcoin::psbt;
 use bitcoin::secp256k1::Secp256k1;
 use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
 use bitcoin::{
     absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid,
     Weight, Witness,
 };
+use bitcoin::{consensus::encode::serialize, BlockHash};
+use bitcoin::{constants::genesis_block, psbt};
 use core::fmt;
 use core::ops::Deref;
 use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
@@ -225,26 +225,57 @@ impl Wallet {
         descriptor: E,
         change_descriptor: Option<E>,
         network: Network,
-    ) -> Result<Self, crate::descriptor::DescriptorError> {
+    ) -> Result<Self, NewNoPersistError> {
         Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
-            NewError::Descriptor(e) => e,
-            NewError::Persist(_) => unreachable!("no persistence so it can't fail"),
+            NewError::Descriptor(e) => NewNoPersistError::Descriptor(e),
+            NewError::Persist(_) | NewError::InvalidPersistenceGenesis => {
+                unreachable!("no persistence so it can't fail")
+            }
+            NewError::UnknownNetwork => NewNoPersistError::UnknownNetwork,
         })
     }
 }
 
+/// Error returned from [`Wallet::new_no_persist`]
+#[derive(Debug)]
+pub enum NewNoPersistError {
+    /// There was problem with the descriptors passed in
+    Descriptor(crate::descriptor::DescriptorError),
+    /// We cannot determine the genesis hash from the network.
+    UnknownNetwork,
+}
+
+impl fmt::Display for NewNoPersistError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            NewNoPersistError::Descriptor(e) => e.fmt(f),
+            NewNoPersistError::UnknownNetwork => write!(
+                f,
+                "unknown network - genesis block hash needs to be provided explicitly"
+            ),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for NewNoPersistError {}
+
 #[derive(Debug)]
 /// Error returned from [`Wallet::new`]
-pub enum NewError<P> {
+pub enum NewError<PE> {
     /// There was problem with the descriptors passed in
     Descriptor(crate::descriptor::DescriptorError),
     /// We were unable to load the wallet's data from the persistence backend
-    Persist(P),
+    Persist(PE),
+    /// We cannot determine the genesis hash from the network
+    UnknownNetwork,
+    /// The genesis block hash is either missing from persistence or has an unexpected value
+    InvalidPersistenceGenesis,
 }
 
-impl<P> fmt::Display for NewError<P>
+impl<PE> fmt::Display for NewError<PE>
 where
-    P: fmt::Display,
+    PE: fmt::Display,
 {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
@@ -252,10 +283,18 @@ where
             NewError::Persist(e) => {
                 write!(f, "failed to load wallet from persistence backend: {}", e)
             }
+            NewError::UnknownNetwork => write!(
+                f,
+                "unknown network - genesis block hash needs to be provided explicitly"
+            ),
+            NewError::InvalidPersistenceGenesis => write!(f, "the genesis block hash is either missing from persistence or has an unexpected value"),
         }
     }
 }
 
+#[cfg(feature = "std")]
+impl<PE> std::error::Error for NewError<PE> where PE: core::fmt::Display + core::fmt::Debug {}
+
 /// An error that may occur when inserting a transaction into [`Wallet`].
 #[derive(Debug)]
 pub enum InsertTxError {
@@ -263,29 +302,44 @@ pub enum InsertTxError {
     /// confirmation height that is greater than the internal chain tip.
     ConfirmationHeightCannotBeGreaterThanTip {
         /// The internal chain's tip height.
-        tip_height: Option<u32>,
+        tip_height: u32,
         /// The introduced transaction's confirmation height.
         tx_height: u32,
     },
 }
 
-#[cfg(feature = "std")]
-impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for NewError<P> {}
-
 impl<D> Wallet<D> {
     /// Create a wallet from a `descriptor` (and an optional `change_descriptor`) and load related
     /// transaction data from `db`.
     pub fn new<E: IntoWalletDescriptor>(
+        descriptor: E,
+        change_descriptor: Option<E>,
+        db: D,
+        network: Network,
+    ) -> Result<Self, NewError<D::LoadError>>
+    where
+        D: PersistBackend<ChangeSet>,
+    {
+        Self::with_custom_genesis_hash(descriptor, change_descriptor, db, network, None)
+    }
+
+    /// Create a new [`Wallet`] with a custom genesis hash.
+    ///
+    /// This is like [`Wallet::new`] with an additional `custom_genesis_hash` parameter.
+    pub fn with_custom_genesis_hash<E: IntoWalletDescriptor>(
         descriptor: E,
         change_descriptor: Option<E>,
         mut db: D,
         network: Network,
+        custom_genesis_hash: Option<BlockHash>,
     ) -> Result<Self, NewError<D::LoadError>>
     where
         D: PersistBackend<ChangeSet>,
     {
         let secp = Secp256k1::new();
-        let mut chain = LocalChain::default();
+        let genesis_hash =
+            custom_genesis_hash.unwrap_or_else(|| genesis_block(network).block_hash());
+        let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
         let mut indexed_graph = IndexedTxGraph::<
             ConfirmationTimeHeightAnchor,
             KeychainTxOutIndex<KeychainKind>,
@@ -319,7 +373,9 @@ impl<D> Wallet<D> {
         };
 
         let changeset = db.load_from_persistence().map_err(NewError::Persist)?;
-        chain.apply_changeset(&changeset.chain);
+        chain
+            .apply_changeset(&changeset.chain)
+            .map_err(|_| NewError::InvalidPersistenceGenesis)?;
         indexed_graph.apply_changeset(changeset.indexed_tx_graph);
 
         let persist = Persist::new(db);
@@ -446,7 +502,7 @@ impl<D> Wallet<D> {
             .graph()
             .filter_chain_unspents(
                 &self.chain,
-                self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
+                self.chain.tip().block_id(),
                 self.indexed_graph.index.outpoints().iter().cloned(),
             )
             .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
@@ -458,7 +514,7 @@ impl<D> Wallet<D> {
     }
 
     /// Returns the latest checkpoint.
-    pub fn latest_checkpoint(&self) -> Option<CheckPoint> {
+    pub fn latest_checkpoint(&self) -> CheckPoint {
         self.chain.tip()
     }
 
@@ -496,7 +552,7 @@ impl<D> Wallet<D> {
             .graph()
             .filter_chain_unspents(
                 &self.chain,
-                self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
+                self.chain.tip().block_id(),
                 core::iter::once((spk_i, op)),
             )
             .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
@@ -669,7 +725,7 @@ impl<D> Wallet<D> {
         Some(CanonicalTx {
             chain_position: graph.get_chain_position(
                 &self.chain,
-                self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
+                self.chain.tip().block_id(),
                 txid,
             )?,
             tx_node: graph.get_tx_node(txid)?,
@@ -686,7 +742,7 @@ impl<D> Wallet<D> {
     pub fn insert_checkpoint(
         &mut self,
         block_id: BlockId,
-    ) -> Result<bool, local_chain::InsertBlockError>
+    ) -> Result<bool, local_chain::AlterCheckPointError>
     where
         D: PersistBackend<ChangeSet>,
     {
@@ -730,7 +786,7 @@ impl<D> Wallet<D> {
                     .range(height..)
                     .next()
                     .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
-                        tip_height: self.chain.tip().map(|b| b.height()),
+                        tip_height: self.chain.tip().height(),
                         tx_height: height,
                     })
                     .map(|(&anchor_height, &hash)| ConfirmationTimeHeightAnchor {
@@ -766,10 +822,9 @@ impl<D> Wallet<D> {
     pub fn transactions(
         &self,
     ) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> + '_ {
-        self.indexed_graph.graph().list_chain_txs(
-            &self.chain,
-            self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
-        )
+        self.indexed_graph
+            .graph()
+            .list_chain_txs(&self.chain, self.chain.tip().block_id())
     }
 
     /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
@@ -777,7 +832,7 @@ impl<D> Wallet<D> {
     pub fn get_balance(&self) -> Balance {
         self.indexed_graph.graph().balance(
             &self.chain,
-            self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
+            self.chain.tip().block_id(),
             self.indexed_graph.index.outpoints().iter().cloned(),
             |&(k, _), _| k == KeychainKind::Internal,
         )
@@ -945,14 +1000,14 @@ impl<D> Wallet<D> {
             _ => 1,
         };
 
-        // We use a match here instead of a map_or_else as it's way more readable :)
+        // We use a match here instead of a unwrap_or_else as it's way more readable :)
         let current_height = match params.current_height {
             // If they didn't tell us the current height, we assume it's the latest sync height.
-            None => self
-                .chain
-                .tip()
-                .map(|cp| absolute::LockTime::from_height(cp.height()).expect("Invalid height")),
-            h => h,
+            None => {
+                let tip_height = self.chain.tip().height();
+                absolute::LockTime::from_height(tip_height).expect("invalid height")
+            }
+            Some(h) => h,
         };
 
         let lock_time = match params.locktime {
@@ -961,7 +1016,7 @@ impl<D> Wallet<D> {
                 // Fee sniping can be partially prevented by setting the timelock
                 // to current_height. If we don't know the current_height,
                 // we default to 0.
-                let fee_sniping_height = current_height.unwrap_or(absolute::LockTime::ZERO);
+                let fee_sniping_height = current_height;
 
                 // We choose the biggest between the required nlocktime and the fee sniping
                 // height
@@ -1115,7 +1170,7 @@ impl<D> Wallet<D> {
             params.drain_wallet,
             params.manually_selected_only,
             params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
-            current_height.map(absolute::LockTime::to_consensus_u32),
+            Some(current_height.to_consensus_u32()),
         );
 
         // get drain script
@@ -1257,7 +1312,7 @@ impl<D> Wallet<D> {
     ) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
         let graph = self.indexed_graph.graph();
         let txout_index = &self.indexed_graph.index;
-        let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
+        let chain_tip = self.chain.tip().block_id();
 
         let mut tx = graph
             .get_tx(txid)
@@ -1492,7 +1547,7 @@ impl<D> Wallet<D> {
         psbt: &mut psbt::PartiallySignedTransaction,
         sign_options: SignOptions,
     ) -> Result<bool, Error> {
-        let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
+        let chain_tip = self.chain.tip().block_id();
 
         let tx = &psbt.unsigned_tx;
         let mut finished = true;
@@ -1515,7 +1570,7 @@ impl<D> Wallet<D> {
                 });
             let current_height = sign_options
                 .assume_height
-                .or(self.chain.tip().map(|b| b.height()));
+                .unwrap_or_else(|| self.chain.tip().height());
 
             debug!(
                 "Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
@@ -1552,8 +1607,8 @@ impl<D> Wallet<D> {
                         &mut tmp_input,
                         (
                             PsbtInputSatisfier::new(psbt, n),
-                            After::new(current_height, false),
-                            Older::new(current_height, confirmation_height, false),
+                            After::new(Some(current_height), false),
+                            Older::new(Some(current_height), confirmation_height, false),
                         ),
                     ) {
                         Ok(_) => {
@@ -1661,7 +1716,7 @@ impl<D> Wallet<D> {
         must_only_use_confirmed_tx: bool,
         current_height: Option<u32>,
     ) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
-        let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
+        let chain_tip = self.chain.tip().block_id();
         //    must_spend <- manually selected utxos
         //    may_spend  <- all other available utxos
         let mut may_spend = self.get_available_utxos();
index aad8c2db25412758e46e8448f1031131e21acf12..3aab7016ded2a5c5decba9e1d93361c1eb3d02f4 100644 (file)
@@ -42,14 +42,14 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->
 }
 
 fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
-    let height = match wallet.latest_checkpoint() {
-        Some(cp) => ConfirmationTime::Confirmed {
-            height: cp.height(),
-            time: 0,
-        },
-        None => ConfirmationTime::Unconfirmed { last_seen: 0 },
+    let latest_cp = wallet.latest_checkpoint();
+    let height = latest_cp.height();
+    let anchor = if height == 0 {
+        ConfirmationTime::Unconfirmed { last_seen: 0 }
+    } else {
+        ConfirmationTime::Confirmed { height, time: 0 }
     };
-    receive_output(wallet, value, height)
+    receive_output(wallet, value, anchor)
 }
 
 // The satisfaction size of a P2WPKH is 112 WU =
@@ -277,7 +277,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() {
     // If there's no current_height we're left with using the last sync height
     assert_eq!(
         psbt.unsigned_tx.lock_time.to_consensus_u32(),
-        wallet.latest_checkpoint().unwrap().height()
+        wallet.latest_checkpoint().height()
     );
 }
 
@@ -1615,7 +1615,7 @@ fn test_bump_fee_drain_wallet() {
         .insert_tx(
             tx.clone(),
             ConfirmationTime::Confirmed {
-                height: wallet.latest_checkpoint().unwrap().height(),
+                height: wallet.latest_checkpoint().height(),
                 time: 42_000,
             },
         )
index a5016ce6fb92702c32659ca7dc50d2f8c3b4d8b7..07f6ea3d4c98dc7d8f0f29a428566f502385765c 100644 (file)
@@ -25,7 +25,7 @@ pub struct Emitter<'c, C> {
 
     /// The checkpoint of the last-emitted block that is in the best chain. If it is later found
     /// that the block is no longer in the best chain, it will be popped off from here.
-    last_cp: Option<CheckPoint>,
+    last_cp: CheckPoint,
 
     /// The block result returned from rpc of the last-emitted block. As this result contains the
     /// next block's block hash (which we use to fetch the next block), we set this to `None`
@@ -43,29 +43,12 @@ pub struct Emitter<'c, C> {
 }
 
 impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
-    /// Construct a new [`Emitter`] with the given RPC `client` and `start_height`.
-    ///
-    /// `start_height` is the block height to start emitting blocks from.
-    pub fn from_height(client: &'c C, start_height: u32) -> Self {
+    /// TODO
+    pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
         Self {
             client,
             start_height,
-            last_cp: None,
-            last_block: None,
-            last_mempool_time: 0,
-            last_mempool_tip: None,
-        }
-    }
-
-    /// Construct a new [`Emitter`] with the given RPC `client` and `checkpoint`.
-    ///
-    /// `checkpoint` is used to find the latest block which is still part of the best chain. The
-    /// [`Emitter`] will emit blocks starting right above this block.
-    pub fn from_checkpoint(client: &'c C, checkpoint: CheckPoint) -> Self {
-        Self {
-            client,
-            start_height: 0,
-            last_cp: Some(checkpoint),
+            last_cp,
             last_block: None,
             last_mempool_time: 0,
             last_mempool_tip: None,
@@ -134,7 +117,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
             .collect::<Result<Vec<_>, _>>()?;
 
         self.last_mempool_time = latest_time;
-        self.last_mempool_tip = self.last_cp.as_ref().map(|cp| cp.height());
+        self.last_mempool_tip = Some(self.last_cp.height());
 
         Ok(txs_to_emit)
     }
@@ -156,7 +139,8 @@ enum PollResponse {
     /// Fetched block is not in the best chain.
     BlockNotInBestChain,
     AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint),
-    AgreementPointNotFound,
+    /// Force the genesis checkpoint down the receiver's throat.
+    AgreementPointNotFound(BlockHash),
 }
 
 fn poll_once<C>(emitter: &Emitter<C>) -> Result<PollResponse, bitcoincore_rpc::Error>
@@ -166,45 +150,50 @@ where
     let client = emitter.client;
 
     if let Some(last_res) = &emitter.last_block {
-        assert!(
-            emitter.last_cp.is_some(),
-            "must not have block result without last cp"
-        );
-
-        let next_hash = match last_res.nextblockhash {
-            None => return Ok(PollResponse::NoMoreBlocks),
-            Some(next_hash) => next_hash,
+        let next_hash = if last_res.height < emitter.start_height as _ {
+            // enforce start height
+            let next_hash = client.get_block_hash(emitter.start_height as _)?;
+            // make sure last emission is still in best chain
+            if client.get_block_hash(last_res.height as _)? != last_res.hash {
+                return Ok(PollResponse::BlockNotInBestChain);
+            }
+            next_hash
+        } else {
+            match last_res.nextblockhash {
+                None => return Ok(PollResponse::NoMoreBlocks),
+                Some(next_hash) => next_hash,
+            }
         };
 
         let res = client.get_block_info(&next_hash)?;
         if res.confirmations < 0 {
             return Ok(PollResponse::BlockNotInBestChain);
         }
-        return Ok(PollResponse::Block(res));
-    }
-
-    if emitter.last_cp.is_none() {
-        let hash = client.get_block_hash(emitter.start_height as _)?;
 
-        let res = client.get_block_info(&hash)?;
-        if res.confirmations < 0 {
-            return Ok(PollResponse::BlockNotInBestChain);
-        }
         return Ok(PollResponse::Block(res));
     }
 
-    for cp in emitter.last_cp.iter().flat_map(CheckPoint::iter) {
-        let res = client.get_block_info(&cp.hash())?;
-        if res.confirmations < 0 {
-            // block is not in best chain
-            continue;
-        }
+    for cp in emitter.last_cp.iter() {
+        let res = match client.get_block_info(&cp.hash()) {
+            // block not in best chain
+            Ok(res) if res.confirmations < 0 => continue,
+            Ok(res) => res,
+            Err(e) if e.is_not_found_error() => {
+                if cp.height() > 0 {
+                    continue;
+                }
+                // if we can't find genesis block, we can't create an update that connects
+                break;
+            }
+            Err(e) => return Err(e),
+        };
 
         // agreement point found
         return Ok(PollResponse::AgreementFound(res, cp));
     }
 
-    Ok(PollResponse::AgreementPointNotFound)
+    let genesis_hash = client.get_block_hash(0)?;
+    Ok(PollResponse::AgreementPointNotFound(genesis_hash))
 }
 
 fn poll<C, V, F>(
@@ -222,25 +211,12 @@ where
                 let hash = res.hash;
                 let item = get_item(&hash)?;
 
-                let this_id = BlockId { height, hash };
-                let prev_id = res.previousblockhash.map(|prev_hash| BlockId {
-                    height: height - 1,
-                    hash: prev_hash,
-                });
-
-                match (&mut emitter.last_cp, prev_id) {
-                    (Some(cp), _) => *cp = cp.clone().push(this_id).expect("must push"),
-                    (last_cp, None) => *last_cp = Some(CheckPoint::new(this_id)),
-                    // When the receiver constructs a local_chain update from a block, the previous
-                    // checkpoint is also included in the update. We need to reflect this state in
-                    // `Emitter::last_cp` as well.
-                    (last_cp, Some(prev_id)) => {
-                        *last_cp = Some(CheckPoint::new(prev_id).push(this_id).expect("must push"))
-                    }
-                }
-
+                emitter.last_cp = emitter
+                    .last_cp
+                    .clone()
+                    .push(BlockId { height, hash })
+                    .expect("must push");
                 emitter.last_block = Some(res);
-
                 return Ok(Some((height, item)));
             }
             PollResponse::NoMoreBlocks => {
@@ -254,9 +230,6 @@ where
             PollResponse::AgreementFound(res, cp) => {
                 let agreement_h = res.height as u32;
 
-                // get rid of evicted blocks
-                emitter.last_cp = Some(cp);
-
                 // The tip during the last mempool emission needs to in the best chain, we reduce
                 // it if it is not.
                 if let Some(h) = emitter.last_mempool_tip.as_mut() {
@@ -264,15 +237,17 @@ where
                         *h = agreement_h;
                     }
                 }
+
+                // get rid of evicted blocks
+                emitter.last_cp = cp;
                 emitter.last_block = Some(res);
                 continue;
             }
-            PollResponse::AgreementPointNotFound => {
-                // We want to clear `last_cp` and set `start_height` to the first checkpoint's
-                // height. This way, the first checkpoint in `LocalChain` can be replaced.
-                if let Some(last_cp) = emitter.last_cp.take() {
-                    emitter.start_height = last_cp.height();
-                }
+            PollResponse::AgreementPointNotFound(genesis_hash) => {
+                emitter.last_cp = CheckPoint::new(BlockId {
+                    height: 0,
+                    hash: genesis_hash,
+                });
                 emitter.last_block = None;
                 continue;
             }
index f0bbd3d15806b40f479d399fc9a2c42f872aea48..d3c9e36bd889cc600cb4ace83ec1c94e1d780b45 100644 (file)
@@ -188,8 +188,8 @@ fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Up
 #[test]
 pub fn test_sync_local_chain() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
-    let mut local_chain = LocalChain::default();
-    let mut emitter = Emitter::from_height(&env.client, 0);
+    let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
+    let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
 
     // mine some blocks and returned the actual block hashes
     let exp_hashes = {
@@ -296,7 +296,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
     env.mine_blocks(101, None)?;
     println!("mined blocks!");
 
-    let mut chain = LocalChain::default();
+    let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
     let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
         let mut index = SpkTxOutIndex::<usize>::default();
         index.insert_spk(0, addr_0.script_pubkey());
@@ -305,7 +305,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
         index
     });
 
-    let emitter = &mut Emitter::from_height(&env.client, 0);
+    let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
 
     while let Some((height, block)) = emitter.next_block()? {
         let _ = chain.apply_update(block_to_chain_update(&block, height))?;
@@ -393,7 +393,14 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
     const CHAIN_TIP_HEIGHT: usize = 110;
 
     let env = TestEnv::new()?;
-    let mut emitter = Emitter::from_height(&env.client, EMITTER_START_HEIGHT as _);
+    let mut emitter = Emitter::new(
+        &env.client,
+        CheckPoint::new(BlockId {
+            height: 0,
+            hash: env.client.get_block_hash(0)?,
+        }),
+        EMITTER_START_HEIGHT as _,
+    );
 
     env.mine_blocks(CHAIN_TIP_HEIGHT, None)?;
     while emitter.next_header()?.is_some() {}
@@ -442,9 +449,7 @@ fn get_balance(
     recv_chain: &LocalChain,
     recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
 ) -> anyhow::Result<Balance> {
-    let chain_tip = recv_chain
-        .tip()
-        .map_or(BlockId::default(), |cp| cp.block_id());
+    let chain_tip = recv_chain.tip().block_id();
     let outpoints = recv_graph.index.outpoints().clone();
     let balance = recv_graph
         .graph()
@@ -461,7 +466,14 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
     const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
 
     let env = TestEnv::new()?;
-    let mut emitter = Emitter::from_height(&env.client, 0);
+    let mut emitter = Emitter::new(
+        &env.client,
+        CheckPoint::new(BlockId {
+            height: 0,
+            hash: env.client.get_block_hash(0)?,
+        }),
+        0,
+    );
 
     // setup addresses
     let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
@@ -469,7 +481,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
     let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
 
     // setup receiver
-    let mut recv_chain = LocalChain::default();
+    let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
     let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
         let mut recv_index = SpkTxOutIndex::default();
         recv_index.insert_spk((), spk_to_track.clone());
@@ -542,7 +554,14 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
     const MEMPOOL_TX_COUNT: usize = 2;
 
     let env = TestEnv::new()?;
-    let mut emitter = Emitter::from_height(&env.client, 0);
+    let mut emitter = Emitter::new(
+        &env.client,
+        CheckPoint::new(BlockId {
+            height: 0,
+            hash: env.client.get_block_hash(0)?,
+        }),
+        0,
+    );
 
     // mine blocks and sync up emitter
     let addr = env.client.get_new_address(None, None)?.assume_checked();
@@ -597,7 +616,14 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
     const MEMPOOL_TX_COUNT: usize = 21;
 
     let env = TestEnv::new()?;
-    let mut emitter = Emitter::from_height(&env.client, 0);
+    let mut emitter = Emitter::new(
+        &env.client,
+        CheckPoint::new(BlockId {
+            height: 0,
+            hash: env.client.get_block_hash(0)?,
+        }),
+        0,
+    );
 
     // mine blocks to get initial balance, sync emitter up to tip
     let addr = env.client.get_new_address(None, None)?.assume_checked();
@@ -674,7 +700,14 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
     const PREMINE_COUNT: usize = 101;
 
     let env = TestEnv::new()?;
-    let mut emitter = Emitter::from_height(&env.client, 0);
+    let mut emitter = Emitter::new(
+        &env.client,
+        CheckPoint::new(BlockId {
+            height: 0,
+            hash: env.client.get_block_hash(0)?,
+        }),
+        0,
+    );
 
     // mine blocks to get initial balance
     let addr = env.client.get_new_address(None, None)?.assume_checked();
@@ -789,7 +822,14 @@ fn no_agreement_point() -> anyhow::Result<()> {
     let env = TestEnv::new()?;
 
     // start height is 99
-    let mut emitter = Emitter::from_height(&env.client, (PREMINE_COUNT - 2) as u32);
+    let mut emitter = Emitter::new(
+        &env.client,
+        CheckPoint::new(BlockId {
+            height: 0,
+            hash: env.client.get_block_hash(0)?,
+        }),
+        (PREMINE_COUNT - 2) as u32,
+    );
 
     // mine 101 blocks
     env.mine_blocks(PREMINE_COUNT, None)?;
index e736be0354d68ca7ca82b4dd18a5e258f3b2e5ba..038025619f00774006e57af34456c8472e5166e2 100644 (file)
@@ -21,5 +21,5 @@ pub trait ChainOracle {
     ) -> Result<Option<bool>, Self::Error>;
 
     /// Get the best chain's chain tip.
-    fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error>;
+    fn get_chain_tip(&self) -> Result<BlockId, Self::Error>;
 }
index 094b77424d3780dcc4eb80098a80b6ff37dfd39f..1538cdfda55949f3bf1569e0754abc17b4bae8ce 100644 (file)
@@ -179,9 +179,9 @@ pub struct Update {
 }
 
 /// This is a local implementation of [`ChainOracle`].
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Clone)]
 pub struct LocalChain {
-    tip: Option<CheckPoint>,
+    tip: CheckPoint,
     index: BTreeMap<u32, BlockHash>,
 }
 
@@ -197,12 +197,6 @@ impl From<LocalChain> for BTreeMap<u32, BlockHash> {
     }
 }
 
-impl From<BTreeMap<u32, BlockHash>> for LocalChain {
-    fn from(value: BTreeMap<u32, BlockHash>) -> Self {
-        Self::from_blocks(value)
-    }
-}
-
 impl ChainOracle for LocalChain {
     type Error = Infallible;
 
@@ -225,39 +219,71 @@ impl ChainOracle for LocalChain {
         )
     }
 
-    fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> {
-        Ok(self.tip.as_ref().map(|tip| tip.block_id()))
+    fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
+        Ok(self.tip.block_id())
     }
 }
 
 impl LocalChain {
+    /// Get the genesis hash.
+    pub fn genesis_hash(&self) -> BlockHash {
+        self.index.get(&0).copied().expect("must have genesis hash")
+    }
+
+    /// Construct [`LocalChain`] from genesis `hash`.
+    #[must_use]
+    pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
+        let height = 0;
+        let chain = Self {
+            tip: CheckPoint::new(BlockId { height, hash }),
+            index: core::iter::once((height, hash)).collect(),
+        };
+        let changeset = chain.initial_changeset();
+        (chain, changeset)
+    }
+
     /// Construct a [`LocalChain`] from an initial `changeset`.
-    pub fn from_changeset(changeset: ChangeSet) -> Self {
-        let mut chain = Self::default();
-        chain.apply_changeset(&changeset);
+    pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
+        let genesis_entry = changeset.get(&0).copied().flatten();
+        let genesis_hash = match genesis_entry {
+            Some(hash) => hash,
+            None => return Err(MissingGenesisError),
+        };
+
+        let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
+        chain.apply_changeset(&changeset)?;
 
         debug_assert!(chain._check_index_is_consistent_with_tip());
         debug_assert!(chain._check_changeset_is_applied(&changeset));
 
-        chain
+        Ok(chain)
     }
 
     /// Construct a [`LocalChain`] from a given `checkpoint` tip.
-    pub fn from_tip(tip: CheckPoint) -> Self {
+    pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
         let mut chain = Self {
-            tip: Some(tip),
-            ..Default::default()
+            tip,
+            index: BTreeMap::new(),
         };
         chain.reindex(0);
+
+        if chain.index.get(&0).copied().is_none() {
+            return Err(MissingGenesisError);
+        }
+
         debug_assert!(chain._check_index_is_consistent_with_tip());
-        chain
+        Ok(chain)
     }
 
     /// 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 {
+    pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
+        if !blocks.contains_key(&0) {
+            return Err(MissingGenesisError);
+        }
+
         let mut tip: Option<CheckPoint> = None;
 
         for block in &blocks {
@@ -272,25 +298,20 @@ impl LocalChain {
             }
         }
 
-        let chain = Self { index: blocks, tip };
+        let chain = Self {
+            index: blocks,
+            tip: tip.expect("already checked to have genesis"),
+        };
 
         debug_assert!(chain._check_index_is_consistent_with_tip());
-
-        chain
+        Ok(chain)
     }
 
     /// Get the highest checkpoint.
-    pub fn tip(&self) -> Option<CheckPoint> {
+    pub fn tip(&self) -> CheckPoint {
         self.tip.clone()
     }
 
-    /// Returns whether the [`LocalChain`] is empty (has no checkpoints).
-    pub fn is_empty(&self) -> bool {
-        let res = self.tip.is_none();
-        debug_assert_eq!(res, self.index.is_empty());
-        res
-    }
-
     /// Applies the given `update` to the chain.
     ///
     /// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
@@ -312,34 +333,28 @@ impl LocalChain {
     ///
     /// [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();
-
-                debug_assert!(self._check_index_is_consistent_with_tip());
-                debug_assert!(self._check_changeset_is_applied(&changeset));
-                Ok(changeset)
-            }
-        }
+        let changeset = merge_chains(
+            self.tip.clone(),
+            update.tip.clone(),
+            update.introduce_older_blocks,
+        )?;
+        // `._check_index_is_consistent_with_tip` and `._check_changeset_is_applied` is called in
+        // `.apply_changeset`
+        self.apply_changeset(&changeset)
+            .map_err(|_| CannotConnectError {
+                try_include_height: 0,
+            })?;
+        Ok(changeset)
     }
 
     /// Apply the given `changeset`.
-    pub fn apply_changeset(&mut self, changeset: &ChangeSet) {
+    pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
         if let Some(start_height) = changeset.keys().next().cloned() {
+            // changes after point of agreement
             let mut extension = BTreeMap::default();
+            // point of agreement
             let mut base: Option<CheckPoint> = None;
+
             for cp in self.iter_checkpoints() {
                 if cp.height() >= start_height {
                     extension.insert(cp.height(), cp.hash());
@@ -359,12 +374,12 @@ impl LocalChain {
                     }
                 };
             }
+
             let new_tip = match base {
-                Some(base) => Some(
-                    base.extend(extension.into_iter().map(BlockId::from))
-                        .expect("extension is strictly greater than base"),
-                ),
-                None => LocalChain::from_blocks(extension).tip(),
+                Some(base) => base
+                    .extend(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);
@@ -372,6 +387,8 @@ impl LocalChain {
             debug_assert!(self._check_index_is_consistent_with_tip());
             debug_assert!(self._check_changeset_is_applied(changeset));
         }
+
+        Ok(())
     }
 
     /// Insert a [`BlockId`].
@@ -379,13 +396,13 @@ impl LocalChain {
     /// # Errors
     ///
     /// 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> {
+    pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
         if let Some(&original_hash) = self.index.get(&block_id.height) {
             if original_hash != block_id.hash {
-                return Err(InsertBlockError {
+                return Err(AlterCheckPointError {
                     height: block_id.height,
                     original_hash,
-                    update_hash: block_id.hash,
+                    update_hash: Some(block_id.hash),
                 });
             } else {
                 return Ok(ChangeSet::default());
@@ -394,7 +411,12 @@ impl LocalChain {
 
         let mut changeset = ChangeSet::default();
         changeset.insert(block_id.height, Some(block_id.hash));
-        self.apply_changeset(&changeset);
+        self.apply_changeset(&changeset)
+            .map_err(|_| AlterCheckPointError {
+                height: 0,
+                original_hash: self.genesis_hash(),
+                update_hash: changeset.get(&0).cloned().flatten(),
+            })?;
         Ok(changeset)
     }
 
@@ -418,7 +440,7 @@ impl LocalChain {
     /// Iterate over checkpoints in descending height order.
     pub fn iter_checkpoints(&self) -> CheckPointIter {
         CheckPointIter {
-            current: self.tip.as_ref().map(|tip| tip.0.clone()),
+            current: Some(self.tip.0.clone()),
         }
     }
 
@@ -431,7 +453,6 @@ impl LocalChain {
         let tip_history = self
             .tip
             .iter()
-            .flat_map(CheckPoint::iter)
             .map(|cp| (cp.height(), cp.hash()))
             .collect::<BTreeMap<_, _>>();
         self.index == tip_history
@@ -447,29 +468,52 @@ impl LocalChain {
     }
 }
 
-/// Represents a failure when trying to insert a checkpoint into [`LocalChain`].
+/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
 #[derive(Clone, Debug, PartialEq)]
-pub struct InsertBlockError {
-    /// The checkpoints' height.
-    pub height: u32,
-    /// Original checkpoint's block hash.
-    pub original_hash: BlockHash,
-    /// Update checkpoint's block hash.
-    pub update_hash: BlockHash,
-}
+pub struct MissingGenesisError;
 
-impl core::fmt::Display for InsertBlockError {
+impl core::fmt::Display for MissingGenesisError {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
         write!(
             f,
-            "failed to insert block at height {} as block hashes conflict: original={}, update={}",
-            self.height, self.original_hash, self.update_hash
+            "cannot construct `LocalChain` without a genesis checkpoint"
         )
     }
 }
 
 #[cfg(feature = "std")]
-impl std::error::Error for InsertBlockError {}
+impl std::error::Error for MissingGenesisError {}
+
+/// Represents a failure when trying to insert/remove a checkpoint to/from [`LocalChain`].
+#[derive(Clone, Debug, PartialEq)]
+pub struct AlterCheckPointError {
+    /// The checkpoint's height.
+    pub height: u32,
+    /// The original checkpoint's block hash which cannot be replaced/removed.
+    pub original_hash: BlockHash,
+    /// The attempted update to the `original_block` hash.
+    pub update_hash: Option<BlockHash>,
+}
+
+impl core::fmt::Display for AlterCheckPointError {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self.update_hash {
+            Some(update_hash) => write!(
+                f,
+                "failed to insert block at height {}: original={} update={}",
+                self.height, self.original_hash, update_hash
+            ),
+            None => write!(
+                f,
+                "failed to remove block at height {}: original={}",
+                self.height, self.original_hash
+            ),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AlterCheckPointError {}
 
 /// Occurs when an update does not have a common checkpoint with the original chain.
 #[derive(Clone, Debug, PartialEq)]
index d3db819103726ecd99d459b0cfaa2a6c26564088..cfb8b7c4496b7e62c4f6adfbabfd1c5a978b111b 100644 (file)
@@ -23,6 +23,7 @@ macro_rules! local_chain {
     [ $(($height:expr, $block_hash:expr)), * ] => {{
         #[allow(unused_mut)]
         bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
+            .expect("chain must have genesis block")
     }};
 }
 
@@ -32,8 +33,8 @@ macro_rules! chain_update {
         #[allow(unused_mut)]
         bdk_chain::local_chain::Update {
             tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
-            .tip()
-            .expect("must have tip"),
+                .expect("chain must have genesis block")
+                .tip(),
             introduce_older_blocks: true,
         }
     }};
index 3dc22ef5ba41ed235c8a28f47b0a741d3ee75e81..ec250c95c3aa4bb9f442aa47b976e1a1af05b65f 100644 (file)
@@ -1,7 +1,7 @@
 #[macro_use]
 mod common;
 
-use std::collections::{BTreeMap, BTreeSet};
+use std::collections::BTreeSet;
 
 use bdk_chain::{
     indexed_tx_graph::{self, IndexedTxGraph},
@@ -9,9 +9,7 @@ use bdk_chain::{
     local_chain::LocalChain,
     tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
 };
-use bitcoin::{
-    secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
-};
+use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
 use miniscript::Descriptor;
 
 /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
@@ -112,11 +110,8 @@ fn insert_relevant_txs() {
 
 fn test_list_owned_txouts() {
     // Create Local chains
-    let local_chain = LocalChain::from(
-        (0..150)
-            .map(|i| (i as u32, h!("random")))
-            .collect::<BTreeMap<u32, BlockHash>>(),
-    );
+    let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
+        .expect("must have genesis hash");
 
     // Initiate IndexedTxGraph
 
index 9ea8b7f3a6ace355e939e25520aa02f9b23a489f..d09325bd98e4c063c75e9dbc2f1afaae4c243a6e 100644 (file)
@@ -1,4 +1,6 @@
-use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update};
+use bdk_chain::local_chain::{
+    AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
+};
 use bitcoin::BlockHash;
 
 #[macro_use]
@@ -68,10 +70,10 @@ fn update_local_chain() {
     [
         TestLocalChain {
             name: "add first tip",
-            chain: local_chain![],
+            chain: local_chain![(0, h!("A"))],
             update: chain_update![(0, h!("A"))],
             exp: ExpectedResult::Ok {
-                changeset: &[(0, Some(h!("A")))],
+                changeset: &[],
                 init_changeset: &[(0, Some(h!("A")))],
             },
         },
@@ -86,18 +88,18 @@ fn update_local_chain() {
         },
         TestLocalChain {
             name: "two disjoint chains cannot merge",
-            chain: local_chain![(0, h!("A"))],
-            update: chain_update![(1, h!("B"))],
+            chain: local_chain![(0, h!("_")), (1, h!("A"))],
+            update: chain_update![(0, h!("_")), (2, h!("B"))],
             exp: ExpectedResult::Err(CannotConnectError {
-                try_include_height: 0,
+                try_include_height: 1,
             }),
         },
         TestLocalChain {
             name: "two disjoint chains cannot merge (existing chain longer)",
-            chain: local_chain![(1, h!("A"))],
-            update: chain_update![(0, h!("B"))],
+            chain: local_chain![(0, h!("_")), (2, h!("A"))],
+            update: chain_update![(0, h!("_")), (1, h!("B"))],
             exp: ExpectedResult::Err(CannotConnectError {
-                try_include_height: 1,
+                try_include_height: 2,
             }),
         },
         TestLocalChain {
@@ -111,54 +113,54 @@ fn update_local_chain() {
         },
         // Introduce an older checkpoint (B)
         //        | 0 | 1 | 2 | 3
-        // chain  |         C   D
-        // update |     B   C
+        // chain  | _       C   D
+        // update | _   B   C
         TestLocalChain {
             name: "can introduce older checkpoint",
-            chain: local_chain![(2, h!("C")), (3, h!("D"))],
-            update: chain_update![(1, h!("B")), (2, h!("C"))],
+            chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D"))],
+            update: chain_update![(0, h!("_")), (1, h!("B")), (2, h!("C"))],
             exp: ExpectedResult::Ok {
                 changeset: &[(1, Some(h!("B")))],
-                init_changeset: &[(1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
+                init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
             },
         },
         // Introduce an older checkpoint (A) that is not directly behind PoA
-        //        | 2 | 3 | 4
-        // chain  |     B   C
-        // update | A       C
+        //        | 0 | 2 | 3 | 4
+        // chain  | _       B   C
+        // update | _   A       C
         TestLocalChain {
             name: "can introduce older checkpoint 2",
-            chain: local_chain![(3, h!("B")), (4, h!("C"))],
-            update: chain_update![(2, h!("A")), (4, h!("C"))],
+            chain: local_chain![(0, h!("_")), (3, h!("B")), (4, h!("C"))],
+            update: chain_update![(0, h!("_")), (2, h!("A")), (4, h!("C"))],
             exp: ExpectedResult::Ok {
                 changeset: &[(2, Some(h!("A")))],
-                init_changeset: &[(2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
+                init_changeset: &[(0, Some(h!("_"))), (2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
             }
         },
         // Introduce an older checkpoint (B) that is not the oldest checkpoint
-        //        | 1 | 2 | 3
-        // chain  | A       C
-        // update |     B   C
+        //        | 0 | 1 | 2 | 3
+        // chain  | _   A       C
+        // update | _       B   C
         TestLocalChain {
             name: "can introduce older checkpoint 3",
-            chain: local_chain![(1, h!("A")), (3, h!("C"))],
-            update: chain_update![(2, h!("B")), (3, h!("C"))],
+            chain: local_chain![(0, h!("_")), (1, h!("A")), (3, h!("C"))],
+            update: chain_update![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
             exp: ExpectedResult::Ok {
                 changeset: &[(2, Some(h!("B")))],
-                init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
+                init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
             }
         },
         // Introduce two older checkpoints below the PoA
-        //        | 1 | 2 | 3
-        // chain  |         C
-        // update | A   B   C
+        //        | 0 | 1 | 2 | 3
+        // chain  | _           C
+        // update | _   A   B   C
         TestLocalChain {
             name: "introduce two older checkpoints below PoA",
-            chain: local_chain![(3, h!("C"))],
-            update: chain_update![(1, h!("A")), (2, h!("B")), (3, h!("C"))],
+            chain: local_chain![(0, h!("_")), (3, h!("C"))],
+            update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C"))],
             exp: ExpectedResult::Ok {
                 changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
-                init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
+                init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
             },
         },
         TestLocalChain {
@@ -172,45 +174,46 @@ fn update_local_chain() {
         },
         // B and C are in both chain and update
         //        | 0 | 1 | 2 | 3 | 4
-        // chain  |     B   C
-        // update | A   B   C   D
+        // chain  | _       B   C
+        // update | _   A   B   C   D
         // This should succeed with the point of agreement being C and A should be added in addition.
         TestLocalChain {
             name: "two points of agreement",
-            chain: local_chain![(1, h!("B")), (2, h!("C"))],
-            update: chain_update![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))],
+            chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
+            update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
             exp: ExpectedResult::Ok {
-                changeset: &[(0, Some(h!("A"))), (3, Some(h!("D")))],
+                changeset: &[(1, Some(h!("A"))), (4, Some(h!("D")))],
                 init_changeset: &[
-                    (0, Some(h!("A"))),
-                    (1, Some(h!("B"))),
-                    (2, Some(h!("C"))),
-                    (3, Some(h!("D"))),
+                    (0, Some(h!("_"))),
+                    (1, Some(h!("A"))),
+                    (2, Some(h!("B"))),
+                    (3, Some(h!("C"))),
+                    (4, Some(h!("D"))),
                 ],
             },
         },
         // Update and chain does not connect:
         //        | 0 | 1 | 2 | 3 | 4
-        // chain  |     B   C
-        // update | A   B       D
+        // chain  | _       B   C
+        // update | _   A   B       D
         // This should fail as we cannot figure out whether C & D are on the same chain
         TestLocalChain {
             name: "update and chain does not connect",
-            chain: local_chain![(1, h!("B")), (2, h!("C"))],
-            update: chain_update![(0, h!("A")), (1, h!("B")), (3, h!("D"))],
+            chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
+            update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (4, h!("D"))],
             exp: ExpectedResult::Err(CannotConnectError {
-                try_include_height: 2,
+                try_include_height: 3,
             }),
         },
         // Transient invalidation:
         //        | 0 | 1 | 2 | 3 | 4 | 5
-        // chain  | A       B   C       E
-        // update | A       B'  C'  D
+        // chain  | _       B   C       E
+        // update | _       B'  C'  D
         // This should succeed and invalidate B,C and E with point of agreement being A.
         TestLocalChain {
             name: "transitive invalidation applies to checkpoints higher than invalidation",
-            chain: local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
-            update: chain_update![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
+            chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
+            update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
             exp: ExpectedResult::Ok {
                 changeset: &[
                     (2, Some(h!("B'"))),
@@ -219,7 +222,7 @@ fn update_local_chain() {
                     (5, None),
                 ],
                 init_changeset: &[
-                    (0, Some(h!("A"))),
+                    (0, Some(h!("_"))),
                     (2, Some(h!("B'"))),
                     (3, Some(h!("C'"))),
                     (4, Some(h!("D"))),
@@ -228,13 +231,13 @@ fn update_local_chain() {
         },
         // Transient invalidation:
         //        | 0 | 1 | 2 | 3 | 4
-        // chain  |     B   C       E
-        // update |     B'  C'  D
+        // chain  | _   B   C       E
+        // update | _   B'  C'  D
         // This should succeed and invalidate B, C and E with no point of agreement
         TestLocalChain {
             name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement",
-            chain: local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))],
-            update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
+            chain: local_chain![(0, h!("_")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
+            update: chain_update![(0, h!("_")), (1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
             exp: ExpectedResult::Ok {
                 changeset: &[
                     (1, Some(h!("B'"))),
@@ -243,6 +246,7 @@ fn update_local_chain() {
                     (4, None)
                 ],
                 init_changeset: &[
+                    (0, Some(h!("_"))), 
                     (1, Some(h!("B'"))),
                     (2, Some(h!("C'"))),
                     (3, Some(h!("D"))),
@@ -250,16 +254,16 @@ fn update_local_chain() {
             },
         },
         // Transient invalidation:
-        //        | 0 | 1 | 2 | 3 | 4
-        // chain  | A   B   C       E
-        // update |     B'  C'  D
+        //        | 0 | 1 | 2 | 3 | 4 | 5
+        // chain  | _   A   B   C       E
+        // update | _       B'  C'  D
         // This should fail since although it tells us that B and C are invalid it doesn't tell us whether
         // A was invalid.
         TestLocalChain {
             name: "invalidation but no connection",
-            chain: local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
-            update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
-            exp: ExpectedResult::Err(CannotConnectError { try_include_height: 0 }),
+            chain: local_chain![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
+            update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
+            exp: ExpectedResult::Err(CannotConnectError { try_include_height: 1 }),
         },
         // Introduce blocks between two points of agreement
         //        | 0 | 1 | 2 | 3 | 4 | 5
@@ -294,44 +298,44 @@ fn local_chain_insert_block() {
     struct TestCase {
         original: LocalChain,
         insert: (u32, BlockHash),
-        expected_result: Result<ChangeSet, InsertBlockError>,
+        expected_result: Result<ChangeSet, AlterCheckPointError>,
         expected_final: LocalChain,
     }
 
     let test_cases = [
         TestCase {
-            original: local_chain![],
+            original: local_chain![(0, h!("_"))],
             insert: (5, h!("block5")),
             expected_result: Ok([(5, Some(h!("block5")))].into()),
-            expected_final: local_chain![(5, h!("block5"))],
+            expected_final: local_chain![(0, h!("_")), (5, h!("block5"))],
         },
         TestCase {
-            original: local_chain![(3, h!("A"))],
+            original: local_chain![(0, h!("_")), (3, h!("A"))],
             insert: (4, h!("B")),
             expected_result: Ok([(4, Some(h!("B")))].into()),
-            expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
+            expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
         },
         TestCase {
-            original: local_chain![(4, h!("B"))],
+            original: local_chain![(0, h!("_")), (4, h!("B"))],
             insert: (3, h!("A")),
             expected_result: Ok([(3, Some(h!("A")))].into()),
-            expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
+            expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
         },
         TestCase {
-            original: local_chain![(2, h!("K"))],
+            original: local_chain![(0, h!("_")), (2, h!("K"))],
             insert: (2, h!("K")),
             expected_result: Ok([].into()),
-            expected_final: local_chain![(2, h!("K"))],
+            expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
         },
         TestCase {
-            original: local_chain![(2, h!("K"))],
+            original: local_chain![(0, h!("_")), (2, h!("K"))],
             insert: (2, h!("J")),
-            expected_result: Err(InsertBlockError {
+            expected_result: Err(AlterCheckPointError {
                 height: 2,
                 original_hash: h!("K"),
-                update_hash: h!("J"),
+                update_hash: Some(h!("J")),
             }),
-            expected_final: local_chain![(2, h!("K"))],
+            expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
         },
     ];
 
index a0efd1004c4714add05183a629f218fd371f3db5..b5311ea0b8c3ed9459c02d5c64bcdd7e436349b3 100644 (file)
@@ -511,11 +511,13 @@ fn test_calculate_fee_on_coinbase() {
 // where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc.
 #[test]
 fn test_walk_ancestors() {
-    let local_chain: LocalChain = (0..=20)
-        .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
-        .collect::<BTreeMap<u32, BlockHash>>()
-        .into();
-    let tip = local_chain.tip().expect("must have tip");
+    let local_chain = LocalChain::from_blocks(
+        (0..=20)
+            .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
+            .collect(),
+    )
+    .expect("must contain genesis hash");
+    let tip = local_chain.tip();
 
     let tx_a0 = Transaction {
         input: vec![TxIn {
@@ -839,11 +841,13 @@ fn test_descendants_no_repeat() {
 
 #[test]
 fn test_chain_spends() {
-    let local_chain: LocalChain = (0..=100)
-        .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
-        .collect::<BTreeMap<u32, BlockHash>>()
-        .into();
-    let tip = local_chain.tip().expect("must have tip");
+    let local_chain = LocalChain::from_blocks(
+        (0..=100)
+            .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
+            .collect(),
+    )
+    .expect("must have genesis hash");
+    let tip = local_chain.tip();
 
     // The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
     // The parent tx is confirmed at block 95.
@@ -1078,7 +1082,7 @@ fn test_missing_blocks() {
                 g
             },
             chain: {
-                let mut c = LocalChain::default();
+                let (mut c, _) = LocalChain::from_genesis_hash(h!("genesis"));
                 for (height, hash) in chain {
                     let _ = c.insert_block(BlockId {
                         height: *height,
index 9660e3cb1f0b170d54c6d5113558b2c892ee67ba..f2a161b43c014b112bdc95c30cdaea653bbe9551 100644 (file)
@@ -39,10 +39,7 @@ fn test_tx_conflict_handling() {
         (5, h!("F")),
         (6, h!("G"))
     );
-    let chain_tip = local_chain
-        .tip()
-        .map(|cp| cp.block_id())
-        .unwrap_or_default();
+    let chain_tip = local_chain.tip().block_id();
 
     let scenarios = [
         Scenario {
index e0d2a756583d1d835528091f69ab472c85eb0988..e54007cd93b83f183a40ba58abb914835323007d 100644 (file)
@@ -148,7 +148,7 @@ pub trait ElectrumExt {
     /// single batch request.
     fn scan<K: Ord + Clone>(
         &self,
-        prev_tip: Option<CheckPoint>,
+        prev_tip: CheckPoint,
         keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
         txids: impl IntoIterator<Item = Txid>,
         outpoints: impl IntoIterator<Item = OutPoint>,
@@ -161,7 +161,7 @@ pub trait ElectrumExt {
     /// [`scan`]: ElectrumExt::scan
     fn scan_without_keychain(
         &self,
-        prev_tip: Option<CheckPoint>,
+        prev_tip: CheckPoint,
         misc_spks: impl IntoIterator<Item = ScriptBuf>,
         txids: impl IntoIterator<Item = Txid>,
         outpoints: impl IntoIterator<Item = OutPoint>,
@@ -188,7 +188,7 @@ pub trait ElectrumExt {
 impl ElectrumExt for Client {
     fn scan<K: Ord + Clone>(
         &self,
-        prev_tip: Option<CheckPoint>,
+        prev_tip: CheckPoint,
         keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
         txids: impl IntoIterator<Item = Txid>,
         outpoints: impl IntoIterator<Item = OutPoint>,
@@ -289,17 +289,15 @@ impl ElectrumExt for Client {
 /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
 fn construct_update_tip(
     client: &Client,
-    prev_tip: Option<CheckPoint>,
+    prev_tip: CheckPoint,
 ) -> Result<(CheckPoint, Option<u32>), Error> {
     let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
     let new_tip_height = height as u32;
 
     // If electrum returns a tip height that is lower than our previous tip, then checkpoints do
     // not need updating. We just return the previous tip and use that as the point of agreement.
-    if let Some(prev_tip) = prev_tip.as_ref() {
-        if new_tip_height < prev_tip.height() {
-            return Ok((prev_tip.clone(), Some(prev_tip.height())));
-        }
+    if new_tip_height < prev_tip.height() {
+        return Ok((prev_tip.clone(), Some(prev_tip.height())));
     }
 
     // Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
@@ -317,7 +315,7 @@ fn construct_update_tip(
     // Find the "point of agreement" (if any).
     let agreement_cp = {
         let mut agreement_cp = Option::<CheckPoint>::None;
-        for cp in prev_tip.iter().flat_map(CheckPoint::iter) {
+        for cp in prev_tip.iter() {
             let cp_block = cp.block_id();
             let hash = match new_blocks.get(&cp_block.height) {
                 Some(&hash) => hash,
index 10df02d44b926f2fe989f9e90de492a24613b252..653a80f030cd421cb56293a0a60c76596a02f4aa 100644 (file)
@@ -32,7 +32,7 @@ pub trait EsploraAsyncExt {
     #[allow(clippy::result_large_err)]
     async fn update_local_chain(
         &self,
-        local_tip: Option<CheckPoint>,
+        local_tip: CheckPoint,
         request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
     ) -> Result<local_chain::Update, Error>;
 
@@ -95,7 +95,7 @@ pub trait EsploraAsyncExt {
 impl EsploraAsyncExt for esplora_client::AsyncClient {
     async fn update_local_chain(
         &self,
-        local_tip: Option<CheckPoint>,
+        local_tip: CheckPoint,
         request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
     ) -> Result<local_chain::Update, Error> {
         let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
@@ -129,41 +129,39 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
         let earliest_agreement_cp = {
             let mut earliest_agreement_cp = Option::<CheckPoint>::None;
 
-            if let Some(local_tip) = local_tip {
-                let local_tip_height = local_tip.height();
-                for local_cp in local_tip.iter() {
-                    let local_block = local_cp.block_id();
+            let local_tip_height = local_tip.height();
+            for local_cp in local_tip.iter() {
+                let local_block = local_cp.block_id();
 
-                    // the updated hash (block hash at this height after the update), can either be:
-                    // 1. a block that already existed in `fetched_blocks`
-                    // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
-                    // 3. otherwise we can freshly fetch the block from remote, which is safe as it
-                    //    is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
-                    //    remote tip
-                    let updated_hash = match fetched_blocks.entry(local_block.height) {
-                        btree_map::Entry::Occupied(entry) => *entry.get(),
-                        btree_map::Entry::Vacant(entry) => *entry.insert(
-                            if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
-                                local_block.hash
-                            } else {
-                                self.get_block_hash(local_block.height).await?
-                            },
-                        ),
-                    };
+                // the updated hash (block hash at this height after the update), can either be:
+                // 1. a block that already existed in `fetched_blocks`
+                // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
+                // 3. otherwise we can freshly fetch the block from remote, which is safe as it
+                //    is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
+                //    remote tip
+                let updated_hash = match fetched_blocks.entry(local_block.height) {
+                    btree_map::Entry::Occupied(entry) => *entry.get(),
+                    btree_map::Entry::Vacant(entry) => *entry.insert(
+                        if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
+                            local_block.hash
+                        } else {
+                            self.get_block_hash(local_block.height).await?
+                        },
+                    ),
+                };
 
-                    // since we may introduce blocks below the point of agreement, we cannot break
-                    // here unconditionally - we only break if we guarantee there are no new heights
-                    // below our current local checkpoint
-                    if local_block.hash == updated_hash {
-                        earliest_agreement_cp = Some(local_cp);
+                // since we may introduce blocks below the point of agreement, we cannot break
+                // here unconditionally - we only break if we guarantee there are no new heights
+                // below our current local checkpoint
+                if local_block.hash == updated_hash {
+                    earliest_agreement_cp = Some(local_cp);
 
-                        let first_new_height = *fetched_blocks
-                            .keys()
-                            .next()
-                            .expect("must have at least one new block");
-                        if first_new_height >= local_block.height {
-                            break;
-                        }
+                    let first_new_height = *fetched_blocks
+                        .keys()
+                        .next()
+                        .expect("must have at least one new block");
+                    if first_new_height >= local_block.height {
+                        break;
                     }
                 }
             }
index 84c7fd4b6ca506c3b5993726da556f4d9ca1964a..9c259d5835d7c05636be0237c27dafd542168b14 100644 (file)
@@ -30,7 +30,7 @@ pub trait EsploraExt {
     #[allow(clippy::result_large_err)]
     fn update_local_chain(
         &self,
-        local_tip: Option<CheckPoint>,
+        local_tip: CheckPoint,
         request_heights: impl IntoIterator<Item = u32>,
     ) -> Result<local_chain::Update, Error>;
 
@@ -87,7 +87,7 @@ pub trait EsploraExt {
 impl EsploraExt for esplora_client::BlockingClient {
     fn update_local_chain(
         &self,
-        local_tip: Option<CheckPoint>,
+        local_tip: CheckPoint,
         request_heights: impl IntoIterator<Item = u32>,
     ) -> Result<local_chain::Update, Error> {
         let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
@@ -120,41 +120,39 @@ impl EsploraExt for esplora_client::BlockingClient {
         let earliest_agreement_cp = {
             let mut earliest_agreement_cp = Option::<CheckPoint>::None;
 
-            if let Some(local_tip) = local_tip {
-                let local_tip_height = local_tip.height();
-                for local_cp in local_tip.iter() {
-                    let local_block = local_cp.block_id();
+            let local_tip_height = local_tip.height();
+            for local_cp in local_tip.iter() {
+                let local_block = local_cp.block_id();
 
-                    // the updated hash (block hash at this height after the update), can either be:
-                    // 1. a block that already existed in `fetched_blocks`
-                    // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
-                    // 3. otherwise we can freshly fetch the block from remote, which is safe as it
-                    //    is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
-                    //    remote tip
-                    let updated_hash = match fetched_blocks.entry(local_block.height) {
-                        btree_map::Entry::Occupied(entry) => *entry.get(),
-                        btree_map::Entry::Vacant(entry) => *entry.insert(
-                            if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
-                                local_block.hash
-                            } else {
-                                self.get_block_hash(local_block.height)?
-                            },
-                        ),
-                    };
+                // the updated hash (block hash at this height after the update), can either be:
+                // 1. a block that already existed in `fetched_blocks`
+                // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
+                // 3. otherwise we can freshly fetch the block from remote, which is safe as it
+                //    is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
+                //    remote tip
+                let updated_hash = match fetched_blocks.entry(local_block.height) {
+                    btree_map::Entry::Occupied(entry) => *entry.get(),
+                    btree_map::Entry::Vacant(entry) => *entry.insert(
+                        if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
+                            local_block.hash
+                        } else {
+                            self.get_block_hash(local_block.height)?
+                        },
+                    ),
+                };
 
-                    // since we may introduce blocks below the point of agreement, we cannot break
-                    // here unconditionally - we only break if we guarantee there are no new heights
-                    // below our current local checkpoint
-                    if local_block.hash == updated_hash {
-                        earliest_agreement_cp = Some(local_cp);
+                // since we may introduce blocks below the point of agreement, we cannot break
+                // here unconditionally - we only break if we guarantee there are no new heights
+                // below our current local checkpoint
+                if local_block.hash == updated_hash {
+                    earliest_agreement_cp = Some(local_cp);
 
-                        let first_new_height = *fetched_blocks
-                            .keys()
-                            .next()
-                            .expect("must have at least one new block");
-                        if first_new_height >= local_block.height {
-                            break;
-                        }
+                    let first_new_height = *fetched_blocks
+                        .keys()
+                        .next()
+                        .expect("must have at least one new block");
+                    if first_new_height >= local_block.height {
+                        break;
                     }
                 }
             }
index f0b9996d36ea0cacdca1ad4f71dcfc4bdd01e378..6b30f01f663c6a6b630f39a3ca9a2a6edcd9e859 100644 (file)
@@ -131,7 +131,7 @@ fn main() -> anyhow::Result<()> {
         start.elapsed().as_secs_f32()
     );
 
-    let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0));
+    let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)?);
     println!(
         "[{:>10}s] loaded local chain from changeset",
         start.elapsed().as_secs_f32()
@@ -170,10 +170,7 @@ fn main() -> anyhow::Result<()> {
 
             let chain_tip = chain.lock().unwrap().tip();
             let rpc_client = rpc_args.new_client()?;
-            let mut emitter = match chain_tip {
-                Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
-                None => Emitter::from_height(&rpc_client, fallback_height),
-            };
+            let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
 
             let mut last_db_commit = Instant::now();
             let mut last_print = Instant::now();
@@ -205,23 +202,22 @@ fn main() -> anyhow::Result<()> {
                 // print synced-to height and current balance in intervals
                 if last_print.elapsed() >= STDOUT_PRINT_DELAY {
                     last_print = Instant::now();
-                    if let Some(synced_to) = chain.tip() {
-                        let balance = {
-                            graph.graph().balance(
-                                &*chain,
-                                synced_to.block_id(),
-                                graph.index.outpoints().iter().cloned(),
-                                |(k, _), _| k == &Keychain::Internal,
-                            )
-                        };
-                        println!(
-                            "[{:>10}s] synced to {} @ {} | total: {} sats",
-                            start.elapsed().as_secs_f32(),
-                            synced_to.hash(),
-                            synced_to.height(),
-                            balance.total()
-                        );
-                    }
+                    let synced_to = chain.tip();
+                    let balance = {
+                        graph.graph().balance(
+                            &*chain,
+                            synced_to.block_id(),
+                            graph.index.outpoints().iter().cloned(),
+                            |(k, _), _| k == &Keychain::Internal,
+                        )
+                    };
+                    println!(
+                        "[{:>10}s] synced to {} @ {} | total: {} sats",
+                        start.elapsed().as_secs_f32(),
+                        synced_to.hash(),
+                        synced_to.height(),
+                        balance.total()
+                    );
                 }
             }
 
@@ -253,10 +249,7 @@ fn main() -> anyhow::Result<()> {
             let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND);
             let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> {
                 let rpc_client = rpc_args.new_client()?;
-                let mut emitter = match last_cp {
-                    Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
-                    None => Emitter::from_height(&rpc_client, fallback_height),
-                };
+                let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height);
 
                 let mut block_count = rpc_client.get_block_count()? as u32;
                 tx.send(Emission::Tip(block_count))?;
@@ -335,24 +328,23 @@ fn main() -> anyhow::Result<()> {
 
                 if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY {
                     last_print = Some(Instant::now());
-                    if let Some(synced_to) = chain.tip() {
-                        let balance = {
-                            graph.graph().balance(
-                                &*chain,
-                                synced_to.block_id(),
-                                graph.index.outpoints().iter().cloned(),
-                                |(k, _), _| k == &Keychain::Internal,
-                            )
-                        };
-                        println!(
-                            "[{:>10}s] synced to {} @ {} / {} | total: {} sats",
-                            start.elapsed().as_secs_f32(),
-                            synced_to.hash(),
-                            synced_to.height(),
-                            tip_height,
-                            balance.total()
-                        );
-                    }
+                    let synced_to = chain.tip();
+                    let balance = {
+                        graph.graph().balance(
+                            &*chain,
+                            synced_to.block_id(),
+                            graph.index.outpoints().iter().cloned(),
+                            |(k, _), _| k == &Keychain::Internal,
+                        )
+                    };
+                    println!(
+                        "[{:>10}s] synced to {} @ {} / {} | total: {} sats",
+                        start.elapsed().as_secs_f32(),
+                        synced_to.hash(),
+                        synced_to.height(),
+                        tip_height,
+                        balance.total()
+                    );
                 }
             }
 
index 9e572a89299dbf5adb233a40fa8eb64344f260ec..3649980346ad1e6de793974d8e1c1cc0a395f9b7 100644 (file)
@@ -315,10 +315,8 @@ where
         version: 0x02,
         // because the temporary planning module does not support timelocks, we can use the chain
         // tip as the `lock_time` for anti-fee-sniping purposes
-        lock_time: chain
-            .get_chain_tip()?
-            .and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok())
-            .unwrap_or(absolute::LockTime::ZERO),
+        lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
+            .expect("invalid height"),
         input: selected_txos
             .iter()
             .map(|(_, utxo)| TxIn {
@@ -404,7 +402,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
     chain: &O,
     assets: &bdk_tmp_plan::Assets<K>,
 ) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
-    let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
+    let chain_tip = chain.get_chain_tip()?;
     let outpoints = graph.index.outpoints().iter().cloned();
     graph
         .graph()
@@ -509,7 +507,7 @@ where
 
             let balance = graph.graph().try_balance(
                 chain,
-                chain.get_chain_tip()?.unwrap_or_default(),
+                chain.get_chain_tip()?,
                 graph.index.outpoints().iter().cloned(),
                 |(k, _), _| k == &Keychain::Internal,
             )?;
@@ -539,7 +537,7 @@ where
         Commands::TxOut { txout_cmd } => {
             let graph = &*graph.lock().unwrap();
             let chain = &*chain.lock().unwrap();
-            let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
+            let chain_tip = chain.get_chain_tip()?;
             let outpoints = graph.index.outpoints().iter().cloned();
 
             match txout_cmd {
index be5ffc7bc86dadb2c6db0ea27989cfc526118000..a96378f64750257b045c65047c209f255e0b8273 100644 (file)
@@ -112,7 +112,7 @@ fn main() -> anyhow::Result<()> {
         graph
     });
 
-    let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain));
+    let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)?);
 
     let electrum_cmd = match &args.command {
         example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
@@ -193,7 +193,7 @@ fn main() -> anyhow::Result<()> {
             // Get a short lock on the tracker to get the spks we're interested in
             let graph = graph.lock().unwrap();
             let chain = chain.lock().unwrap();
-            let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
+            let chain_tip = chain.tip().block_id();
 
             if !(all_spks || unused_spks || utxos || unconfirmed) {
                 unused_spks = true;
index df669d36f3f27bf48f5297de51e97617d8557265..c294986ad59cbe0788f2821b1d0dbd74c978ef6e 100644 (file)
@@ -5,10 +5,10 @@ use std::{
 };
 
 use bdk_chain::{
-    bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid},
+    bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
     indexed_tx_graph::{self, IndexedTxGraph},
     keychain,
-    local_chain::{self, CheckPoint, LocalChain},
+    local_chain::{self, LocalChain},
     Append, ConfirmationTimeHeightAnchor,
 };
 
@@ -102,6 +102,8 @@ fn main() -> anyhow::Result<()> {
     let (args, keymap, index, db, init_changeset) =
         example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
 
+    let genesis_hash = genesis_block(args.network).block_hash();
+
     let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset;
 
     // Contruct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in
@@ -113,8 +115,8 @@ fn main() -> anyhow::Result<()> {
         graph
     });
     let chain = Mutex::new({
-        let mut chain = LocalChain::default();
-        chain.apply_changeset(&init_chain_changeset);
+        let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
+        chain.apply_changeset(&init_chain_changeset)?;
         chain
     });
 
@@ -234,7 +236,7 @@ fn main() -> anyhow::Result<()> {
             {
                 let graph = graph.lock().unwrap();
                 let chain = chain.lock().unwrap();
-                let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
+                let chain_tip = chain.tip().block_id();
 
                 if *all_spks {
                     let all_spks = graph
@@ -332,7 +334,7 @@ fn main() -> anyhow::Result<()> {
         (missing_block_heights, tip)
     };
 
-    println!("prev tip: {}", tip.as_ref().map_or(0, CheckPoint::height));
+    println!("prev tip: {}", tip.height());
     println!("missing block heights: {:?}", missing_block_heights);
 
     // Here, we actually fetch the missing blocks and create a `local_chain::Update`.