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:
志宇 2023-10-12 16:55:32 +08:00
parent d6a0cf0795
commit 5998a22819
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
19 changed files with 562 additions and 452 deletions

View File

@ -199,3 +199,12 @@ impl_error!(miniscript::Error, Miniscript);
impl_error!(MiniscriptPsbtError, MiniscriptPsbt); impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
impl_error!(bitcoin::bip32::Error, Bip32); impl_error!(bitcoin::bip32::Error, Bip32);
impl_error!(bitcoin::psbt::Error, Psbt); 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)),
}
}
}

View File

@ -28,14 +28,14 @@ use bdk_chain::{
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut, Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
IndexedTxGraph, Persist, PersistBackend, IndexedTxGraph, Persist, PersistBackend,
}; };
use bitcoin::consensus::encode::serialize;
use bitcoin::psbt;
use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::Secp256k1;
use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::{ use bitcoin::{
absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid, absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid,
Weight, Witness, Weight, Witness,
}; };
use bitcoin::{consensus::encode::serialize, BlockHash};
use bitcoin::{constants::genesis_block, psbt};
use core::fmt; use core::fmt;
use core::ops::Deref; use core::ops::Deref;
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
@ -225,26 +225,57 @@ impl Wallet {
descriptor: E, descriptor: E,
change_descriptor: Option<E>, change_descriptor: Option<E>,
network: Network, network: Network,
) -> Result<Self, crate::descriptor::DescriptorError> { ) -> Result<Self, NewNoPersistError> {
Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
NewError::Descriptor(e) => e, NewError::Descriptor(e) => NewNoPersistError::Descriptor(e),
NewError::Persist(_) => unreachable!("no persistence so it can't fail"), 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)] #[derive(Debug)]
/// Error returned from [`Wallet::new`] /// Error returned from [`Wallet::new`]
pub enum NewError<P> { pub enum NewError<PE> {
/// There was problem with the descriptors passed in /// There was problem with the descriptors passed in
Descriptor(crate::descriptor::DescriptorError), Descriptor(crate::descriptor::DescriptorError),
/// We were unable to load the wallet's data from the persistence backend /// 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 where
P: fmt::Display, PE: fmt::Display,
{ {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -252,10 +283,18 @@ where
NewError::Persist(e) => { NewError::Persist(e) => {
write!(f, "failed to load wallet from persistence backend: {}", 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`]. /// An error that may occur when inserting a transaction into [`Wallet`].
#[derive(Debug)] #[derive(Debug)]
pub enum InsertTxError { pub enum InsertTxError {
@ -263,29 +302,44 @@ pub enum InsertTxError {
/// confirmation height that is greater than the internal chain tip. /// confirmation height that is greater than the internal chain tip.
ConfirmationHeightCannotBeGreaterThanTip { ConfirmationHeightCannotBeGreaterThanTip {
/// The internal chain's tip height. /// The internal chain's tip height.
tip_height: Option<u32>, tip_height: u32,
/// The introduced transaction's confirmation height. /// The introduced transaction's confirmation height.
tx_height: u32, tx_height: u32,
}, },
} }
#[cfg(feature = "std")]
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for NewError<P> {}
impl<D> Wallet<D> { impl<D> Wallet<D> {
/// Create a wallet from a `descriptor` (and an optional `change_descriptor`) and load related /// Create a wallet from a `descriptor` (and an optional `change_descriptor`) and load related
/// transaction data from `db`. /// transaction data from `db`.
pub fn new<E: IntoWalletDescriptor>( pub fn new<E: IntoWalletDescriptor>(
descriptor: E, descriptor: E,
change_descriptor: Option<E>, change_descriptor: Option<E>,
mut db: D, db: D,
network: Network, network: Network,
) -> Result<Self, NewError<D::LoadError>> ) -> 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 where
D: PersistBackend<ChangeSet>, D: PersistBackend<ChangeSet>,
{ {
let secp = Secp256k1::new(); 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::< let mut indexed_graph = IndexedTxGraph::<
ConfirmationTimeHeightAnchor, ConfirmationTimeHeightAnchor,
KeychainTxOutIndex<KeychainKind>, KeychainTxOutIndex<KeychainKind>,
@ -319,7 +373,9 @@ impl<D> Wallet<D> {
}; };
let changeset = db.load_from_persistence().map_err(NewError::Persist)?; 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); indexed_graph.apply_changeset(changeset.indexed_tx_graph);
let persist = Persist::new(db); let persist = Persist::new(db);
@ -446,7 +502,7 @@ impl<D> Wallet<D> {
.graph() .graph()
.filter_chain_unspents( .filter_chain_unspents(
&self.chain, &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(), self.indexed_graph.index.outpoints().iter().cloned(),
) )
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
@ -458,7 +514,7 @@ impl<D> Wallet<D> {
} }
/// Returns the latest checkpoint. /// Returns the latest checkpoint.
pub fn latest_checkpoint(&self) -> Option<CheckPoint> { pub fn latest_checkpoint(&self) -> CheckPoint {
self.chain.tip() self.chain.tip()
} }
@ -496,7 +552,7 @@ impl<D> Wallet<D> {
.graph() .graph()
.filter_chain_unspents( .filter_chain_unspents(
&self.chain, &self.chain,
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), self.chain.tip().block_id(),
core::iter::once((spk_i, op)), core::iter::once((spk_i, op)),
) )
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
@ -669,7 +725,7 @@ impl<D> Wallet<D> {
Some(CanonicalTx { Some(CanonicalTx {
chain_position: graph.get_chain_position( chain_position: graph.get_chain_position(
&self.chain, &self.chain,
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), self.chain.tip().block_id(),
txid, txid,
)?, )?,
tx_node: graph.get_tx_node(txid)?, tx_node: graph.get_tx_node(txid)?,
@ -686,7 +742,7 @@ impl<D> Wallet<D> {
pub fn insert_checkpoint( pub fn insert_checkpoint(
&mut self, &mut self,
block_id: BlockId, block_id: BlockId,
) -> Result<bool, local_chain::InsertBlockError> ) -> Result<bool, local_chain::AlterCheckPointError>
where where
D: PersistBackend<ChangeSet>, D: PersistBackend<ChangeSet>,
{ {
@ -730,7 +786,7 @@ impl<D> Wallet<D> {
.range(height..) .range(height..)
.next() .next()
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height: self.chain.tip().map(|b| b.height()), tip_height: self.chain.tip().height(),
tx_height: height, tx_height: height,
}) })
.map(|(&anchor_height, &hash)| ConfirmationTimeHeightAnchor { .map(|(&anchor_height, &hash)| ConfirmationTimeHeightAnchor {
@ -766,10 +822,9 @@ impl<D> Wallet<D> {
pub fn transactions( pub fn transactions(
&self, &self,
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> + '_ { ) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> + '_ {
self.indexed_graph.graph().list_chain_txs( self.indexed_graph
&self.chain, .graph()
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), .list_chain_txs(&self.chain, self.chain.tip().block_id())
)
} }
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// 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 { pub fn get_balance(&self) -> Balance {
self.indexed_graph.graph().balance( self.indexed_graph.graph().balance(
&self.chain, &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(), self.indexed_graph.index.outpoints().iter().cloned(),
|&(k, _), _| k == KeychainKind::Internal, |&(k, _), _| k == KeychainKind::Internal,
) )
@ -945,14 +1000,14 @@ impl<D> Wallet<D> {
_ => 1, _ => 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 { let current_height = match params.current_height {
// If they didn't tell us the current height, we assume it's the latest sync height. // If they didn't tell us the current height, we assume it's the latest sync height.
None => self None => {
.chain let tip_height = self.chain.tip().height();
.tip() absolute::LockTime::from_height(tip_height).expect("invalid height")
.map(|cp| absolute::LockTime::from_height(cp.height()).expect("Invalid height")), }
h => h, Some(h) => h,
}; };
let lock_time = match params.locktime { let lock_time = match params.locktime {
@ -961,7 +1016,7 @@ impl<D> Wallet<D> {
// Fee sniping can be partially prevented by setting the timelock // Fee sniping can be partially prevented by setting the timelock
// to current_height. If we don't know the current_height, // to current_height. If we don't know the current_height,
// we default to 0. // 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 // We choose the biggest between the required nlocktime and the fee sniping
// height // height
@ -1115,7 +1170,7 @@ impl<D> Wallet<D> {
params.drain_wallet, params.drain_wallet,
params.manually_selected_only, params.manually_selected_only,
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee 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 // get drain script
@ -1257,7 +1312,7 @@ impl<D> Wallet<D> {
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> { ) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
let graph = self.indexed_graph.graph(); let graph = self.indexed_graph.graph();
let txout_index = &self.indexed_graph.index; 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 let mut tx = graph
.get_tx(txid) .get_tx(txid)
@ -1492,7 +1547,7 @@ impl<D> Wallet<D> {
psbt: &mut psbt::PartiallySignedTransaction, psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions, sign_options: SignOptions,
) -> Result<bool, Error> { ) -> 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 tx = &psbt.unsigned_tx;
let mut finished = true; let mut finished = true;
@ -1515,7 +1570,7 @@ impl<D> Wallet<D> {
}); });
let current_height = sign_options let current_height = sign_options
.assume_height .assume_height
.or(self.chain.tip().map(|b| b.height())); .unwrap_or_else(|| self.chain.tip().height());
debug!( debug!(
"Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}", "Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
@ -1552,8 +1607,8 @@ impl<D> Wallet<D> {
&mut tmp_input, &mut tmp_input,
( (
PsbtInputSatisfier::new(psbt, n), PsbtInputSatisfier::new(psbt, n),
After::new(current_height, false), After::new(Some(current_height), false),
Older::new(current_height, confirmation_height, false), Older::new(Some(current_height), confirmation_height, false),
), ),
) { ) {
Ok(_) => { Ok(_) => {
@ -1661,7 +1716,7 @@ impl<D> Wallet<D> {
must_only_use_confirmed_tx: bool, must_only_use_confirmed_tx: bool,
current_height: Option<u32>, current_height: Option<u32>,
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) { ) -> (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 // must_spend <- manually selected utxos
// may_spend <- all other available utxos // may_spend <- all other available utxos
let mut may_spend = self.get_available_utxos(); let mut may_spend = self.get_available_utxos();

View File

@ -42,14 +42,14 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->
} }
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
let height = match wallet.latest_checkpoint() { let latest_cp = wallet.latest_checkpoint();
Some(cp) => ConfirmationTime::Confirmed { let height = latest_cp.height();
height: cp.height(), let anchor = if height == 0 {
time: 0, ConfirmationTime::Unconfirmed { last_seen: 0 }
}, } else {
None => ConfirmationTime::Unconfirmed { last_seen: 0 }, ConfirmationTime::Confirmed { height, time: 0 }
}; };
receive_output(wallet, value, height) receive_output(wallet, value, anchor)
} }
// The satisfaction size of a P2WPKH is 112 WU = // 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 // If there's no current_height we're left with using the last sync height
assert_eq!( assert_eq!(
psbt.unsigned_tx.lock_time.to_consensus_u32(), 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( .insert_tx(
tx.clone(), tx.clone(),
ConfirmationTime::Confirmed { ConfirmationTime::Confirmed {
height: wallet.latest_checkpoint().unwrap().height(), height: wallet.latest_checkpoint().height(),
time: 42_000, time: 42_000,
}, },
) )

View File

@ -25,7 +25,7 @@ pub struct Emitter<'c, C> {
/// The checkpoint of the last-emitted block that is in the best chain. If it is later found /// 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. /// 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 /// 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` /// 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> { impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
/// Construct a new [`Emitter`] with the given RPC `client` and `start_height`. /// TODO
/// pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
/// `start_height` is the block height to start emitting blocks from.
pub fn from_height(client: &'c C, start_height: u32) -> Self {
Self { Self {
client, client,
start_height, start_height,
last_cp: None, last_cp,
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_block: None, last_block: None,
last_mempool_time: 0, last_mempool_time: 0,
last_mempool_tip: None, last_mempool_tip: None,
@ -134,7 +117,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
self.last_mempool_time = latest_time; 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) Ok(txs_to_emit)
} }
@ -156,7 +139,8 @@ enum PollResponse {
/// Fetched block is not in the best chain. /// Fetched block is not in the best chain.
BlockNotInBestChain, BlockNotInBestChain,
AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint), 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> fn poll_once<C>(emitter: &Emitter<C>) -> Result<PollResponse, bitcoincore_rpc::Error>
@ -166,45 +150,50 @@ where
let client = emitter.client; let client = emitter.client;
if let Some(last_res) = &emitter.last_block { if let Some(last_res) = &emitter.last_block {
assert!( let next_hash = if last_res.height < emitter.start_height as _ {
emitter.last_cp.is_some(), // enforce start height
"must not have block result without last cp" 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 {
let next_hash = match last_res.nextblockhash { return Ok(PollResponse::BlockNotInBestChain);
None => return Ok(PollResponse::NoMoreBlocks), }
Some(next_hash) => next_hash, 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)?; let res = client.get_block_info(&next_hash)?;
if res.confirmations < 0 { if res.confirmations < 0 {
return Ok(PollResponse::BlockNotInBestChain); return Ok(PollResponse::BlockNotInBestChain);
} }
return Ok(PollResponse::Block(res)); return Ok(PollResponse::Block(res));
} }
if emitter.last_cp.is_none() { for cp in emitter.last_cp.iter() {
let hash = client.get_block_hash(emitter.start_height as _)?; let res = match client.get_block_info(&cp.hash()) {
// block not in best chain
let res = client.get_block_info(&hash)?; Ok(res) if res.confirmations < 0 => continue,
if res.confirmations < 0 { Ok(res) => res,
return Ok(PollResponse::BlockNotInBestChain); Err(e) if e.is_not_found_error() => {
} if cp.height() > 0 {
return Ok(PollResponse::Block(res)); continue;
} }
// if we can't find genesis block, we can't create an update that connects
for cp in emitter.last_cp.iter().flat_map(CheckPoint::iter) { break;
let res = client.get_block_info(&cp.hash())?; }
if res.confirmations < 0 { Err(e) => return Err(e),
// block is not in best chain };
continue;
}
// agreement point found // agreement point found
return Ok(PollResponse::AgreementFound(res, cp)); 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>( fn poll<C, V, F>(
@ -222,25 +211,12 @@ where
let hash = res.hash; let hash = res.hash;
let item = get_item(&hash)?; let item = get_item(&hash)?;
let this_id = BlockId { height, hash }; emitter.last_cp = emitter
let prev_id = res.previousblockhash.map(|prev_hash| BlockId { .last_cp
height: height - 1, .clone()
hash: prev_hash, .push(BlockId { height, hash })
}); .expect("must push");
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_block = Some(res); emitter.last_block = Some(res);
return Ok(Some((height, item))); return Ok(Some((height, item)));
} }
PollResponse::NoMoreBlocks => { PollResponse::NoMoreBlocks => {
@ -254,9 +230,6 @@ where
PollResponse::AgreementFound(res, cp) => { PollResponse::AgreementFound(res, cp) => {
let agreement_h = res.height as u32; 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 // The tip during the last mempool emission needs to in the best chain, we reduce
// it if it is not. // it if it is not.
if let Some(h) = emitter.last_mempool_tip.as_mut() { if let Some(h) = emitter.last_mempool_tip.as_mut() {
@ -264,15 +237,17 @@ where
*h = agreement_h; *h = agreement_h;
} }
} }
// get rid of evicted blocks
emitter.last_cp = cp;
emitter.last_block = Some(res); emitter.last_block = Some(res);
continue; continue;
} }
PollResponse::AgreementPointNotFound => { PollResponse::AgreementPointNotFound(genesis_hash) => {
// We want to clear `last_cp` and set `start_height` to the first checkpoint's emitter.last_cp = CheckPoint::new(BlockId {
// height. This way, the first checkpoint in `LocalChain` can be replaced. height: 0,
if let Some(last_cp) = emitter.last_cp.take() { hash: genesis_hash,
emitter.start_height = last_cp.height(); });
}
emitter.last_block = None; emitter.last_block = None;
continue; continue;
} }

View File

@ -188,8 +188,8 @@ fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Up
#[test] #[test]
pub fn test_sync_local_chain() -> anyhow::Result<()> { pub fn test_sync_local_chain() -> anyhow::Result<()> {
let env = TestEnv::new()?; let env = TestEnv::new()?;
let mut local_chain = LocalChain::default(); let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
let mut emitter = Emitter::from_height(&env.client, 0); let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
// mine some blocks and returned the actual block hashes // mine some blocks and returned the actual block hashes
let exp_hashes = { let exp_hashes = {
@ -296,7 +296,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
env.mine_blocks(101, None)?; env.mine_blocks(101, None)?;
println!("mined blocks!"); 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 indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
let mut index = SpkTxOutIndex::<usize>::default(); let mut index = SpkTxOutIndex::<usize>::default();
index.insert_spk(0, addr_0.script_pubkey()); index.insert_spk(0, addr_0.script_pubkey());
@ -305,7 +305,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
index 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()? { while let Some((height, block)) = emitter.next_block()? {
let _ = chain.apply_update(block_to_chain_update(&block, height))?; 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; const CHAIN_TIP_HEIGHT: usize = 110;
let env = TestEnv::new()?; 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)?; env.mine_blocks(CHAIN_TIP_HEIGHT, None)?;
while emitter.next_header()?.is_some() {} while emitter.next_header()?.is_some() {}
@ -442,9 +449,7 @@ fn get_balance(
recv_chain: &LocalChain, recv_chain: &LocalChain,
recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>, recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
) -> anyhow::Result<Balance> { ) -> anyhow::Result<Balance> {
let chain_tip = recv_chain let chain_tip = recv_chain.tip().block_id();
.tip()
.map_or(BlockId::default(), |cp| cp.block_id());
let outpoints = recv_graph.index.outpoints().clone(); let outpoints = recv_graph.index.outpoints().clone();
let balance = recv_graph let balance = recv_graph
.graph() .graph()
@ -461,7 +466,14 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
const SEND_AMOUNT: Amount = Amount::from_sat(10_000); const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
let env = TestEnv::new()?; 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 // setup addresses
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked(); 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)?; let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
// setup receiver // 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_graph = IndexedTxGraph::<BlockId, _>::new({
let mut recv_index = SpkTxOutIndex::default(); let mut recv_index = SpkTxOutIndex::default();
recv_index.insert_spk((), spk_to_track.clone()); 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; const MEMPOOL_TX_COUNT: usize = 2;
let env = TestEnv::new()?; 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 // mine blocks and sync up emitter
let addr = env.client.get_new_address(None, None)?.assume_checked(); 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; const MEMPOOL_TX_COUNT: usize = 21;
let env = TestEnv::new()?; 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 // mine blocks to get initial balance, sync emitter up to tip
let addr = env.client.get_new_address(None, None)?.assume_checked(); 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; const PREMINE_COUNT: usize = 101;
let env = TestEnv::new()?; 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 // mine blocks to get initial balance
let addr = env.client.get_new_address(None, None)?.assume_checked(); 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()?; let env = TestEnv::new()?;
// start height is 99 // 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 // mine 101 blocks
env.mine_blocks(PREMINE_COUNT, None)?; env.mine_blocks(PREMINE_COUNT, None)?;

View File

@ -21,5 +21,5 @@ pub trait ChainOracle {
) -> Result<Option<bool>, Self::Error>; ) -> Result<Option<bool>, Self::Error>;
/// Get the best chain's chain tip. /// 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>;
} }

View File

@ -179,9 +179,9 @@ pub struct Update {
} }
/// This is a local implementation of [`ChainOracle`]. /// This is a local implementation of [`ChainOracle`].
#[derive(Debug, Default, Clone)] #[derive(Debug, Clone)]
pub struct LocalChain { pub struct LocalChain {
tip: Option<CheckPoint>, tip: CheckPoint,
index: BTreeMap<u32, BlockHash>, 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 { impl ChainOracle for LocalChain {
type Error = Infallible; type Error = Infallible;
@ -225,39 +219,71 @@ impl ChainOracle for LocalChain {
) )
} }
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> { fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
Ok(self.tip.as_ref().map(|tip| tip.block_id())) Ok(self.tip.block_id())
} }
} }
impl LocalChain { 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`. /// Construct a [`LocalChain`] from an initial `changeset`.
pub fn from_changeset(changeset: ChangeSet) -> Self { pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
let mut chain = Self::default(); let genesis_entry = changeset.get(&0).copied().flatten();
chain.apply_changeset(&changeset); 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_index_is_consistent_with_tip());
debug_assert!(chain._check_changeset_is_applied(&changeset)); debug_assert!(chain._check_changeset_is_applied(&changeset));
chain Ok(chain)
} }
/// Construct a [`LocalChain`] from a given `checkpoint` tip. /// 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 { let mut chain = Self {
tip: Some(tip), tip,
..Default::default() index: BTreeMap::new(),
}; };
chain.reindex(0); chain.reindex(0);
if chain.index.get(&0).copied().is_none() {
return Err(MissingGenesisError);
}
debug_assert!(chain._check_index_is_consistent_with_tip()); debug_assert!(chain._check_index_is_consistent_with_tip());
chain Ok(chain)
} }
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`]. /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
/// ///
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
/// all of the same chain. /// 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; let mut tip: Option<CheckPoint> = None;
for block in &blocks { 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()); debug_assert!(chain._check_index_is_consistent_with_tip());
Ok(chain)
chain
} }
/// Get the highest checkpoint. /// Get the highest checkpoint.
pub fn tip(&self) -> Option<CheckPoint> { pub fn tip(&self) -> CheckPoint {
self.tip.clone() 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. /// Applies the given `update` to the chain.
/// ///
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`. /// 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 /// [module-level documentation]: crate::local_chain
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> { pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
match self.tip() { let changeset = merge_chains(
Some(original_tip) => { self.tip.clone(),
let changeset = merge_chains( update.tip.clone(),
original_tip, update.introduce_older_blocks,
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); self.apply_changeset(&changeset)
.map_err(|_| CannotConnectError {
// return early as `apply_changeset` already calls `check_consistency` try_include_height: 0,
Ok(changeset) })?;
} 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)
}
}
} }
/// Apply the given `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() { if let Some(start_height) = changeset.keys().next().cloned() {
// changes after point of agreement
let mut extension = BTreeMap::default(); let mut extension = BTreeMap::default();
// point of agreement
let mut base: Option<CheckPoint> = None; let mut base: Option<CheckPoint> = None;
for cp in self.iter_checkpoints() { for cp in self.iter_checkpoints() {
if cp.height() >= start_height { if cp.height() >= start_height {
extension.insert(cp.height(), cp.hash()); extension.insert(cp.height(), cp.hash());
@ -359,12 +374,12 @@ impl LocalChain {
} }
}; };
} }
let new_tip = match base { let new_tip = match base {
Some(base) => Some( Some(base) => base
base.extend(extension.into_iter().map(BlockId::from)) .extend(extension.into_iter().map(BlockId::from))
.expect("extension is strictly greater than base"), .expect("extension is strictly greater than base"),
), None => LocalChain::from_blocks(extension)?.tip(),
None => LocalChain::from_blocks(extension).tip(),
}; };
self.tip = new_tip; self.tip = new_tip;
self.reindex(start_height); self.reindex(start_height);
@ -372,6 +387,8 @@ impl LocalChain {
debug_assert!(self._check_index_is_consistent_with_tip()); debug_assert!(self._check_index_is_consistent_with_tip());
debug_assert!(self._check_changeset_is_applied(changeset)); debug_assert!(self._check_changeset_is_applied(changeset));
} }
Ok(())
} }
/// Insert a [`BlockId`]. /// Insert a [`BlockId`].
@ -379,13 +396,13 @@ impl LocalChain {
/// # Errors /// # Errors
/// ///
/// Replacing the block hash of an existing checkpoint will result in an error. /// 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 let Some(&original_hash) = self.index.get(&block_id.height) {
if original_hash != block_id.hash { if original_hash != block_id.hash {
return Err(InsertBlockError { return Err(AlterCheckPointError {
height: block_id.height, height: block_id.height,
original_hash, original_hash,
update_hash: block_id.hash, update_hash: Some(block_id.hash),
}); });
} else { } else {
return Ok(ChangeSet::default()); return Ok(ChangeSet::default());
@ -394,7 +411,12 @@ impl LocalChain {
let mut changeset = ChangeSet::default(); let mut changeset = ChangeSet::default();
changeset.insert(block_id.height, Some(block_id.hash)); 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) Ok(changeset)
} }
@ -418,7 +440,7 @@ impl LocalChain {
/// Iterate over checkpoints in descending height order. /// Iterate over checkpoints in descending height order.
pub fn iter_checkpoints(&self) -> CheckPointIter { pub fn iter_checkpoints(&self) -> CheckPointIter {
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 let tip_history = self
.tip .tip
.iter() .iter()
.flat_map(CheckPoint::iter)
.map(|cp| (cp.height(), cp.hash())) .map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
self.index == tip_history 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)] #[derive(Clone, Debug, PartialEq)]
pub struct InsertBlockError { pub struct MissingGenesisError;
/// The checkpoints' height.
pub height: u32,
/// Original checkpoint's block hash.
pub original_hash: BlockHash,
/// Update checkpoint's block hash.
pub update_hash: BlockHash,
}
impl core::fmt::Display for InsertBlockError { impl core::fmt::Display for MissingGenesisError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!( write!(
f, f,
"failed to insert block at height {} as block hashes conflict: original={}, update={}", "cannot construct `LocalChain` without a genesis checkpoint"
self.height, self.original_hash, self.update_hash
) )
} }
} }
#[cfg(feature = "std")] #[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. /// Occurs when an update does not have a common checkpoint with the original chain.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View File

@ -23,6 +23,7 @@ macro_rules! local_chain {
[ $(($height:expr, $block_hash:expr)), * ] => {{ [ $(($height:expr, $block_hash:expr)), * ] => {{
#[allow(unused_mut)] #[allow(unused_mut)]
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect()) 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)] #[allow(unused_mut)]
bdk_chain::local_chain::Update { bdk_chain::local_chain::Update {
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect()) tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
.tip() .expect("chain must have genesis block")
.expect("must have tip"), .tip(),
introduce_older_blocks: true, introduce_older_blocks: true,
} }
}}; }};

View File

@ -1,7 +1,7 @@
#[macro_use] #[macro_use]
mod common; mod common;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeSet;
use bdk_chain::{ use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph}, indexed_tx_graph::{self, IndexedTxGraph},
@ -9,9 +9,7 @@ use bdk_chain::{
local_chain::LocalChain, local_chain::LocalChain,
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor, tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
}; };
use bitcoin::{ use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
};
use miniscript::Descriptor; use miniscript::Descriptor;
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
@ -112,11 +110,8 @@ fn insert_relevant_txs() {
fn test_list_owned_txouts() { fn test_list_owned_txouts() {
// Create Local chains // Create Local chains
let local_chain = LocalChain::from( let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
(0..150) .expect("must have genesis hash");
.map(|i| (i as u32, h!("random")))
.collect::<BTreeMap<u32, BlockHash>>(),
);
// Initiate IndexedTxGraph // Initiate IndexedTxGraph

View File

@ -1,4 +1,6 @@
use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update}; use bdk_chain::local_chain::{
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
};
use bitcoin::BlockHash; use bitcoin::BlockHash;
#[macro_use] #[macro_use]
@ -68,10 +70,10 @@ fn update_local_chain() {
[ [
TestLocalChain { TestLocalChain {
name: "add first tip", name: "add first tip",
chain: local_chain![], chain: local_chain![(0, h!("A"))],
update: chain_update![(0, h!("A"))], update: chain_update![(0, h!("A"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(0, Some(h!("A")))], changeset: &[],
init_changeset: &[(0, Some(h!("A")))], init_changeset: &[(0, Some(h!("A")))],
}, },
}, },
@ -86,18 +88,18 @@ fn update_local_chain() {
}, },
TestLocalChain { TestLocalChain {
name: "two disjoint chains cannot merge", name: "two disjoint chains cannot merge",
chain: local_chain![(0, h!("A"))], chain: local_chain![(0, h!("_")), (1, h!("A"))],
update: chain_update![(1, h!("B"))], update: chain_update![(0, h!("_")), (2, h!("B"))],
exp: ExpectedResult::Err(CannotConnectError { exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 0, try_include_height: 1,
}), }),
}, },
TestLocalChain { TestLocalChain {
name: "two disjoint chains cannot merge (existing chain longer)", name: "two disjoint chains cannot merge (existing chain longer)",
chain: local_chain![(1, h!("A"))], chain: local_chain![(0, h!("_")), (2, h!("A"))],
update: chain_update![(0, h!("B"))], update: chain_update![(0, h!("_")), (1, h!("B"))],
exp: ExpectedResult::Err(CannotConnectError { exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 1, try_include_height: 2,
}), }),
}, },
TestLocalChain { TestLocalChain {
@ -111,54 +113,54 @@ fn update_local_chain() {
}, },
// Introduce an older checkpoint (B) // Introduce an older checkpoint (B)
// | 0 | 1 | 2 | 3 // | 0 | 1 | 2 | 3
// chain | C D // chain | _ C D
// update | B C // update | _ B C
TestLocalChain { TestLocalChain {
name: "can introduce older checkpoint", name: "can introduce older checkpoint",
chain: local_chain![(2, h!("C")), (3, h!("D"))], chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D"))],
update: chain_update![(1, h!("B")), (2, h!("C"))], update: chain_update![(0, h!("_")), (1, h!("B")), (2, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(1, Some(h!("B")))], 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 // Introduce an older checkpoint (A) that is not directly behind PoA
// | 2 | 3 | 4 // | 0 | 2 | 3 | 4
// chain | B C // chain | _ B C
// update | A C // update | _ A C
TestLocalChain { TestLocalChain {
name: "can introduce older checkpoint 2", name: "can introduce older checkpoint 2",
chain: local_chain![(3, h!("B")), (4, h!("C"))], chain: local_chain![(0, h!("_")), (3, h!("B")), (4, h!("C"))],
update: chain_update![(2, h!("A")), (4, h!("C"))], update: chain_update![(0, h!("_")), (2, h!("A")), (4, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(2, Some(h!("A")))], 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 // Introduce an older checkpoint (B) that is not the oldest checkpoint
// | 1 | 2 | 3 // | 0 | 1 | 2 | 3
// chain | A C // chain | _ A C
// update | B C // update | _ B C
TestLocalChain { TestLocalChain {
name: "can introduce older checkpoint 3", name: "can introduce older checkpoint 3",
chain: local_chain![(1, h!("A")), (3, h!("C"))], chain: local_chain![(0, h!("_")), (1, h!("A")), (3, h!("C"))],
update: chain_update![(2, h!("B")), (3, h!("C"))], update: chain_update![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(2, Some(h!("B")))], 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 // Introduce two older checkpoints below the PoA
// | 1 | 2 | 3 // | 0 | 1 | 2 | 3
// chain | C // chain | _ C
// update | A B C // update | _ A B C
TestLocalChain { TestLocalChain {
name: "introduce two older checkpoints below PoA", name: "introduce two older checkpoints below PoA",
chain: local_chain![(3, h!("C"))], chain: local_chain![(0, h!("_")), (3, h!("C"))],
update: chain_update![(1, h!("A")), (2, h!("B")), (3, h!("C"))], update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))], 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 { TestLocalChain {
@ -172,45 +174,46 @@ fn update_local_chain() {
}, },
// B and C are in both chain and update // B and C are in both chain and update
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4
// chain | B C // chain | _ B C
// update | A B C D // update | _ A B C D
// This should succeed with the point of agreement being C and A should be added in addition. // This should succeed with the point of agreement being C and A should be added in addition.
TestLocalChain { TestLocalChain {
name: "two points of agreement", name: "two points of agreement",
chain: local_chain![(1, h!("B")), (2, h!("C"))], chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
update: chain_update![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))], update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[(0, Some(h!("A"))), (3, Some(h!("D")))], changeset: &[(1, Some(h!("A"))), (4, Some(h!("D")))],
init_changeset: &[ init_changeset: &[
(0, Some(h!("A"))), (0, Some(h!("_"))),
(1, Some(h!("B"))), (1, Some(h!("A"))),
(2, Some(h!("C"))), (2, Some(h!("B"))),
(3, Some(h!("D"))), (3, Some(h!("C"))),
(4, Some(h!("D"))),
], ],
}, },
}, },
// Update and chain does not connect: // Update and chain does not connect:
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4
// chain | B C // chain | _ B C
// update | A B D // update | _ A B D
// This should fail as we cannot figure out whether C & D are on the same chain // This should fail as we cannot figure out whether C & D are on the same chain
TestLocalChain { TestLocalChain {
name: "update and chain does not connect", name: "update and chain does not connect",
chain: local_chain![(1, h!("B")), (2, h!("C"))], chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
update: chain_update![(0, h!("A")), (1, h!("B")), (3, h!("D"))], update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (4, h!("D"))],
exp: ExpectedResult::Err(CannotConnectError { exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 2, try_include_height: 3,
}), }),
}, },
// Transient invalidation: // Transient invalidation:
// | 0 | 1 | 2 | 3 | 4 | 5 // | 0 | 1 | 2 | 3 | 4 | 5
// chain | A B C E // chain | _ B C E
// update | A B' C' D // update | _ B' C' D
// This should succeed and invalidate B,C and E with point of agreement being A. // This should succeed and invalidate B,C and E with point of agreement being A.
TestLocalChain { TestLocalChain {
name: "transitive invalidation applies to checkpoints higher than invalidation", name: "transitive invalidation applies to checkpoints higher than invalidation",
chain: local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))], chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
update: chain_update![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))], update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[ changeset: &[
(2, Some(h!("B'"))), (2, Some(h!("B'"))),
@ -219,7 +222,7 @@ fn update_local_chain() {
(5, None), (5, None),
], ],
init_changeset: &[ init_changeset: &[
(0, Some(h!("A"))), (0, Some(h!("_"))),
(2, Some(h!("B'"))), (2, Some(h!("B'"))),
(3, Some(h!("C'"))), (3, Some(h!("C'"))),
(4, Some(h!("D"))), (4, Some(h!("D"))),
@ -228,13 +231,13 @@ fn update_local_chain() {
}, },
// Transient invalidation: // Transient invalidation:
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4
// chain | B C E // chain | _ B C E
// update | B' C' D // update | _ B' C' D
// This should succeed and invalidate B, C and E with no point of agreement // This should succeed and invalidate B, C and E with no point of agreement
TestLocalChain { TestLocalChain {
name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement", 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"))], chain: local_chain![(0, h!("_")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))], update: chain_update![(0, h!("_")), (1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
exp: ExpectedResult::Ok { exp: ExpectedResult::Ok {
changeset: &[ changeset: &[
(1, Some(h!("B'"))), (1, Some(h!("B'"))),
@ -243,6 +246,7 @@ fn update_local_chain() {
(4, None) (4, None)
], ],
init_changeset: &[ init_changeset: &[
(0, Some(h!("_"))),
(1, Some(h!("B'"))), (1, Some(h!("B'"))),
(2, Some(h!("C'"))), (2, Some(h!("C'"))),
(3, Some(h!("D"))), (3, Some(h!("D"))),
@ -250,16 +254,16 @@ fn update_local_chain() {
}, },
}, },
// Transient invalidation: // Transient invalidation:
// | 0 | 1 | 2 | 3 | 4 // | 0 | 1 | 2 | 3 | 4 | 5
// chain | A B C E // chain | _ A B C E
// update | B' C' D // update | _ B' C' D
// This should fail since although it tells us that B and C are invalid it doesn't tell us whether // This should fail since although it tells us that B and C are invalid it doesn't tell us whether
// A was invalid. // A was invalid.
TestLocalChain { TestLocalChain {
name: "invalidation but no connection", name: "invalidation but no connection",
chain: local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))], chain: local_chain![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))], update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 0 }), exp: ExpectedResult::Err(CannotConnectError { try_include_height: 1 }),
}, },
// Introduce blocks between two points of agreement // Introduce blocks between two points of agreement
// | 0 | 1 | 2 | 3 | 4 | 5 // | 0 | 1 | 2 | 3 | 4 | 5
@ -294,44 +298,44 @@ fn local_chain_insert_block() {
struct TestCase { struct TestCase {
original: LocalChain, original: LocalChain,
insert: (u32, BlockHash), insert: (u32, BlockHash),
expected_result: Result<ChangeSet, InsertBlockError>, expected_result: Result<ChangeSet, AlterCheckPointError>,
expected_final: LocalChain, expected_final: LocalChain,
} }
let test_cases = [ let test_cases = [
TestCase { TestCase {
original: local_chain![], original: local_chain![(0, h!("_"))],
insert: (5, h!("block5")), insert: (5, h!("block5")),
expected_result: Ok([(5, Some(h!("block5")))].into()), 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 { TestCase {
original: local_chain![(3, h!("A"))], original: local_chain![(0, h!("_")), (3, h!("A"))],
insert: (4, h!("B")), insert: (4, h!("B")),
expected_result: Ok([(4, Some(h!("B")))].into()), 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 { TestCase {
original: local_chain![(4, h!("B"))], original: local_chain![(0, h!("_")), (4, h!("B"))],
insert: (3, h!("A")), insert: (3, h!("A")),
expected_result: Ok([(3, Some(h!("A")))].into()), 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 { TestCase {
original: local_chain![(2, h!("K"))], original: local_chain![(0, h!("_")), (2, h!("K"))],
insert: (2, h!("K")), insert: (2, h!("K")),
expected_result: Ok([].into()), expected_result: Ok([].into()),
expected_final: local_chain![(2, h!("K"))], expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
}, },
TestCase { TestCase {
original: local_chain![(2, h!("K"))], original: local_chain![(0, h!("_")), (2, h!("K"))],
insert: (2, h!("J")), insert: (2, h!("J")),
expected_result: Err(InsertBlockError { expected_result: Err(AlterCheckPointError {
height: 2, height: 2,
original_hash: h!("K"), 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"))],
}, },
]; ];

View File

@ -511,11 +511,13 @@ fn test_calculate_fee_on_coinbase() {
// where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc. // where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc.
#[test] #[test]
fn test_walk_ancestors() { fn test_walk_ancestors() {
let local_chain: LocalChain = (0..=20) let local_chain = LocalChain::from_blocks(
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) (0..=20)
.collect::<BTreeMap<u32, BlockHash>>() .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.into(); .collect(),
let tip = local_chain.tip().expect("must have tip"); )
.expect("must contain genesis hash");
let tip = local_chain.tip();
let tx_a0 = Transaction { let tx_a0 = Transaction {
input: vec![TxIn { input: vec![TxIn {
@ -839,11 +841,13 @@ fn test_descendants_no_repeat() {
#[test] #[test]
fn test_chain_spends() { fn test_chain_spends() {
let local_chain: LocalChain = (0..=100) let local_chain = LocalChain::from_blocks(
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) (0..=100)
.collect::<BTreeMap<u32, BlockHash>>() .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.into(); .collect(),
let tip = local_chain.tip().expect("must have tip"); )
.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 contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
// The parent tx is confirmed at block 95. // The parent tx is confirmed at block 95.
@ -1078,7 +1082,7 @@ fn test_missing_blocks() {
g g
}, },
chain: { chain: {
let mut c = LocalChain::default(); let (mut c, _) = LocalChain::from_genesis_hash(h!("genesis"));
for (height, hash) in chain { for (height, hash) in chain {
let _ = c.insert_block(BlockId { let _ = c.insert_block(BlockId {
height: *height, height: *height,

View File

@ -39,10 +39,7 @@ fn test_tx_conflict_handling() {
(5, h!("F")), (5, h!("F")),
(6, h!("G")) (6, h!("G"))
); );
let chain_tip = local_chain let chain_tip = local_chain.tip().block_id();
.tip()
.map(|cp| cp.block_id())
.unwrap_or_default();
let scenarios = [ let scenarios = [
Scenario { Scenario {

View File

@ -148,7 +148,7 @@ pub trait ElectrumExt {
/// single batch request. /// single batch request.
fn scan<K: Ord + Clone>( fn scan<K: Ord + Clone>(
&self, &self,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>, keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
@ -161,7 +161,7 @@ pub trait ElectrumExt {
/// [`scan`]: ElectrumExt::scan /// [`scan`]: ElectrumExt::scan
fn scan_without_keychain( fn scan_without_keychain(
&self, &self,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
misc_spks: impl IntoIterator<Item = ScriptBuf>, misc_spks: impl IntoIterator<Item = ScriptBuf>,
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>, outpoints: impl IntoIterator<Item = OutPoint>,
@ -188,7 +188,7 @@ pub trait ElectrumExt {
impl ElectrumExt for Client { impl ElectrumExt for Client {
fn scan<K: Ord + Clone>( fn scan<K: Ord + Clone>(
&self, &self,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>, keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
txids: impl IntoIterator<Item = Txid>, txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>, 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`. /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
fn construct_update_tip( fn construct_update_tip(
client: &Client, client: &Client,
prev_tip: Option<CheckPoint>, prev_tip: CheckPoint,
) -> Result<(CheckPoint, Option<u32>), Error> { ) -> Result<(CheckPoint, Option<u32>), Error> {
let HeaderNotification { height, .. } = client.block_headers_subscribe()?; let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
let new_tip_height = height as u32; let new_tip_height = height as u32;
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do // 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. // 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() {
if new_tip_height < prev_tip.height() { return Ok((prev_tip.clone(), Some(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 // 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). // Find the "point of agreement" (if any).
let agreement_cp = { let agreement_cp = {
let mut agreement_cp = Option::<CheckPoint>::None; 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 cp_block = cp.block_id();
let hash = match new_blocks.get(&cp_block.height) { let hash = match new_blocks.get(&cp_block.height) {
Some(&hash) => hash, Some(&hash) => hash,

View File

@ -32,7 +32,7 @@ pub trait EsploraAsyncExt {
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
async fn update_local_chain( async fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send, request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
) -> Result<local_chain::Update, Error>; ) -> Result<local_chain::Update, Error>;
@ -95,7 +95,7 @@ pub trait EsploraAsyncExt {
impl EsploraAsyncExt for esplora_client::AsyncClient { impl EsploraAsyncExt for esplora_client::AsyncClient {
async fn update_local_chain( async fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send, request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
) -> Result<local_chain::Update, Error> { ) -> Result<local_chain::Update, Error> {
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>(); let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
@ -129,41 +129,39 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
let earliest_agreement_cp = { let earliest_agreement_cp = {
let mut earliest_agreement_cp = Option::<CheckPoint>::None; let mut earliest_agreement_cp = Option::<CheckPoint>::None;
if let Some(local_tip) = local_tip { let local_tip_height = local_tip.height();
let local_tip_height = local_tip.height(); for local_cp in local_tip.iter() {
for local_cp in local_tip.iter() { let local_block = local_cp.block_id();
let local_block = local_cp.block_id();
// the updated hash (block hash at this height after the update), can either be: // the updated hash (block hash at this height after the update), can either be:
// 1. a block that already existed in `fetched_blocks` // 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 // 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 // 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 // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
// remote tip // remote tip
let updated_hash = match fetched_blocks.entry(local_block.height) { let updated_hash = match fetched_blocks.entry(local_block.height) {
btree_map::Entry::Occupied(entry) => *entry.get(), btree_map::Entry::Occupied(entry) => *entry.get(),
btree_map::Entry::Vacant(entry) => *entry.insert( btree_map::Entry::Vacant(entry) => *entry.insert(
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
local_block.hash local_block.hash
} else { } else {
self.get_block_hash(local_block.height).await? self.get_block_hash(local_block.height).await?
}, },
), ),
}; };
// since we may introduce blocks below the point of agreement, we cannot break // 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 // here unconditionally - we only break if we guarantee there are no new heights
// below our current local checkpoint // below our current local checkpoint
if local_block.hash == updated_hash { if local_block.hash == updated_hash {
earliest_agreement_cp = Some(local_cp); earliest_agreement_cp = Some(local_cp);
let first_new_height = *fetched_blocks let first_new_height = *fetched_blocks
.keys() .keys()
.next() .next()
.expect("must have at least one new block"); .expect("must have at least one new block");
if first_new_height >= local_block.height { if first_new_height >= local_block.height {
break; break;
}
} }
} }
} }

View File

@ -30,7 +30,7 @@ pub trait EsploraExt {
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
fn update_local_chain( fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<Item = u32>, request_heights: impl IntoIterator<Item = u32>,
) -> Result<local_chain::Update, Error>; ) -> Result<local_chain::Update, Error>;
@ -87,7 +87,7 @@ pub trait EsploraExt {
impl EsploraExt for esplora_client::BlockingClient { impl EsploraExt for esplora_client::BlockingClient {
fn update_local_chain( fn update_local_chain(
&self, &self,
local_tip: Option<CheckPoint>, local_tip: CheckPoint,
request_heights: impl IntoIterator<Item = u32>, request_heights: impl IntoIterator<Item = u32>,
) -> Result<local_chain::Update, Error> { ) -> Result<local_chain::Update, Error> {
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>(); let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
@ -120,41 +120,39 @@ impl EsploraExt for esplora_client::BlockingClient {
let earliest_agreement_cp = { let earliest_agreement_cp = {
let mut earliest_agreement_cp = Option::<CheckPoint>::None; let mut earliest_agreement_cp = Option::<CheckPoint>::None;
if let Some(local_tip) = local_tip { let local_tip_height = local_tip.height();
let local_tip_height = local_tip.height(); for local_cp in local_tip.iter() {
for local_cp in local_tip.iter() { let local_block = local_cp.block_id();
let local_block = local_cp.block_id();
// the updated hash (block hash at this height after the update), can either be: // the updated hash (block hash at this height after the update), can either be:
// 1. a block that already existed in `fetched_blocks` // 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 // 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 // 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 // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
// remote tip // remote tip
let updated_hash = match fetched_blocks.entry(local_block.height) { let updated_hash = match fetched_blocks.entry(local_block.height) {
btree_map::Entry::Occupied(entry) => *entry.get(), btree_map::Entry::Occupied(entry) => *entry.get(),
btree_map::Entry::Vacant(entry) => *entry.insert( btree_map::Entry::Vacant(entry) => *entry.insert(
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
local_block.hash local_block.hash
} else { } else {
self.get_block_hash(local_block.height)? self.get_block_hash(local_block.height)?
}, },
), ),
}; };
// since we may introduce blocks below the point of agreement, we cannot break // 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 // here unconditionally - we only break if we guarantee there are no new heights
// below our current local checkpoint // below our current local checkpoint
if local_block.hash == updated_hash { if local_block.hash == updated_hash {
earliest_agreement_cp = Some(local_cp); earliest_agreement_cp = Some(local_cp);
let first_new_height = *fetched_blocks let first_new_height = *fetched_blocks
.keys() .keys()
.next() .next()
.expect("must have at least one new block"); .expect("must have at least one new block");
if first_new_height >= local_block.height { if first_new_height >= local_block.height {
break; break;
}
} }
} }
} }

View File

@ -131,7 +131,7 @@ fn main() -> anyhow::Result<()> {
start.elapsed().as_secs_f32() 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!( println!(
"[{:>10}s] loaded local chain from changeset", "[{:>10}s] loaded local chain from changeset",
start.elapsed().as_secs_f32() start.elapsed().as_secs_f32()
@ -170,10 +170,7 @@ fn main() -> anyhow::Result<()> {
let chain_tip = chain.lock().unwrap().tip(); let chain_tip = chain.lock().unwrap().tip();
let rpc_client = rpc_args.new_client()?; let rpc_client = rpc_args.new_client()?;
let mut emitter = match chain_tip { let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
None => Emitter::from_height(&rpc_client, fallback_height),
};
let mut last_db_commit = Instant::now(); let mut last_db_commit = Instant::now();
let mut last_print = 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 // print synced-to height and current balance in intervals
if last_print.elapsed() >= STDOUT_PRINT_DELAY { if last_print.elapsed() >= STDOUT_PRINT_DELAY {
last_print = Instant::now(); last_print = Instant::now();
if let Some(synced_to) = chain.tip() { let synced_to = chain.tip();
let balance = { let balance = {
graph.graph().balance( graph.graph().balance(
&*chain, &*chain,
synced_to.block_id(), synced_to.block_id(),
graph.index.outpoints().iter().cloned(), graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal, |(k, _), _| k == &Keychain::Internal,
) )
}; };
println!( println!(
"[{:>10}s] synced to {} @ {} | total: {} sats", "[{:>10}s] synced to {} @ {} | total: {} sats",
start.elapsed().as_secs_f32(), start.elapsed().as_secs_f32(),
synced_to.hash(), synced_to.hash(),
synced_to.height(), synced_to.height(),
balance.total() balance.total()
); );
}
} }
} }
@ -253,10 +249,7 @@ fn main() -> anyhow::Result<()> {
let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND); let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND);
let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> { let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> {
let rpc_client = rpc_args.new_client()?; let rpc_client = rpc_args.new_client()?;
let mut emitter = match last_cp { let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height);
Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
None => Emitter::from_height(&rpc_client, fallback_height),
};
let mut block_count = rpc_client.get_block_count()? as u32; let mut block_count = rpc_client.get_block_count()? as u32;
tx.send(Emission::Tip(block_count))?; 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 { if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY {
last_print = Some(Instant::now()); last_print = Some(Instant::now());
if let Some(synced_to) = chain.tip() { let synced_to = chain.tip();
let balance = { let balance = {
graph.graph().balance( graph.graph().balance(
&*chain, &*chain,
synced_to.block_id(), synced_to.block_id(),
graph.index.outpoints().iter().cloned(), graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal, |(k, _), _| k == &Keychain::Internal,
) )
}; };
println!( println!(
"[{:>10}s] synced to {} @ {} / {} | total: {} sats", "[{:>10}s] synced to {} @ {} / {} | total: {} sats",
start.elapsed().as_secs_f32(), start.elapsed().as_secs_f32(),
synced_to.hash(), synced_to.hash(),
synced_to.height(), synced_to.height(),
tip_height, tip_height,
balance.total() balance.total()
); );
}
} }
} }

View File

@ -315,10 +315,8 @@ where
version: 0x02, version: 0x02,
// because the temporary planning module does not support timelocks, we can use the chain // because the temporary planning module does not support timelocks, we can use the chain
// tip as the `lock_time` for anti-fee-sniping purposes // tip as the `lock_time` for anti-fee-sniping purposes
lock_time: chain lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
.get_chain_tip()? .expect("invalid height"),
.and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok())
.unwrap_or(absolute::LockTime::ZERO),
input: selected_txos input: selected_txos
.iter() .iter()
.map(|(_, utxo)| TxIn { .map(|(_, utxo)| TxIn {
@ -404,7 +402,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
chain: &O, chain: &O,
assets: &bdk_tmp_plan::Assets<K>, assets: &bdk_tmp_plan::Assets<K>,
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> { ) -> 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(); let outpoints = graph.index.outpoints().iter().cloned();
graph graph
.graph() .graph()
@ -509,7 +507,7 @@ where
let balance = graph.graph().try_balance( let balance = graph.graph().try_balance(
chain, chain,
chain.get_chain_tip()?.unwrap_or_default(), chain.get_chain_tip()?,
graph.index.outpoints().iter().cloned(), graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal, |(k, _), _| k == &Keychain::Internal,
)?; )?;
@ -539,7 +537,7 @@ where
Commands::TxOut { txout_cmd } => { Commands::TxOut { txout_cmd } => {
let graph = &*graph.lock().unwrap(); let graph = &*graph.lock().unwrap();
let chain = &*chain.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(); let outpoints = graph.index.outpoints().iter().cloned();
match txout_cmd { match txout_cmd {

View File

@ -112,7 +112,7 @@ fn main() -> anyhow::Result<()> {
graph 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 { let electrum_cmd = match &args.command {
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd, 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 // Get a short lock on the tracker to get the spks we're interested in
let graph = graph.lock().unwrap(); let graph = graph.lock().unwrap();
let chain = chain.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) { if !(all_spks || unused_spks || utxos || unconfirmed) {
unused_spks = true; unused_spks = true;

View File

@ -5,10 +5,10 @@ use std::{
}; };
use bdk_chain::{ use bdk_chain::{
bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid}, bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
indexed_tx_graph::{self, IndexedTxGraph}, indexed_tx_graph::{self, IndexedTxGraph},
keychain, keychain,
local_chain::{self, CheckPoint, LocalChain}, local_chain::{self, LocalChain},
Append, ConfirmationTimeHeightAnchor, Append, ConfirmationTimeHeightAnchor,
}; };
@ -102,6 +102,8 @@ fn main() -> anyhow::Result<()> {
let (args, keymap, index, db, init_changeset) = let (args, keymap, index, db, init_changeset) =
example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?; 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; let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset;
// Contruct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in // Contruct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in
@ -113,8 +115,8 @@ fn main() -> anyhow::Result<()> {
graph graph
}); });
let chain = Mutex::new({ let chain = Mutex::new({
let mut chain = LocalChain::default(); let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
chain.apply_changeset(&init_chain_changeset); chain.apply_changeset(&init_chain_changeset)?;
chain chain
}); });
@ -234,7 +236,7 @@ fn main() -> anyhow::Result<()> {
{ {
let graph = graph.lock().unwrap(); let graph = graph.lock().unwrap();
let chain = chain.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 { if *all_spks {
let all_spks = graph let all_spks = graph
@ -332,7 +334,7 @@ fn main() -> anyhow::Result<()> {
(missing_block_heights, tip) (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); println!("missing block heights: {:?}", missing_block_heights);
// Here, we actually fetch the missing blocks and create a `local_chain::Update`. // Here, we actually fetch the missing blocks and create a `local_chain::Update`.