feat!: LocalChain with hardwired genesis checkpoint
				
					
				
			This ensures that `LocalChain` will always have a tip. The `ChainOracle` trait's `get_chain_tip` method no longer needs to return an option.
This commit is contained in:
		
							parent
							
								
									d6a0cf0795
								
							
						
					
					
						commit
						5998a22819
					
				@ -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)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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>,
 | 
			
		||||
        mut db: D,
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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)?;
 | 
			
		||||
 | 
			
		||||
@ -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>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)]
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        }
 | 
			
		||||
    }};
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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"))],
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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`.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user