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)),
+ }
+ }
+}
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};
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 {
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 {
/// confirmation height that is greater than the internal chain tip.
ConfirmationHeightCannotBeGreaterThanTip {
/// The internal chain's tip height.
- tip_height: Option<u32>,
+ tip_height: u32,
/// The introduced transaction's confirmation height.
tx_height: u32,
},
}
-#[cfg(feature = "std")]
-impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for NewError<P> {}
-
impl<D> Wallet<D> {
/// Create a wallet from a `descriptor` (and an optional `change_descriptor`) and load related
/// transaction data from `db`.
pub fn new<E: IntoWalletDescriptor>(
+ descriptor: E,
+ change_descriptor: Option<E>,
+ db: D,
+ network: Network,
+ ) -> Result<Self, NewError<D::LoadError>>
+ where
+ D: PersistBackend<ChangeSet>,
+ {
+ Self::with_custom_genesis_hash(descriptor, change_descriptor, db, network, None)
+ }
+
+ /// Create a new [`Wallet`] with a custom genesis hash.
+ ///
+ /// This is like [`Wallet::new`] with an additional `custom_genesis_hash` parameter.
+ pub fn with_custom_genesis_hash<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
mut db: D,
network: Network,
+ custom_genesis_hash: Option<BlockHash>,
) -> Result<Self, NewError<D::LoadError>>
where
D: PersistBackend<ChangeSet>,
{
let secp = Secp256k1::new();
- let mut chain = LocalChain::default();
+ let genesis_hash =
+ custom_genesis_hash.unwrap_or_else(|| genesis_block(network).block_hash());
+ let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
let mut indexed_graph = IndexedTxGraph::<
ConfirmationTimeHeightAnchor,
KeychainTxOutIndex<KeychainKind>,
};
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);
.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))
}
/// Returns the latest checkpoint.
- pub fn latest_checkpoint(&self) -> Option<CheckPoint> {
+ pub fn latest_checkpoint(&self) -> CheckPoint {
self.chain.tip()
}
.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))
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)?,
pub fn insert_checkpoint(
&mut self,
block_id: BlockId,
- ) -> Result<bool, local_chain::InsertBlockError>
+ ) -> Result<bool, local_chain::AlterCheckPointError>
where
D: PersistBackend<ChangeSet>,
{
.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 {
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
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,
)
_ => 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 {
// 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
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
) -> 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)
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;
});
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` = {:?}",
&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(_) => {
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();
}
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 =
// 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()
);
}
.insert_tx(
tx.clone(),
ConfirmationTime::Confirmed {
- height: wallet.latest_checkpoint().unwrap().height(),
+ height: wallet.latest_checkpoint().height(),
time: 42_000,
},
)
/// 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`
}
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,
.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)
}
/// 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>
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>(
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 => {
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() {
*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;
}
#[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 = {
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());
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))?;
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() {}
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()
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();
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());
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();
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();
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();
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)?;
) -> 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>;
}
}
/// 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>,
}
}
}
-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;
)
}
- 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 {
}
}
- 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`.
///
/// [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());
}
};
}
+
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);
debug_assert!(self._check_index_is_consistent_with_tip());
debug_assert!(self._check_changeset_is_applied(changeset));
}
+
+ Ok(())
}
/// Insert a [`BlockId`].
/// # 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());
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)
}
/// 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()),
}
}
let tip_history = self
.tip
.iter()
- .flat_map(CheckPoint::iter)
.map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeMap<_, _>>();
self.index == tip_history
}
}
-/// 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)]
[ $(($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")
}};
}
#[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,
}
}};
#[macro_use]
mod common;
-use std::collections::{BTreeMap, BTreeSet};
+use std::collections::BTreeSet;
use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph},
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
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
-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]
[
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")))],
},
},
},
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 {
},
// 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 {
},
// 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'"))),
(5, None),
],
init_changeset: &[
- (0, Some(h!("A"))),
+ (0, Some(h!("_"))),
(2, Some(h!("B'"))),
(3, Some(h!("C'"))),
(4, Some(h!("D"))),
},
// 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'"))),
(4, None)
],
init_changeset: &[
+ (0, Some(h!("_"))),
(1, Some(h!("B'"))),
(2, Some(h!("C'"))),
(3, Some(h!("D"))),
},
},
// 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
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"))],
},
];
// 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 {
#[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.
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,
(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 {
/// 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>,
/// [`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>,
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>,
/// 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
// 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,
#[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>;
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<_>>();
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;
}
}
}
#[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>;
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<_>>();
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;
}
}
}
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()
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();
// 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()
+ );
}
}
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))?;
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()
+ );
}
}
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 {
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()
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,
)?;
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 {
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,
// 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;
};
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,
};
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
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
});
{
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
(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`.