-
Notifications
You must be signed in to change notification settings - Fork 438
Simplify Esplora::update_local_chain and add tests
#1267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
f05e850
6f824cf
63fa710
216648b
929b5dd
c5afbaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,14 +2,14 @@ use async_trait::async_trait; | |
| use bdk_chain::collections::btree_map; | ||
| use bdk_chain::{ | ||
| bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, | ||
| collections::{BTreeMap, BTreeSet}, | ||
| collections::BTreeMap, | ||
| local_chain::{self, CheckPoint}, | ||
| BlockId, ConfirmationTimeHeightAnchor, TxGraph, | ||
| }; | ||
| use esplora_client::{Error, TxStatus}; | ||
| use futures::{stream::FuturesOrdered, TryStreamExt}; | ||
|
|
||
| use crate::{anchor_from_status, ASSUME_FINAL_DEPTH}; | ||
| use crate::anchor_from_status; | ||
|
|
||
| /// Trait to extend the functionality of [`esplora_client::AsyncClient`]. | ||
| /// | ||
|
|
@@ -85,10 +85,11 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { | |
| 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 new_tip_height = self.get_height().await?; | ||
|
|
||
| // atomically fetch blocks from esplora | ||
| // Atomically fetch latest blocks from Esplora. This way, we avoid creating an update with | ||
| // an inconsistent set of blocks (assuming that a reorg depth cannot be greater than the | ||
| // latest blocks fetched). | ||
evanlinjin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let mut fetched_blocks = { | ||
| let heights = (0..=new_tip_height).rev(); | ||
| let hashes = self | ||
|
|
@@ -99,89 +100,41 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { | |
| heights.zip(hashes).collect::<BTreeMap<u32, BlockHash>>() | ||
| }; | ||
|
|
||
| // fetch heights that the caller is interested in | ||
| // fetch blocks of heights that the caller is interested in, reusing latest blocks that are | ||
evanlinjin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // already fetched. | ||
| for height in request_heights { | ||
| // do not fetch blocks higher than remote tip | ||
| if height > new_tip_height { | ||
| continue; | ||
| } | ||
| // only fetch what is missing | ||
| if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) { | ||
| let hash = self.get_block_hash(height).await?; | ||
| entry.insert(hash); | ||
| entry.insert(self.get_block_hash(height).await?); | ||
| } | ||
| } | ||
|
|
||
| // find the earliest point of agreement between local chain and fetched chain | ||
| let earliest_agreement_cp = { | ||
| let mut earliest_agreement_cp = Option::<CheckPoint>::None; | ||
|
|
||
| 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? | ||
| }, | ||
| ), | ||
| }; | ||
|
|
||
| // 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; | ||
| } | ||
| } | ||
| // Ensure `fetched_blocks` can create an update that connects with the original chain. | ||
evanlinjin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| for (height, local_hash) in local_tip.iter().map(|cp| (cp.height(), cp.hash())) { | ||
evanlinjin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if height > new_tip_height { | ||
| continue; | ||
| } | ||
|
|
||
| earliest_agreement_cp | ||
| }; | ||
|
|
||
| let tip = { | ||
| // first checkpoint to use for the update chain | ||
| let first_cp = match earliest_agreement_cp { | ||
| Some(cp) => cp, | ||
| None => { | ||
| let (&height, &hash) = fetched_blocks | ||
| .iter() | ||
| .next() | ||
| .expect("must have at least one new block"); | ||
| CheckPoint::new(BlockId { height, hash }) | ||
| let fetched_hash = match fetched_blocks.entry(height) { | ||
| btree_map::Entry::Occupied(entry) => *entry.get(), | ||
| btree_map::Entry::Vacant(entry) => { | ||
| *entry.insert(self.get_block_hash(height).await?) | ||
| } | ||
| }; | ||
| // transform fetched chain into the update chain | ||
| fetched_blocks | ||
| // we exclude anything at or below the first cp of the update chain otherwise | ||
| // building the chain will fail | ||
| .split_off(&(first_cp.height() + 1)) | ||
| .into_iter() | ||
| .map(|(height, hash)| BlockId { height, hash }) | ||
| .fold(first_cp, |prev_cp, block| { | ||
| prev_cp.push(block).expect("must extend checkpoint") | ||
| }) | ||
| }; | ||
|
|
||
| // We have found point of agreement so the update will connect! | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if no point of agreement is found? Is this possible?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should never happen because the genesis block must connect! However, let's say hypothetically, the genesis doesn't connect (because the wallet and esplora are on different networks), the update formed will try replace the genesis, resulting in the |
||
| if fetched_hash == local_hash { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| Ok(local_chain::Update { | ||
| tip, | ||
| tip: CheckPoint::from_block_ids(fetched_blocks.into_iter().map(BlockId::from)) | ||
| .expect("must be in height order"), | ||
| introduce_older_blocks: true, | ||
| }) | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.