Merge bitcoindevkit/bdk#1178: LocalChain
with hardwired genesis block
f1b112e8f9563c2afb9097911ee5e1afbbc55d22 docs(bitcoind_rpc): update docs for `Emitter::new` (志宇) 9a250baf6203a077fcfc03c3f6b386d5fba03d60 chore: make clippy happy (志宇) 79b84bed0ec399a375490c2c0f16c6ea3f5969b6 feat(bdk): changeset's `Append` impl checks that network is consistent (志宇) 06a956ad20ca5f1dcd108e45a1a1fed924128cdb feat!: change `load_from_persistence` to return an option (志宇) c3265e2514070bd4da92ca343fe884e13e831360 test(bdk): add tests for wallet constructor methods (志宇) 96f1d94e2c8378d820a59e5ceafe686477243bf8 test(file_store): add construction method tests (志宇) 1886dc4fe743408132c3257afc1cacbb4d964105 chore(examples): use `Wallet::new_or_load` method where appropriate (志宇) 24994a3ed47aacf203ca0b456eb22f4b93ec58a8 feat(file_store)!: have separate methods for creating and opening Store (志宇) d294e2e3189dc14efe5b9916cf83f5e7f8592c64 feat(wallet)!: add `new_or_load` methods (志宇) 7c6cbc4d9f7349e769594151936b3e2947b79d00 chore(file_store): rm empty test file (志宇) 6cf3963c6cfeacc8cd9d59bdb0bafded5022baaf feat(bdk)!: have separate methods for creating and loading `Wallet` (志宇) 7d5f31f6cc323241e866c136656f9674e0bf7c53 feat(chain, file_store): add `is_empty` method to `PersistBackend` trait (志宇) 5998a228191caccfaeac2b38ed4f319994b944c1 feat!: `LocalChain` with hardwired genesis checkpoint (志宇) Pull request description: closes #1079 closes #1107 ### Description Many methods of `TxGraph` require a `chain_tip: BlockId` input to use against a `ChainOracle` implementation. This is used to ask the `ChainOracle` implementation whether a certain block exists in the chain identified by the `chain_tip`. This guarantees that the `TxGraph` methods will return a consistent history of transactions. However, the `ChainOracle` trait's `get_chain_tip` method returns an option of `BlockId`. It becomes unclear what to do when `get_chain_tip` returns `None`. This PR changes the `ChainOracle::get_chain_tip` method to always return a `BlockId` (no `Option`). `LocalChain` now hardwires the genesis block in order to implement `ChainOracle`. `bdk::Wallet` and `bdk_file_store::Store` are changed to have separate constructor methods for initializing a fresh instance and recovering a previous instance from persistence. ### Notes to the reviewers ### Changelog notice - Changed `ChainOracle::get_chain_tip` method to return a `BlockId` instead of an `Option` of a `BlockId`. - Refactored `LocalChain` so that the genesis `BlockId` is hardwired. This way, the `ChainOracle::get_chain_tip` implementation can always return a tip. - Add `is_empty` method to `PersistBackend`. This returns true when there is no data in the persistence. - Changed `Wallet::new` to initialize a fresh wallet only. - Added `Wallet::load` to restore an instance of a wallet. - Replaced `Store::new` with separate methods to create/open the database file. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature Top commit has no ACKs. Tree-SHA512: 31b75fb53cc451f1fce7e409f1112c43973db7e8b5b31640e01e5b52089683b60320565427d6ea0478ff4c8680dbdb9272fdab08aef69d30f257da52e731e1a3
This commit is contained in:
commit
bc8d6a396b
@ -47,6 +47,8 @@ dev-getrandom-wasm = ["getrandom/js"]
|
||||
lazy_static = "1.4"
|
||||
env_logger = "0.7"
|
||||
assert_matches = "1.5.0"
|
||||
tempfile = "3"
|
||||
bdk_file_store = { path = "../file_store" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
@ -836,7 +836,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 250_000 + FEE_AMOUNT;
|
||||
|
||||
let result = LargestFirstCoinSelection::default()
|
||||
let result = LargestFirstCoinSelection
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
@ -857,7 +857,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 20_000 + FEE_AMOUNT;
|
||||
|
||||
let result = LargestFirstCoinSelection::default()
|
||||
let result = LargestFirstCoinSelection
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
@ -878,7 +878,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 20_000 + FEE_AMOUNT;
|
||||
|
||||
let result = LargestFirstCoinSelection::default()
|
||||
let result = LargestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
@ -900,7 +900,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 500_000 + FEE_AMOUNT;
|
||||
|
||||
LargestFirstCoinSelection::default()
|
||||
LargestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
@ -918,7 +918,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 250_000 + FEE_AMOUNT;
|
||||
|
||||
LargestFirstCoinSelection::default()
|
||||
LargestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
@ -935,7 +935,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 180_000 + FEE_AMOUNT;
|
||||
|
||||
let result = OldestFirstCoinSelection::default()
|
||||
let result = OldestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
@ -956,7 +956,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 20_000 + FEE_AMOUNT;
|
||||
|
||||
let result = OldestFirstCoinSelection::default()
|
||||
let result = OldestFirstCoinSelection
|
||||
.coin_select(
|
||||
utxos,
|
||||
vec![],
|
||||
@ -977,7 +977,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 20_000 + FEE_AMOUNT;
|
||||
|
||||
let result = OldestFirstCoinSelection::default()
|
||||
let result = OldestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
@ -999,7 +999,7 @@ mod test {
|
||||
let drain_script = ScriptBuf::default();
|
||||
let target_amount = 600_000 + FEE_AMOUNT;
|
||||
|
||||
OldestFirstCoinSelection::default()
|
||||
OldestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
@ -1018,7 +1018,7 @@ mod test {
|
||||
let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
OldestFirstCoinSelection::default()
|
||||
OldestFirstCoinSelection
|
||||
.coin_select(
|
||||
vec![],
|
||||
utxos,
|
||||
|
@ -28,14 +28,14 @@ use bdk_chain::{
|
||||
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
|
||||
IndexedTxGraph, Persist, PersistBackend,
|
||||
};
|
||||
use bitcoin::consensus::encode::serialize;
|
||||
use bitcoin::psbt;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::secp256k1::{All, 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};
|
||||
@ -128,12 +128,22 @@ pub struct ChangeSet {
|
||||
ConfirmationTimeHeightAnchor,
|
||||
keychain::ChangeSet<KeychainKind>,
|
||||
>,
|
||||
|
||||
/// Stores the network type of the wallet.
|
||||
pub network: Option<Network>,
|
||||
}
|
||||
|
||||
impl Append for ChangeSet {
|
||||
fn append(&mut self, other: Self) {
|
||||
Append::append(&mut self.chain, other.chain);
|
||||
Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
|
||||
if other.network.is_some() {
|
||||
debug_assert!(
|
||||
self.network.is_none() || self.network == other.network,
|
||||
"network type must be consistent"
|
||||
);
|
||||
self.network = other.network;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
@ -228,34 +238,156 @@ impl Wallet {
|
||||
) -> Result<Self, crate::descriptor::DescriptorError> {
|
||||
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::Write(_) => unreachable!("mock-write must always succeed"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a wallet that does not persist data, with a custom genesis hash.
|
||||
pub fn new_no_persist_with_genesis_hash<E: IntoWalletDescriptor>(
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
network: Network,
|
||||
genesis_hash: BlockHash,
|
||||
) -> Result<Self, crate::descriptor::DescriptorError> {
|
||||
Self::new_with_genesis_hash(descriptor, change_descriptor, (), network, genesis_hash)
|
||||
.map_err(|e| match e {
|
||||
NewError::Descriptor(e) => e,
|
||||
NewError::Write(_) => unreachable!("mock-write must always succeed"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The error type when constructing a fresh [`Wallet`].
|
||||
///
|
||||
/// Methods [`new`] and [`new_with_genesis_hash`] may return this error.
|
||||
///
|
||||
/// [`new`]: Wallet::new
|
||||
/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`Wallet::new`]
|
||||
pub enum NewError<P> {
|
||||
/// There was problem with the descriptors passed in
|
||||
pub enum NewError<W> {
|
||||
/// There was problem with the passed-in descriptor(s).
|
||||
Descriptor(crate::descriptor::DescriptorError),
|
||||
/// We were unable to load the wallet's data from the persistence backend
|
||||
Persist(P),
|
||||
/// We were unable to write the wallet's data to the persistence backend.
|
||||
Write(W),
|
||||
}
|
||||
|
||||
impl<P> fmt::Display for NewError<P>
|
||||
impl<W> fmt::Display for NewError<W>
|
||||
where
|
||||
P: fmt::Display,
|
||||
W: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
NewError::Descriptor(e) => e.fmt(f),
|
||||
NewError::Persist(e) => {
|
||||
write!(f, "failed to load wallet from persistence backend: {}", e)
|
||||
NewError::Write(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<W> std::error::Error for NewError<W> where W: core::fmt::Display + core::fmt::Debug {}
|
||||
|
||||
/// The error type when loading a [`Wallet`] from persistence.
|
||||
///
|
||||
/// Method [`load`] may return this error.
|
||||
///
|
||||
/// [`load`]: Wallet::load
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError<L> {
|
||||
/// There was a problem with the passed-in descriptor(s).
|
||||
Descriptor(crate::descriptor::DescriptorError),
|
||||
/// Loading data from the persistence backend failed.
|
||||
Load(L),
|
||||
/// Wallet not initialized, persistence backend is empty.
|
||||
NotInitialized,
|
||||
/// Data loaded from persistence is missing network type.
|
||||
MissingNetwork,
|
||||
/// Data loaded from persistence is missing genesis hash.
|
||||
MissingGenesis,
|
||||
}
|
||||
|
||||
impl<L> fmt::Display for LoadError<L>
|
||||
where
|
||||
L: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LoadError::Descriptor(e) => e.fmt(f),
|
||||
LoadError::Load(e) => e.fmt(f),
|
||||
LoadError::NotInitialized => {
|
||||
write!(f, "wallet is not initialized, persistence backend is empty")
|
||||
}
|
||||
LoadError::MissingNetwork => write!(f, "loaded data is missing network type"),
|
||||
LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<L> std::error::Error for LoadError<L> where L: core::fmt::Display + core::fmt::Debug {}
|
||||
|
||||
/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existant.
|
||||
///
|
||||
/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
|
||||
///
|
||||
/// [`new_or_load`]: Wallet::new_or_load
|
||||
/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash
|
||||
#[derive(Debug)]
|
||||
pub enum NewOrLoadError<W, L> {
|
||||
/// There is a problem with the passed-in descriptor.
|
||||
Descriptor(crate::descriptor::DescriptorError),
|
||||
/// Writing to the persistence backend failed.
|
||||
Write(W),
|
||||
/// Loading from the persistence backend failed.
|
||||
Load(L),
|
||||
/// Wallet is not initialized, persistence backend is empty.
|
||||
NotInitialized,
|
||||
/// The loaded genesis hash does not match what was provided.
|
||||
LoadedGenesisDoesNotMatch {
|
||||
/// The expected genesis block hash.
|
||||
expected: BlockHash,
|
||||
/// The block hash loaded from persistence.
|
||||
got: Option<BlockHash>,
|
||||
},
|
||||
/// The loaded network type does not match what was provided.
|
||||
LoadedNetworkDoesNotMatch {
|
||||
/// The expected network type.
|
||||
expected: Network,
|
||||
/// The network type loaded from persistence.
|
||||
got: Option<Network>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<W, L> fmt::Display for NewOrLoadError<W, L>
|
||||
where
|
||||
W: fmt::Display,
|
||||
L: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
NewOrLoadError::Descriptor(e) => e.fmt(f),
|
||||
NewOrLoadError::Write(e) => write!(f, "failed to write to persistence: {}", e),
|
||||
NewOrLoadError::Load(e) => write!(f, "failed to load from persistence: {}", e),
|
||||
NewOrLoadError::NotInitialized => {
|
||||
write!(f, "wallet is not initialized, persistence backend is empty")
|
||||
}
|
||||
NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
|
||||
write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
|
||||
}
|
||||
NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
|
||||
write!(f, "loaded network type is not {}, got {:?}", expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<W, L> std::error::Error for NewOrLoadError<W, L>
|
||||
where
|
||||
W: core::fmt::Display + core::fmt::Debug,
|
||||
L: core::fmt::Display + core::fmt::Debug,
|
||||
{
|
||||
}
|
||||
|
||||
/// An error that may occur when inserting a transaction into [`Wallet`].
|
||||
#[derive(Debug)]
|
||||
pub enum InsertTxError {
|
||||
@ -263,66 +395,58 @@ 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`.
|
||||
/// Initialize an empty [`Wallet`].
|
||||
pub fn new<E: IntoWalletDescriptor>(
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
mut db: D,
|
||||
db: D,
|
||||
network: Network,
|
||||
) -> Result<Self, NewError<D::LoadError>>
|
||||
) -> Result<Self, NewError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let genesis_hash = genesis_block(network).block_hash();
|
||||
Self::new_with_genesis_hash(descriptor, change_descriptor, db, network, genesis_hash)
|
||||
}
|
||||
|
||||
/// Initialize an empty [`Wallet`] with a custom genesis hash.
|
||||
///
|
||||
/// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful
|
||||
/// for syncing from alternative networks.
|
||||
pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
db: D,
|
||||
network: Network,
|
||||
genesis_hash: BlockHash,
|
||||
) -> Result<Self, NewError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let secp = Secp256k1::new();
|
||||
let mut chain = LocalChain::default();
|
||||
let mut indexed_graph = IndexedTxGraph::<
|
||||
ConfirmationTimeHeightAnchor,
|
||||
KeychainTxOutIndex<KeychainKind>,
|
||||
>::default();
|
||||
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
|
||||
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
|
||||
|
||||
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)
|
||||
.map_err(NewError::Descriptor)?;
|
||||
indexed_graph
|
||||
.index
|
||||
.add_keychain(KeychainKind::External, descriptor.clone());
|
||||
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp));
|
||||
let change_signers = match change_descriptor {
|
||||
Some(desc) => {
|
||||
let (change_descriptor, change_keymap) =
|
||||
into_wallet_descriptor_checked(desc, &secp, network)
|
||||
.map_err(NewError::Descriptor)?;
|
||||
let (signers, change_signers) =
|
||||
create_signers(&mut index, &secp, descriptor, change_descriptor, network)
|
||||
.map_err(NewError::Descriptor)?;
|
||||
|
||||
let change_signers = Arc::new(SignersContainer::build(
|
||||
change_keymap,
|
||||
&change_descriptor,
|
||||
&secp,
|
||||
));
|
||||
let indexed_graph = IndexedTxGraph::new(index);
|
||||
|
||||
indexed_graph
|
||||
.index
|
||||
.add_keychain(KeychainKind::Internal, change_descriptor);
|
||||
|
||||
change_signers
|
||||
}
|
||||
None => Arc::new(SignersContainer::new()),
|
||||
};
|
||||
|
||||
let changeset = db.load_from_persistence().map_err(NewError::Persist)?;
|
||||
chain.apply_changeset(&changeset.chain);
|
||||
indexed_graph.apply_changeset(changeset.indexed_tx_graph);
|
||||
|
||||
let persist = Persist::new(db);
|
||||
let mut persist = Persist::new(db);
|
||||
persist.stage(ChangeSet {
|
||||
chain: chain_changeset,
|
||||
indexed_tx_graph: indexed_graph.initial_changeset(),
|
||||
network: Some(network),
|
||||
});
|
||||
persist.commit().map_err(NewError::Write)?;
|
||||
|
||||
Ok(Wallet {
|
||||
signers,
|
||||
@ -335,6 +459,143 @@ impl<D> Wallet<D> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load [`Wallet`] from the given persistence backend.
|
||||
pub fn load<E: IntoWalletDescriptor>(
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
mut db: D,
|
||||
) -> Result<Self, LoadError<D::LoadError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let changeset = db
|
||||
.load_from_persistence()
|
||||
.map_err(LoadError::Load)?
|
||||
.ok_or(LoadError::NotInitialized)?;
|
||||
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
|
||||
}
|
||||
|
||||
fn load_from_changeset<E: IntoWalletDescriptor>(
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
db: D,
|
||||
changeset: ChangeSet,
|
||||
) -> Result<Self, LoadError<D::LoadError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let secp = Secp256k1::new();
|
||||
let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
|
||||
let chain =
|
||||
LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
|
||||
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
|
||||
|
||||
let (signers, change_signers) =
|
||||
create_signers(&mut index, &secp, descriptor, change_descriptor, network)
|
||||
.map_err(LoadError::Descriptor)?;
|
||||
|
||||
let indexed_graph = IndexedTxGraph::new(index);
|
||||
let persist = Persist::new(db);
|
||||
|
||||
Ok(Wallet {
|
||||
signers,
|
||||
change_signers,
|
||||
chain,
|
||||
indexed_graph,
|
||||
persist,
|
||||
network,
|
||||
secp,
|
||||
})
|
||||
}
|
||||
|
||||
/// Either loads [`Wallet`] from persistence, or initializes it if it does not exist.
|
||||
///
|
||||
/// This method will fail if the loaded [`Wallet`] has different parameters to those provided.
|
||||
pub fn new_or_load<E: IntoWalletDescriptor>(
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
db: D,
|
||||
network: Network,
|
||||
) -> Result<Self, NewOrLoadError<D::WriteError, D::LoadError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let genesis_hash = genesis_block(network).block_hash();
|
||||
Self::new_or_load_with_genesis_hash(
|
||||
descriptor,
|
||||
change_descriptor,
|
||||
db,
|
||||
network,
|
||||
genesis_hash,
|
||||
)
|
||||
}
|
||||
|
||||
/// Either loads [`Wallet`] from persistence, or initializes it if it does not exist (with a
|
||||
/// custom genesis hash).
|
||||
///
|
||||
/// This method will fail if the loaded [`Wallet`] has different parameters to those provided.
|
||||
/// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is
|
||||
/// useful for syncing from alternative networks.
|
||||
pub fn new_or_load_with_genesis_hash<E: IntoWalletDescriptor>(
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
mut db: D,
|
||||
network: Network,
|
||||
genesis_hash: BlockHash,
|
||||
) -> Result<Self, NewOrLoadError<D::WriteError, D::LoadError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
let changeset = db.load_from_persistence().map_err(NewOrLoadError::Load)?;
|
||||
match changeset {
|
||||
Some(changeset) => {
|
||||
let wallet =
|
||||
Self::load_from_changeset(descriptor, change_descriptor, db, changeset)
|
||||
.map_err(|e| match e {
|
||||
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
||||
LoadError::Load(e) => NewOrLoadError::Load(e),
|
||||
LoadError::NotInitialized => NewOrLoadError::NotInitialized,
|
||||
LoadError::MissingNetwork => {
|
||||
NewOrLoadError::LoadedNetworkDoesNotMatch {
|
||||
expected: network,
|
||||
got: None,
|
||||
}
|
||||
}
|
||||
LoadError::MissingGenesis => {
|
||||
NewOrLoadError::LoadedGenesisDoesNotMatch {
|
||||
expected: genesis_hash,
|
||||
got: None,
|
||||
}
|
||||
}
|
||||
})?;
|
||||
if wallet.network != network {
|
||||
return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
|
||||
expected: network,
|
||||
got: Some(wallet.network),
|
||||
});
|
||||
}
|
||||
if wallet.chain.genesis_hash() != genesis_hash {
|
||||
return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
|
||||
expected: genesis_hash,
|
||||
got: Some(wallet.chain.genesis_hash()),
|
||||
});
|
||||
}
|
||||
Ok(wallet)
|
||||
}
|
||||
None => Self::new_with_genesis_hash(
|
||||
descriptor,
|
||||
change_descriptor,
|
||||
db,
|
||||
network,
|
||||
genesis_hash,
|
||||
)
|
||||
.map_err(|e| match e {
|
||||
NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
|
||||
NewError::Write(e) => NewOrLoadError::Write(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Bitcoin network the wallet is using.
|
||||
pub fn network(&self) -> Network {
|
||||
self.network
|
||||
@ -446,7 +707,7 @@ impl<D> Wallet<D> {
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&self.chain,
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
self.chain.tip().block_id(),
|
||||
self.indexed_graph.index.outpoints().iter().cloned(),
|
||||
)
|
||||
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
|
||||
@ -458,7 +719,7 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
|
||||
/// Returns the latest checkpoint.
|
||||
pub fn latest_checkpoint(&self) -> Option<CheckPoint> {
|
||||
pub fn latest_checkpoint(&self) -> CheckPoint {
|
||||
self.chain.tip()
|
||||
}
|
||||
|
||||
@ -496,7 +757,7 @@ impl<D> Wallet<D> {
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&self.chain,
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
self.chain.tip().block_id(),
|
||||
core::iter::once((spk_i, op)),
|
||||
)
|
||||
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
|
||||
@ -669,7 +930,7 @@ impl<D> Wallet<D> {
|
||||
Some(CanonicalTx {
|
||||
chain_position: graph.get_chain_position(
|
||||
&self.chain,
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
self.chain.tip().block_id(),
|
||||
txid,
|
||||
)?,
|
||||
tx_node: graph.get_tx_node(txid)?,
|
||||
@ -686,7 +947,7 @@ impl<D> Wallet<D> {
|
||||
pub fn insert_checkpoint(
|
||||
&mut self,
|
||||
block_id: BlockId,
|
||||
) -> Result<bool, local_chain::InsertBlockError>
|
||||
) -> Result<bool, local_chain::AlterCheckPointError>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
@ -730,7 +991,7 @@ impl<D> Wallet<D> {
|
||||
.range(height..)
|
||||
.next()
|
||||
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
|
||||
tip_height: self.chain.tip().map(|b| b.height()),
|
||||
tip_height: self.chain.tip().height(),
|
||||
tx_height: height,
|
||||
})
|
||||
.map(|(&anchor_height, &hash)| ConfirmationTimeHeightAnchor {
|
||||
@ -766,10 +1027,9 @@ impl<D> Wallet<D> {
|
||||
pub fn transactions(
|
||||
&self,
|
||||
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> + '_ {
|
||||
self.indexed_graph.graph().list_chain_txs(
|
||||
&self.chain,
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
)
|
||||
self.indexed_graph
|
||||
.graph()
|
||||
.list_chain_txs(&self.chain, self.chain.tip().block_id())
|
||||
}
|
||||
|
||||
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
|
||||
@ -777,7 +1037,7 @@ impl<D> Wallet<D> {
|
||||
pub fn get_balance(&self) -> Balance {
|
||||
self.indexed_graph.graph().balance(
|
||||
&self.chain,
|
||||
self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(),
|
||||
self.chain.tip().block_id(),
|
||||
self.indexed_graph.index.outpoints().iter().cloned(),
|
||||
|&(k, _), _| k == KeychainKind::Internal,
|
||||
)
|
||||
@ -945,14 +1205,14 @@ impl<D> Wallet<D> {
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
// We use a match here instead of a map_or_else as it's way more readable :)
|
||||
// We use a match here instead of a unwrap_or_else as it's way more readable :)
|
||||
let current_height = match params.current_height {
|
||||
// If they didn't tell us the current height, we assume it's the latest sync height.
|
||||
None => self
|
||||
.chain
|
||||
.tip()
|
||||
.map(|cp| absolute::LockTime::from_height(cp.height()).expect("Invalid height")),
|
||||
h => h,
|
||||
None => {
|
||||
let tip_height = self.chain.tip().height();
|
||||
absolute::LockTime::from_height(tip_height).expect("invalid height")
|
||||
}
|
||||
Some(h) => h,
|
||||
};
|
||||
|
||||
let lock_time = match params.locktime {
|
||||
@ -961,7 +1221,7 @@ impl<D> Wallet<D> {
|
||||
// Fee sniping can be partially prevented by setting the timelock
|
||||
// to current_height. If we don't know the current_height,
|
||||
// we default to 0.
|
||||
let fee_sniping_height = current_height.unwrap_or(absolute::LockTime::ZERO);
|
||||
let fee_sniping_height = current_height;
|
||||
|
||||
// We choose the biggest between the required nlocktime and the fee sniping
|
||||
// height
|
||||
@ -1115,7 +1375,7 @@ impl<D> Wallet<D> {
|
||||
params.drain_wallet,
|
||||
params.manually_selected_only,
|
||||
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
||||
current_height.map(absolute::LockTime::to_consensus_u32),
|
||||
Some(current_height.to_consensus_u32()),
|
||||
);
|
||||
|
||||
// get drain script
|
||||
@ -1257,7 +1517,7 @@ impl<D> Wallet<D> {
|
||||
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
|
||||
let graph = self.indexed_graph.graph();
|
||||
let txout_index = &self.indexed_graph.index;
|
||||
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
let chain_tip = self.chain.tip().block_id();
|
||||
|
||||
let mut tx = graph
|
||||
.get_tx(txid)
|
||||
@ -1492,7 +1752,7 @@ impl<D> Wallet<D> {
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
sign_options: SignOptions,
|
||||
) -> Result<bool, Error> {
|
||||
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
let chain_tip = self.chain.tip().block_id();
|
||||
|
||||
let tx = &psbt.unsigned_tx;
|
||||
let mut finished = true;
|
||||
@ -1515,7 +1775,7 @@ impl<D> Wallet<D> {
|
||||
});
|
||||
let current_height = sign_options
|
||||
.assume_height
|
||||
.or(self.chain.tip().map(|b| b.height()));
|
||||
.unwrap_or_else(|| self.chain.tip().height());
|
||||
|
||||
debug!(
|
||||
"Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
|
||||
@ -1552,8 +1812,8 @@ impl<D> Wallet<D> {
|
||||
&mut tmp_input,
|
||||
(
|
||||
PsbtInputSatisfier::new(psbt, n),
|
||||
After::new(current_height, false),
|
||||
Older::new(current_height, confirmation_height, false),
|
||||
After::new(Some(current_height), false),
|
||||
Older::new(Some(current_height), confirmation_height, false),
|
||||
),
|
||||
) {
|
||||
Ok(_) => {
|
||||
@ -1661,7 +1921,7 @@ impl<D> Wallet<D> {
|
||||
must_only_use_confirmed_tx: bool,
|
||||
current_height: Option<u32>,
|
||||
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
|
||||
let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
let chain_tip = self.chain.tip().block_id();
|
||||
// must_spend <- manually selected utxos
|
||||
// may_spend <- all other available utxos
|
||||
let mut may_spend = self.get_available_utxos();
|
||||
@ -2049,6 +2309,30 @@ fn new_local_utxo(
|
||||
}
|
||||
}
|
||||
|
||||
fn create_signers<E: IntoWalletDescriptor>(
|
||||
index: &mut KeychainTxOutIndex<KeychainKind>,
|
||||
secp: &Secp256k1<All>,
|
||||
descriptor: E,
|
||||
change_descriptor: Option<E>,
|
||||
network: Network,
|
||||
) -> Result<(Arc<SignersContainer>, Arc<SignersContainer>), crate::descriptor::error::Error> {
|
||||
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
|
||||
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
|
||||
index.add_keychain(KeychainKind::External, descriptor);
|
||||
|
||||
let change_signers = match change_descriptor {
|
||||
Some(descriptor) => {
|
||||
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, secp, network)?;
|
||||
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
|
||||
index.add_keychain(KeychainKind::Internal, descriptor);
|
||||
signers
|
||||
}
|
||||
None => Arc::new(SignersContainer::new()),
|
||||
};
|
||||
|
||||
Ok((signers, change_signers))
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
/// Macro for getting a wallet for use in a doctest
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use bdk::descriptor::calc_checksum;
|
||||
use bdk::psbt::PsbtUtils;
|
||||
@ -17,7 +19,6 @@ use bitcoin::{
|
||||
};
|
||||
use bitcoin::{psbt, Network};
|
||||
use bitcoin::{BlockHash, Txid};
|
||||
use core::str::FromStr;
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
@ -42,14 +43,14 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->
|
||||
}
|
||||
|
||||
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
|
||||
let height = match wallet.latest_checkpoint() {
|
||||
Some(cp) => ConfirmationTime::Confirmed {
|
||||
height: cp.height(),
|
||||
time: 0,
|
||||
},
|
||||
None => ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
let latest_cp = wallet.latest_checkpoint();
|
||||
let height = latest_cp.height();
|
||||
let anchor = if height == 0 {
|
||||
ConfirmationTime::Unconfirmed { last_seen: 0 }
|
||||
} else {
|
||||
ConfirmationTime::Confirmed { height, time: 0 }
|
||||
};
|
||||
receive_output(wallet, value, height)
|
||||
receive_output(wallet, value, anchor)
|
||||
}
|
||||
|
||||
// The satisfaction size of a P2WPKH is 112 WU =
|
||||
@ -60,6 +61,101 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
|
||||
// OP_PUSH.
|
||||
const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
|
||||
|
||||
const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
|
||||
|
||||
#[test]
|
||||
fn load_recovers_wallet() {
|
||||
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||
let file_path = temp_dir.path().join("store.db");
|
||||
|
||||
// create new wallet
|
||||
let wallet_keychains = {
|
||||
let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db");
|
||||
let wallet =
|
||||
Wallet::new(get_test_wpkh(), None, db, Network::Testnet).expect("must init wallet");
|
||||
wallet.keychains().clone()
|
||||
};
|
||||
|
||||
// recover wallet
|
||||
{
|
||||
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
|
||||
let wallet = Wallet::load(get_test_wpkh(), None, db).expect("must recover wallet");
|
||||
assert_eq!(wallet.network(), Network::Testnet);
|
||||
assert_eq!(wallet.spk_index().keychains(), &wallet_keychains);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_or_load() {
|
||||
let temp_dir = tempfile::tempdir().expect("must create tempdir");
|
||||
let file_path = temp_dir.path().join("store.db");
|
||||
|
||||
// init wallet when non-existant
|
||||
let wallet_keychains = {
|
||||
let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path)
|
||||
.expect("must create db");
|
||||
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
|
||||
.expect("must init wallet");
|
||||
wallet.keychains().clone()
|
||||
};
|
||||
|
||||
// wrong network
|
||||
{
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin)
|
||||
.expect_err("wrong network");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
bdk::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
|
||||
got: Some(Network::Testnet),
|
||||
expected: Network::Bitcoin
|
||||
}
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
// wrong genesis hash
|
||||
{
|
||||
let exp_blockhash = BlockHash::all_zeros();
|
||||
let got_blockhash =
|
||||
bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash();
|
||||
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let err = Wallet::new_or_load_with_genesis_hash(
|
||||
get_test_wpkh(),
|
||||
None,
|
||||
db,
|
||||
Network::Testnet,
|
||||
exp_blockhash,
|
||||
)
|
||||
.expect_err("wrong genesis hash");
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
bdk::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
|
||||
if got == Some(got_blockhash) && expected == exp_blockhash
|
||||
),
|
||||
"err: {}",
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
// all parameters match
|
||||
{
|
||||
let db =
|
||||
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
|
||||
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
|
||||
.expect("must recover wallet");
|
||||
assert_eq!(wallet.network(), Network::Testnet);
|
||||
assert_eq!(wallet.keychains(), &wallet_keychains);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_descriptor_checksum() {
|
||||
let (wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
@ -277,7 +373,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() {
|
||||
// If there's no current_height we're left with using the last sync height
|
||||
assert_eq!(
|
||||
psbt.unsigned_tx.lock_time.to_consensus_u32(),
|
||||
wallet.latest_checkpoint().unwrap().height()
|
||||
wallet.latest_checkpoint().height()
|
||||
);
|
||||
}
|
||||
|
||||
@ -1615,7 +1711,7 @@ fn test_bump_fee_drain_wallet() {
|
||||
.insert_tx(
|
||||
tx.clone(),
|
||||
ConfirmationTime::Confirmed {
|
||||
height: wallet.latest_checkpoint().unwrap().height(),
|
||||
height: wallet.latest_checkpoint().height(),
|
||||
time: 42_000,
|
||||
},
|
||||
)
|
||||
@ -3085,7 +3181,7 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() {
|
||||
.values()
|
||||
.map(|(script, version)| TapLeafHash::from_script(script, *version))
|
||||
.collect();
|
||||
let included_script_leaves = vec![script_leaves.pop().unwrap()];
|
||||
let included_script_leaves = [script_leaves.pop().unwrap()];
|
||||
let excluded_script_leaves = script_leaves;
|
||||
|
||||
assert!(
|
||||
|
@ -25,7 +25,7 @@ pub struct Emitter<'c, C> {
|
||||
|
||||
/// The checkpoint of the last-emitted block that is in the best chain. If it is later found
|
||||
/// that the block is no longer in the best chain, it will be popped off from here.
|
||||
last_cp: Option<CheckPoint>,
|
||||
last_cp: CheckPoint,
|
||||
|
||||
/// The block result returned from rpc of the last-emitted block. As this result contains the
|
||||
/// next block's block hash (which we use to fetch the next block), we set this to `None`
|
||||
@ -43,29 +43,16 @@ pub struct Emitter<'c, C> {
|
||||
}
|
||||
|
||||
impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
|
||||
/// Construct a new [`Emitter`] with the given RPC `client` and `start_height`.
|
||||
/// Construct a new [`Emitter`] with the given RPC `client`, `last_cp` 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 {
|
||||
/// * `last_cp` is the check point used to find the latest block which is still part of the best
|
||||
/// chain.
|
||||
/// * `start_height` is the block height to start emitting blocks from.
|
||||
pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
|
||||
Self {
|
||||
client,
|
||||
start_height,
|
||||
last_cp: None,
|
||||
last_block: None,
|
||||
last_mempool_time: 0,
|
||||
last_mempool_tip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new [`Emitter`] with the given RPC `client` and `checkpoint`.
|
||||
///
|
||||
/// `checkpoint` is used to find the latest block which is still part of the best chain. The
|
||||
/// [`Emitter`] will emit blocks starting right above this block.
|
||||
pub fn from_checkpoint(client: &'c C, checkpoint: CheckPoint) -> Self {
|
||||
Self {
|
||||
client,
|
||||
start_height: 0,
|
||||
last_cp: Some(checkpoint),
|
||||
last_cp,
|
||||
last_block: None,
|
||||
last_mempool_time: 0,
|
||||
last_mempool_tip: None,
|
||||
@ -134,7 +121,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.last_mempool_time = latest_time;
|
||||
self.last_mempool_tip = self.last_cp.as_ref().map(|cp| cp.height());
|
||||
self.last_mempool_tip = Some(self.last_cp.height());
|
||||
|
||||
Ok(txs_to_emit)
|
||||
}
|
||||
@ -156,7 +143,8 @@ enum PollResponse {
|
||||
/// Fetched block is not in the best chain.
|
||||
BlockNotInBestChain,
|
||||
AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint),
|
||||
AgreementPointNotFound,
|
||||
/// Force the genesis checkpoint down the receiver's throat.
|
||||
AgreementPointNotFound(BlockHash),
|
||||
}
|
||||
|
||||
fn poll_once<C>(emitter: &Emitter<C>) -> Result<PollResponse, bitcoincore_rpc::Error>
|
||||
@ -166,45 +154,50 @@ where
|
||||
let client = emitter.client;
|
||||
|
||||
if let Some(last_res) = &emitter.last_block {
|
||||
assert!(
|
||||
emitter.last_cp.is_some(),
|
||||
"must not have block result without last cp"
|
||||
);
|
||||
|
||||
let next_hash = match last_res.nextblockhash {
|
||||
None => return Ok(PollResponse::NoMoreBlocks),
|
||||
Some(next_hash) => next_hash,
|
||||
let next_hash = if last_res.height < emitter.start_height as _ {
|
||||
// enforce start height
|
||||
let next_hash = client.get_block_hash(emitter.start_height as _)?;
|
||||
// make sure last emission is still in best chain
|
||||
if client.get_block_hash(last_res.height as _)? != last_res.hash {
|
||||
return Ok(PollResponse::BlockNotInBestChain);
|
||||
}
|
||||
next_hash
|
||||
} else {
|
||||
match last_res.nextblockhash {
|
||||
None => return Ok(PollResponse::NoMoreBlocks),
|
||||
Some(next_hash) => next_hash,
|
||||
}
|
||||
};
|
||||
|
||||
let res = client.get_block_info(&next_hash)?;
|
||||
if res.confirmations < 0 {
|
||||
return Ok(PollResponse::BlockNotInBestChain);
|
||||
}
|
||||
|
||||
return Ok(PollResponse::Block(res));
|
||||
}
|
||||
|
||||
if emitter.last_cp.is_none() {
|
||||
let hash = client.get_block_hash(emitter.start_height as _)?;
|
||||
|
||||
let res = client.get_block_info(&hash)?;
|
||||
if res.confirmations < 0 {
|
||||
return Ok(PollResponse::BlockNotInBestChain);
|
||||
}
|
||||
return Ok(PollResponse::Block(res));
|
||||
}
|
||||
|
||||
for cp in emitter.last_cp.iter().flat_map(CheckPoint::iter) {
|
||||
let res = client.get_block_info(&cp.hash())?;
|
||||
if res.confirmations < 0 {
|
||||
// block is not in best chain
|
||||
continue;
|
||||
}
|
||||
for cp in emitter.last_cp.iter() {
|
||||
let res = match client.get_block_info(&cp.hash()) {
|
||||
// block not in best chain
|
||||
Ok(res) if res.confirmations < 0 => continue,
|
||||
Ok(res) => res,
|
||||
Err(e) if e.is_not_found_error() => {
|
||||
if cp.height() > 0 {
|
||||
continue;
|
||||
}
|
||||
// if we can't find genesis block, we can't create an update that connects
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// agreement point found
|
||||
return Ok(PollResponse::AgreementFound(res, cp));
|
||||
}
|
||||
|
||||
Ok(PollResponse::AgreementPointNotFound)
|
||||
let genesis_hash = client.get_block_hash(0)?;
|
||||
Ok(PollResponse::AgreementPointNotFound(genesis_hash))
|
||||
}
|
||||
|
||||
fn poll<C, V, F>(
|
||||
@ -222,25 +215,12 @@ where
|
||||
let hash = res.hash;
|
||||
let item = get_item(&hash)?;
|
||||
|
||||
let this_id = BlockId { height, hash };
|
||||
let prev_id = res.previousblockhash.map(|prev_hash| BlockId {
|
||||
height: height - 1,
|
||||
hash: prev_hash,
|
||||
});
|
||||
|
||||
match (&mut emitter.last_cp, prev_id) {
|
||||
(Some(cp), _) => *cp = cp.clone().push(this_id).expect("must push"),
|
||||
(last_cp, None) => *last_cp = Some(CheckPoint::new(this_id)),
|
||||
// When the receiver constructs a local_chain update from a block, the previous
|
||||
// checkpoint is also included in the update. We need to reflect this state in
|
||||
// `Emitter::last_cp` as well.
|
||||
(last_cp, Some(prev_id)) => {
|
||||
*last_cp = Some(CheckPoint::new(prev_id).push(this_id).expect("must push"))
|
||||
}
|
||||
}
|
||||
|
||||
emitter.last_cp = emitter
|
||||
.last_cp
|
||||
.clone()
|
||||
.push(BlockId { height, hash })
|
||||
.expect("must push");
|
||||
emitter.last_block = Some(res);
|
||||
|
||||
return Ok(Some((height, item)));
|
||||
}
|
||||
PollResponse::NoMoreBlocks => {
|
||||
@ -254,9 +234,6 @@ where
|
||||
PollResponse::AgreementFound(res, cp) => {
|
||||
let agreement_h = res.height as u32;
|
||||
|
||||
// get rid of evicted blocks
|
||||
emitter.last_cp = Some(cp);
|
||||
|
||||
// The tip during the last mempool emission needs to in the best chain, we reduce
|
||||
// it if it is not.
|
||||
if let Some(h) = emitter.last_mempool_tip.as_mut() {
|
||||
@ -264,15 +241,17 @@ where
|
||||
*h = agreement_h;
|
||||
}
|
||||
}
|
||||
|
||||
// get rid of evicted blocks
|
||||
emitter.last_cp = cp;
|
||||
emitter.last_block = Some(res);
|
||||
continue;
|
||||
}
|
||||
PollResponse::AgreementPointNotFound => {
|
||||
// We want to clear `last_cp` and set `start_height` to the first checkpoint's
|
||||
// height. This way, the first checkpoint in `LocalChain` can be replaced.
|
||||
if let Some(last_cp) = emitter.last_cp.take() {
|
||||
emitter.start_height = last_cp.height();
|
||||
}
|
||||
PollResponse::AgreementPointNotFound(genesis_hash) => {
|
||||
emitter.last_cp = CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: genesis_hash,
|
||||
});
|
||||
emitter.last_block = None;
|
||||
continue;
|
||||
}
|
||||
|
@ -188,8 +188,8 @@ fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Up
|
||||
#[test]
|
||||
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let mut local_chain = LocalChain::default();
|
||||
let mut emitter = Emitter::from_height(&env.client, 0);
|
||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
|
||||
|
||||
// mine some blocks and returned the actual block hashes
|
||||
let exp_hashes = {
|
||||
@ -296,7 +296,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
env.mine_blocks(101, None)?;
|
||||
println!("mined blocks!");
|
||||
|
||||
let mut chain = LocalChain::default();
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||
let mut index = SpkTxOutIndex::<usize>::default();
|
||||
index.insert_spk(0, addr_0.script_pubkey());
|
||||
@ -305,7 +305,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
index
|
||||
});
|
||||
|
||||
let emitter = &mut Emitter::from_height(&env.client, 0);
|
||||
let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
|
||||
|
||||
while let Some((height, block)) = emitter.next_block()? {
|
||||
let _ = chain.apply_update(block_to_chain_update(&block, height))?;
|
||||
@ -393,7 +393,14 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
||||
const CHAIN_TIP_HEIGHT: usize = 110;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::from_height(&env.client, EMITTER_START_HEIGHT as _);
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
EMITTER_START_HEIGHT as _,
|
||||
);
|
||||
|
||||
env.mine_blocks(CHAIN_TIP_HEIGHT, None)?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
@ -442,9 +449,7 @@ fn get_balance(
|
||||
recv_chain: &LocalChain,
|
||||
recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
|
||||
) -> anyhow::Result<Balance> {
|
||||
let chain_tip = recv_chain
|
||||
.tip()
|
||||
.map_or(BlockId::default(), |cp| cp.block_id());
|
||||
let chain_tip = recv_chain.tip().block_id();
|
||||
let outpoints = recv_graph.index.outpoints().clone();
|
||||
let balance = recv_graph
|
||||
.graph()
|
||||
@ -461,7 +466,14 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::from_height(&env.client, 0);
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// setup addresses
|
||||
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
|
||||
@ -469,7 +481,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
||||
|
||||
// setup receiver
|
||||
let mut recv_chain = LocalChain::default();
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
@ -542,7 +554,14 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
|
||||
const MEMPOOL_TX_COUNT: usize = 2;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::from_height(&env.client, 0);
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks and sync up emitter
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
@ -597,7 +616,14 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()
|
||||
const MEMPOOL_TX_COUNT: usize = 21;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::from_height(&env.client, 0);
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance, sync emitter up to tip
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
@ -674,7 +700,14 @@ fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
const PREMINE_COUNT: usize = 101;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::from_height(&env.client, 0);
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
@ -789,7 +822,14 @@ fn no_agreement_point() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
|
||||
// start height is 99
|
||||
let mut emitter = Emitter::from_height(&env.client, (PREMINE_COUNT - 2) as u32);
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
(PREMINE_COUNT - 2) as u32,
|
||||
);
|
||||
|
||||
// mine 101 blocks
|
||||
env.mine_blocks(PREMINE_COUNT, None)?;
|
||||
|
@ -21,5 +21,5 @@ pub trait ChainOracle {
|
||||
) -> Result<Option<bool>, Self::Error>;
|
||||
|
||||
/// Get the best chain's chain tip.
|
||||
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error>;
|
||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error>;
|
||||
}
|
||||
|
@ -179,9 +179,9 @@ pub struct Update {
|
||||
}
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalChain {
|
||||
tip: Option<CheckPoint>,
|
||||
tip: CheckPoint,
|
||||
index: BTreeMap<u32, BlockHash>,
|
||||
}
|
||||
|
||||
@ -197,12 +197,6 @@ impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
|
||||
Self::from_blocks(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
type Error = Infallible;
|
||||
|
||||
@ -225,39 +219,71 @@ impl ChainOracle for LocalChain {
|
||||
)
|
||||
}
|
||||
|
||||
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> {
|
||||
Ok(self.tip.as_ref().map(|tip| tip.block_id()))
|
||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
||||
Ok(self.tip.block_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalChain {
|
||||
/// Get the genesis hash.
|
||||
pub fn genesis_hash(&self) -> BlockHash {
|
||||
self.index.get(&0).copied().expect("must have genesis hash")
|
||||
}
|
||||
|
||||
/// Construct [`LocalChain`] from genesis `hash`.
|
||||
#[must_use]
|
||||
pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
|
||||
let height = 0;
|
||||
let chain = Self {
|
||||
tip: CheckPoint::new(BlockId { height, hash }),
|
||||
index: core::iter::once((height, hash)).collect(),
|
||||
};
|
||||
let changeset = chain.initial_changeset();
|
||||
(chain, changeset)
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`] from an initial `changeset`.
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Self {
|
||||
let mut chain = Self::default();
|
||||
chain.apply_changeset(&changeset);
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
|
||||
let genesis_entry = changeset.get(&0).copied().flatten();
|
||||
let genesis_hash = match genesis_entry {
|
||||
Some(hash) => hash,
|
||||
None => return Err(MissingGenesisError),
|
||||
};
|
||||
|
||||
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&changeset)?;
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
||||
|
||||
chain
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||
pub fn from_tip(tip: CheckPoint) -> Self {
|
||||
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
||||
let mut chain = Self {
|
||||
tip: Some(tip),
|
||||
..Default::default()
|
||||
tip,
|
||||
index: BTreeMap::new(),
|
||||
};
|
||||
chain.reindex(0);
|
||||
|
||||
if chain.index.get(&0).copied().is_none() {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
chain
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||
///
|
||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||
/// all of the same chain.
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Self {
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
||||
if !blocks.contains_key(&0) {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
let mut tip: Option<CheckPoint> = None;
|
||||
|
||||
for block in &blocks {
|
||||
@ -272,25 +298,20 @@ impl LocalChain {
|
||||
}
|
||||
}
|
||||
|
||||
let chain = Self { index: blocks, tip };
|
||||
let chain = Self {
|
||||
index: blocks,
|
||||
tip: tip.expect("already checked to have genesis"),
|
||||
};
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
|
||||
chain
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Get the highest checkpoint.
|
||||
pub fn tip(&self) -> Option<CheckPoint> {
|
||||
pub fn tip(&self) -> CheckPoint {
|
||||
self.tip.clone()
|
||||
}
|
||||
|
||||
/// Returns whether the [`LocalChain`] is empty (has no checkpoints).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let res = self.tip.is_none();
|
||||
debug_assert_eq!(res, self.index.is_empty());
|
||||
res
|
||||
}
|
||||
|
||||
/// Applies the given `update` to the chain.
|
||||
///
|
||||
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
|
||||
@ -312,34 +333,28 @@ impl LocalChain {
|
||||
///
|
||||
/// [module-level documentation]: crate::local_chain
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
|
||||
match self.tip() {
|
||||
Some(original_tip) => {
|
||||
let changeset = merge_chains(
|
||||
original_tip,
|
||||
update.tip.clone(),
|
||||
update.introduce_older_blocks,
|
||||
)?;
|
||||
self.apply_changeset(&changeset);
|
||||
|
||||
// return early as `apply_changeset` already calls `check_consistency`
|
||||
Ok(changeset)
|
||||
}
|
||||
None => {
|
||||
*self = Self::from_tip(update.tip);
|
||||
let changeset = self.initial_changeset();
|
||||
|
||||
debug_assert!(self._check_index_is_consistent_with_tip());
|
||||
debug_assert!(self._check_changeset_is_applied(&changeset));
|
||||
Ok(changeset)
|
||||
}
|
||||
}
|
||||
let changeset = merge_chains(
|
||||
self.tip.clone(),
|
||||
update.tip.clone(),
|
||||
update.introduce_older_blocks,
|
||||
)?;
|
||||
// `._check_index_is_consistent_with_tip` and `._check_changeset_is_applied` is called in
|
||||
// `.apply_changeset`
|
||||
self.apply_changeset(&changeset)
|
||||
.map_err(|_| CannotConnectError {
|
||||
try_include_height: 0,
|
||||
})?;
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) {
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
// changes after point of agreement
|
||||
let mut extension = BTreeMap::default();
|
||||
// point of agreement
|
||||
let mut base: Option<CheckPoint> = None;
|
||||
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() >= start_height {
|
||||
extension.insert(cp.height(), cp.hash());
|
||||
@ -359,12 +374,12 @@ impl LocalChain {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let new_tip = match base {
|
||||
Some(base) => Some(
|
||||
base.extend(extension.into_iter().map(BlockId::from))
|
||||
.expect("extension is strictly greater than base"),
|
||||
),
|
||||
None => LocalChain::from_blocks(extension).tip(),
|
||||
Some(base) => base
|
||||
.extend(extension.into_iter().map(BlockId::from))
|
||||
.expect("extension is strictly greater than base"),
|
||||
None => LocalChain::from_blocks(extension)?.tip(),
|
||||
};
|
||||
self.tip = new_tip;
|
||||
self.reindex(start_height);
|
||||
@ -372,6 +387,8 @@ impl LocalChain {
|
||||
debug_assert!(self._check_index_is_consistent_with_tip());
|
||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert a [`BlockId`].
|
||||
@ -379,13 +396,13 @@ impl LocalChain {
|
||||
/// # Errors
|
||||
///
|
||||
/// Replacing the block hash of an existing checkpoint will result in an error.
|
||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, InsertBlockError> {
|
||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
|
||||
if let Some(&original_hash) = self.index.get(&block_id.height) {
|
||||
if original_hash != block_id.hash {
|
||||
return Err(InsertBlockError {
|
||||
return Err(AlterCheckPointError {
|
||||
height: block_id.height,
|
||||
original_hash,
|
||||
update_hash: block_id.hash,
|
||||
update_hash: Some(block_id.hash),
|
||||
});
|
||||
} else {
|
||||
return Ok(ChangeSet::default());
|
||||
@ -394,7 +411,12 @@ impl LocalChain {
|
||||
|
||||
let mut changeset = ChangeSet::default();
|
||||
changeset.insert(block_id.height, Some(block_id.hash));
|
||||
self.apply_changeset(&changeset);
|
||||
self.apply_changeset(&changeset)
|
||||
.map_err(|_| AlterCheckPointError {
|
||||
height: 0,
|
||||
original_hash: self.genesis_hash(),
|
||||
update_hash: changeset.get(&0).cloned().flatten(),
|
||||
})?;
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
@ -418,7 +440,7 @@ impl LocalChain {
|
||||
/// Iterate over checkpoints in descending height order.
|
||||
pub fn iter_checkpoints(&self) -> CheckPointIter {
|
||||
CheckPointIter {
|
||||
current: self.tip.as_ref().map(|tip| tip.0.clone()),
|
||||
current: Some(self.tip.0.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,7 +453,6 @@ impl LocalChain {
|
||||
let tip_history = self
|
||||
.tip
|
||||
.iter()
|
||||
.flat_map(CheckPoint::iter)
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
self.index == tip_history
|
||||
@ -447,29 +468,52 @@ impl LocalChain {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a failure when trying to insert a checkpoint into [`LocalChain`].
|
||||
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct InsertBlockError {
|
||||
/// The checkpoints' height.
|
||||
pub height: u32,
|
||||
/// Original checkpoint's block hash.
|
||||
pub original_hash: BlockHash,
|
||||
/// Update checkpoint's block hash.
|
||||
pub update_hash: BlockHash,
|
||||
}
|
||||
pub struct MissingGenesisError;
|
||||
|
||||
impl core::fmt::Display for InsertBlockError {
|
||||
impl core::fmt::Display for MissingGenesisError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"failed to insert block at height {} as block hashes conflict: original={}, update={}",
|
||||
self.height, self.original_hash, self.update_hash
|
||||
"cannot construct `LocalChain` without a genesis checkpoint"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InsertBlockError {}
|
||||
impl std::error::Error for MissingGenesisError {}
|
||||
|
||||
/// Represents a failure when trying to insert/remove a checkpoint to/from [`LocalChain`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AlterCheckPointError {
|
||||
/// The checkpoint's height.
|
||||
pub height: u32,
|
||||
/// The original checkpoint's block hash which cannot be replaced/removed.
|
||||
pub original_hash: BlockHash,
|
||||
/// The attempted update to the `original_block` hash.
|
||||
pub update_hash: Option<BlockHash>,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for AlterCheckPointError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self.update_hash {
|
||||
Some(update_hash) => write!(
|
||||
f,
|
||||
"failed to insert block at height {}: original={} update={}",
|
||||
self.height, self.original_hash, update_hash
|
||||
),
|
||||
None => write!(
|
||||
f,
|
||||
"failed to remove block at height {}: original={}",
|
||||
self.height, self.original_hash
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AlterCheckPointError {}
|
||||
|
||||
/// Occurs when an update does not have a common checkpoint with the original chain.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
@ -79,10 +79,10 @@ pub trait PersistBackend<C> {
|
||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
|
||||
|
||||
/// Return the aggregate changeset `C` from persistence.
|
||||
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError>;
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
|
||||
}
|
||||
|
||||
impl<C: Default> PersistBackend<C> for () {
|
||||
impl<C> PersistBackend<C> for () {
|
||||
type WriteError = Infallible;
|
||||
|
||||
type LoadError = Infallible;
|
||||
@ -91,7 +91,7 @@ impl<C: Default> PersistBackend<C> for () {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
|
||||
Ok(C::default())
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
}};
|
||||
}
|
||||
|
||||
@ -32,8 +33,8 @@ macro_rules! chain_update {
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::Update {
|
||||
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.tip()
|
||||
.expect("must have tip"),
|
||||
.expect("chain must have genesis block")
|
||||
.tip(),
|
||||
introduce_older_blocks: true,
|
||||
}
|
||||
}};
|
||||
|
@ -1,7 +1,7 @@
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
@ -9,9 +9,7 @@ use bdk_chain::{
|
||||
local_chain::LocalChain,
|
||||
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bitcoin::{
|
||||
secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
|
||||
};
|
||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
||||
@ -112,11 +110,8 @@ fn insert_relevant_txs() {
|
||||
|
||||
fn test_list_owned_txouts() {
|
||||
// Create Local chains
|
||||
let local_chain = LocalChain::from(
|
||||
(0..150)
|
||||
.map(|i| (i as u32, h!("random")))
|
||||
.collect::<BTreeMap<u32, BlockHash>>(),
|
||||
);
|
||||
let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
|
||||
.expect("must have genesis hash");
|
||||
|
||||
// Initiate IndexedTxGraph
|
||||
|
||||
|
@ -312,7 +312,7 @@ fn test_wildcard_derivations() {
|
||||
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
|
||||
|
||||
(0..=15)
|
||||
.chain(vec![17, 20, 23].into_iter())
|
||||
.chain([17, 20, 23])
|
||||
.for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index)));
|
||||
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
|
||||
|
@ -1,4 +1,6 @@
|
||||
use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update};
|
||||
use bdk_chain::local_chain::{
|
||||
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
|
||||
};
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
#[macro_use]
|
||||
@ -68,10 +70,10 @@ fn update_local_chain() {
|
||||
[
|
||||
TestLocalChain {
|
||||
name: "add first tip",
|
||||
chain: local_chain![],
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("A")))],
|
||||
changeset: &[],
|
||||
init_changeset: &[(0, Some(h!("A")))],
|
||||
},
|
||||
},
|
||||
@ -86,18 +88,18 @@ fn update_local_chain() {
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(1, h!("B"))],
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 0,
|
||||
try_include_height: 1,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge (existing chain longer)",
|
||||
chain: local_chain![(1, h!("A"))],
|
||||
update: chain_update![(0, h!("B"))],
|
||||
chain: local_chain![(0, h!("_")), (2, h!("A"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 1,
|
||||
try_include_height: 2,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
@ -111,54 +113,54 @@ fn update_local_chain() {
|
||||
},
|
||||
// Introduce an older checkpoint (B)
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | C D
|
||||
// update | B C
|
||||
// chain | _ C D
|
||||
// update | _ B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint",
|
||||
chain: local_chain![(2, h!("C")), (3, h!("D"))],
|
||||
update: chain_update![(1, h!("B")), (2, h!("C"))],
|
||||
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("B")), (2, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
|
||||
},
|
||||
},
|
||||
// Introduce an older checkpoint (A) that is not directly behind PoA
|
||||
// | 2 | 3 | 4
|
||||
// chain | B C
|
||||
// update | A C
|
||||
// | 0 | 2 | 3 | 4
|
||||
// chain | _ B C
|
||||
// update | _ A C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 2",
|
||||
chain: local_chain![(3, h!("B")), (4, h!("C"))],
|
||||
update: chain_update![(2, h!("A")), (4, h!("C"))],
|
||||
chain: local_chain![(0, h!("_")), (3, h!("B")), (4, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("A")), (4, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("A")))],
|
||||
init_changeset: &[(2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce an older checkpoint (B) that is not the oldest checkpoint
|
||||
// | 1 | 2 | 3
|
||||
// chain | A C
|
||||
// update | B C
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | _ A C
|
||||
// update | _ B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 3",
|
||||
chain: local_chain![(1, h!("A")), (3, h!("C"))],
|
||||
update: chain_update![(2, h!("B")), (3, h!("C"))],
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce two older checkpoints below the PoA
|
||||
// | 1 | 2 | 3
|
||||
// chain | C
|
||||
// update | A B C
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | _ C
|
||||
// update | _ A B C
|
||||
TestLocalChain {
|
||||
name: "introduce two older checkpoints below PoA",
|
||||
chain: local_chain![(3, h!("C"))],
|
||||
update: chain_update![(1, h!("A")), (2, h!("B")), (3, h!("C"))],
|
||||
chain: local_chain![(0, h!("_")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
@ -172,45 +174,46 @@ fn update_local_chain() {
|
||||
},
|
||||
// B and C are in both chain and update
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | B C
|
||||
// update | A B C D
|
||||
// chain | _ B C
|
||||
// update | _ A B C D
|
||||
// This should succeed with the point of agreement being C and A should be added in addition.
|
||||
TestLocalChain {
|
||||
name: "two points of agreement",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))],
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("A"))), (3, Some(h!("D")))],
|
||||
changeset: &[(1, Some(h!("A"))), (4, Some(h!("D")))],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(1, Some(h!("B"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D"))),
|
||||
(0, Some(h!("_"))),
|
||||
(1, Some(h!("A"))),
|
||||
(2, Some(h!("B"))),
|
||||
(3, Some(h!("C"))),
|
||||
(4, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Update and chain does not connect:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | B C
|
||||
// update | A B D
|
||||
// chain | _ B C
|
||||
// update | _ A B D
|
||||
// This should fail as we cannot figure out whether C & D are on the same chain
|
||||
TestLocalChain {
|
||||
name: "update and chain does not connect",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B")), (3, h!("D"))],
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 2,
|
||||
try_include_height: 3,
|
||||
}),
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A B C E
|
||||
// update | A B' C' D
|
||||
// chain | _ B C E
|
||||
// update | _ B' C' D
|
||||
// This should succeed and invalidate B,C and E with point of agreement being A.
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation",
|
||||
chain: local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
||||
update: chain_update![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(2, Some(h!("B'"))),
|
||||
@ -219,7 +222,7 @@ fn update_local_chain() {
|
||||
(5, None),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(0, Some(h!("_"))),
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
@ -228,13 +231,13 @@ fn update_local_chain() {
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | B C E
|
||||
// update | B' C' D
|
||||
// chain | _ B C E
|
||||
// update | _ B' C' D
|
||||
// This should succeed and invalidate B, C and E with no point of agreement
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
chain: local_chain![(0, h!("_")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(1, Some(h!("B'"))),
|
||||
@ -243,6 +246,7 @@ fn update_local_chain() {
|
||||
(4, None)
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("_"))),
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
@ -250,16 +254,16 @@ fn update_local_chain() {
|
||||
},
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | A B C E
|
||||
// update | B' C' D
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | _ A B C E
|
||||
// update | _ B' C' D
|
||||
// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
|
||||
// A was invalid.
|
||||
TestLocalChain {
|
||||
name: "invalidation but no connection",
|
||||
chain: local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 0 }),
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 1 }),
|
||||
},
|
||||
// Introduce blocks between two points of agreement
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
@ -294,44 +298,44 @@ fn local_chain_insert_block() {
|
||||
struct TestCase {
|
||||
original: LocalChain,
|
||||
insert: (u32, BlockHash),
|
||||
expected_result: Result<ChangeSet, InsertBlockError>,
|
||||
expected_result: Result<ChangeSet, AlterCheckPointError>,
|
||||
expected_final: LocalChain,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
original: local_chain![],
|
||||
original: local_chain![(0, h!("_"))],
|
||||
insert: (5, h!("block5")),
|
||||
expected_result: Ok([(5, Some(h!("block5")))].into()),
|
||||
expected_final: local_chain![(5, h!("block5"))],
|
||||
expected_final: local_chain![(0, h!("_")), (5, h!("block5"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(3, h!("A"))],
|
||||
original: local_chain![(0, h!("_")), (3, h!("A"))],
|
||||
insert: (4, h!("B")),
|
||||
expected_result: Ok([(4, Some(h!("B")))].into()),
|
||||
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
|
||||
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(4, h!("B"))],
|
||||
original: local_chain![(0, h!("_")), (4, h!("B"))],
|
||||
insert: (3, h!("A")),
|
||||
expected_result: Ok([(3, Some(h!("A")))].into()),
|
||||
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
|
||||
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(2, h!("K"))],
|
||||
original: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
insert: (2, h!("K")),
|
||||
expected_result: Ok([].into()),
|
||||
expected_final: local_chain![(2, h!("K"))],
|
||||
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(2, h!("K"))],
|
||||
original: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
insert: (2, h!("J")),
|
||||
expected_result: Err(InsertBlockError {
|
||||
expected_result: Err(AlterCheckPointError {
|
||||
height: 2,
|
||||
original_hash: h!("K"),
|
||||
update_hash: h!("J"),
|
||||
update_hash: Some(h!("J")),
|
||||
}),
|
||||
expected_final: local_chain![(2, h!("K"))],
|
||||
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -511,11 +511,13 @@ fn test_calculate_fee_on_coinbase() {
|
||||
// where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc.
|
||||
#[test]
|
||||
fn test_walk_ancestors() {
|
||||
let local_chain: LocalChain = (0..=20)
|
||||
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
|
||||
.collect::<BTreeMap<u32, BlockHash>>()
|
||||
.into();
|
||||
let tip = local_chain.tip().expect("must have tip");
|
||||
let local_chain = LocalChain::from_blocks(
|
||||
(0..=20)
|
||||
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
|
||||
.collect(),
|
||||
)
|
||||
.expect("must contain genesis hash");
|
||||
let tip = local_chain.tip();
|
||||
|
||||
let tx_a0 = Transaction {
|
||||
input: vec![TxIn {
|
||||
@ -839,11 +841,13 @@ fn test_descendants_no_repeat() {
|
||||
|
||||
#[test]
|
||||
fn test_chain_spends() {
|
||||
let local_chain: LocalChain = (0..=100)
|
||||
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
|
||||
.collect::<BTreeMap<u32, BlockHash>>()
|
||||
.into();
|
||||
let tip = local_chain.tip().expect("must have tip");
|
||||
let local_chain = LocalChain::from_blocks(
|
||||
(0..=100)
|
||||
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
|
||||
.collect(),
|
||||
)
|
||||
.expect("must have genesis hash");
|
||||
let tip = local_chain.tip();
|
||||
|
||||
// The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
|
||||
// The parent tx is confirmed at block 95.
|
||||
@ -906,18 +910,15 @@ fn test_chain_spends() {
|
||||
let _ = graph.insert_tx(tx_1.clone());
|
||||
let _ = graph.insert_tx(tx_2.clone());
|
||||
|
||||
[95, 98]
|
||||
.iter()
|
||||
.zip([&tx_0, &tx_1].into_iter())
|
||||
.for_each(|(ht, tx)| {
|
||||
let _ = graph.insert_anchor(
|
||||
tx.txid(),
|
||||
ConfirmationHeightAnchor {
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: *ht,
|
||||
},
|
||||
);
|
||||
});
|
||||
for (ht, tx) in [(95, &tx_0), (98, &tx_1)] {
|
||||
let _ = graph.insert_anchor(
|
||||
tx.txid(),
|
||||
ConfirmationHeightAnchor {
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: ht,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Assert that confirmed spends are returned correctly.
|
||||
assert_eq!(
|
||||
@ -1078,7 +1079,7 @@ fn test_missing_blocks() {
|
||||
g
|
||||
},
|
||||
chain: {
|
||||
let mut c = LocalChain::default();
|
||||
let (mut c, _) = LocalChain::from_genesis_hash(h!("genesis"));
|
||||
for (height, hash) in chain {
|
||||
let _ = c.insert_block(BlockId {
|
||||
height: *height,
|
||||
|
@ -39,10 +39,7 @@ fn test_tx_conflict_handling() {
|
||||
(5, h!("F")),
|
||||
(6, h!("G"))
|
||||
);
|
||||
let chain_tip = local_chain
|
||||
.tip()
|
||||
.map(|cp| cp.block_id())
|
||||
.unwrap_or_default();
|
||||
let chain_tip = local_chain.tip().block_id();
|
||||
|
||||
let scenarios = [
|
||||
Scenario {
|
||||
|
@ -148,7 +148,7 @@ pub trait ElectrumExt {
|
||||
/// single batch request.
|
||||
fn scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: Option<CheckPoint>,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
@ -161,7 +161,7 @@ pub trait ElectrumExt {
|
||||
/// [`scan`]: ElectrumExt::scan
|
||||
fn scan_without_keychain(
|
||||
&self,
|
||||
prev_tip: Option<CheckPoint>,
|
||||
prev_tip: CheckPoint,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
@ -188,7 +188,7 @@ pub trait ElectrumExt {
|
||||
impl ElectrumExt for Client {
|
||||
fn scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: Option<CheckPoint>,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
@ -289,17 +289,15 @@ impl ElectrumExt for Client {
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||
fn construct_update_tip(
|
||||
client: &Client,
|
||||
prev_tip: Option<CheckPoint>,
|
||||
prev_tip: CheckPoint,
|
||||
) -> Result<(CheckPoint, Option<u32>), Error> {
|
||||
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
||||
let new_tip_height = height as u32;
|
||||
|
||||
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
|
||||
// not need updating. We just return the previous tip and use that as the point of agreement.
|
||||
if let Some(prev_tip) = prev_tip.as_ref() {
|
||||
if new_tip_height < prev_tip.height() {
|
||||
return Ok((prev_tip.clone(), Some(prev_tip.height())));
|
||||
}
|
||||
if new_tip_height < prev_tip.height() {
|
||||
return Ok((prev_tip.clone(), Some(prev_tip.height())));
|
||||
}
|
||||
|
||||
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
|
||||
@ -317,7 +315,7 @@ fn construct_update_tip(
|
||||
// Find the "point of agreement" (if any).
|
||||
let agreement_cp = {
|
||||
let mut agreement_cp = Option::<CheckPoint>::None;
|
||||
for cp in prev_tip.iter().flat_map(CheckPoint::iter) {
|
||||
for cp in prev_tip.iter() {
|
||||
let cp_block = cp.block_id();
|
||||
let hash = match new_blocks.get(&cp_block.height) {
|
||||
Some(&hash) => hash,
|
||||
|
@ -32,7 +32,7 @@ pub trait EsploraAsyncExt {
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: Option<CheckPoint>,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error>;
|
||||
|
||||
@ -95,7 +95,7 @@ pub trait EsploraAsyncExt {
|
||||
impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: Option<CheckPoint>,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
|
||||
@ -129,41 +129,39 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
let earliest_agreement_cp = {
|
||||
let mut earliest_agreement_cp = Option::<CheckPoint>::None;
|
||||
|
||||
if let Some(local_tip) = local_tip {
|
||||
let local_tip_height = local_tip.height();
|
||||
for local_cp in local_tip.iter() {
|
||||
let local_block = local_cp.block_id();
|
||||
let local_tip_height = local_tip.height();
|
||||
for local_cp in local_tip.iter() {
|
||||
let local_block = local_cp.block_id();
|
||||
|
||||
// the updated hash (block hash at this height after the update), can either be:
|
||||
// 1. a block that already existed in `fetched_blocks`
|
||||
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
|
||||
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
|
||||
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
|
||||
// remote tip
|
||||
let updated_hash = match fetched_blocks.entry(local_block.height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(
|
||||
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
|
||||
local_block.hash
|
||||
} else {
|
||||
self.get_block_hash(local_block.height).await?
|
||||
},
|
||||
),
|
||||
};
|
||||
// the updated hash (block hash at this height after the update), can either be:
|
||||
// 1. a block that already existed in `fetched_blocks`
|
||||
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
|
||||
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
|
||||
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
|
||||
// remote tip
|
||||
let updated_hash = match fetched_blocks.entry(local_block.height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(
|
||||
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
|
||||
local_block.hash
|
||||
} else {
|
||||
self.get_block_hash(local_block.height).await?
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
// since we may introduce blocks below the point of agreement, we cannot break
|
||||
// here unconditionally - we only break if we guarantee there are no new heights
|
||||
// below our current local checkpoint
|
||||
if local_block.hash == updated_hash {
|
||||
earliest_agreement_cp = Some(local_cp);
|
||||
// since we may introduce blocks below the point of agreement, we cannot break
|
||||
// here unconditionally - we only break if we guarantee there are no new heights
|
||||
// below our current local checkpoint
|
||||
if local_block.hash == updated_hash {
|
||||
earliest_agreement_cp = Some(local_cp);
|
||||
|
||||
let first_new_height = *fetched_blocks
|
||||
.keys()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
if first_new_height >= local_block.height {
|
||||
break;
|
||||
}
|
||||
let first_new_height = *fetched_blocks
|
||||
.keys()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
if first_new_height >= local_block.height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ pub trait EsploraExt {
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn update_local_chain(
|
||||
&self,
|
||||
local_tip: Option<CheckPoint>,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<Item = u32>,
|
||||
) -> Result<local_chain::Update, Error>;
|
||||
|
||||
@ -87,7 +87,7 @@ pub trait EsploraExt {
|
||||
impl EsploraExt for esplora_client::BlockingClient {
|
||||
fn update_local_chain(
|
||||
&self,
|
||||
local_tip: Option<CheckPoint>,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<Item = u32>,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
let request_heights = request_heights.into_iter().collect::<BTreeSet<_>>();
|
||||
@ -120,41 +120,39 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
let earliest_agreement_cp = {
|
||||
let mut earliest_agreement_cp = Option::<CheckPoint>::None;
|
||||
|
||||
if let Some(local_tip) = local_tip {
|
||||
let local_tip_height = local_tip.height();
|
||||
for local_cp in local_tip.iter() {
|
||||
let local_block = local_cp.block_id();
|
||||
let local_tip_height = local_tip.height();
|
||||
for local_cp in local_tip.iter() {
|
||||
let local_block = local_cp.block_id();
|
||||
|
||||
// the updated hash (block hash at this height after the update), can either be:
|
||||
// 1. a block that already existed in `fetched_blocks`
|
||||
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
|
||||
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
|
||||
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
|
||||
// remote tip
|
||||
let updated_hash = match fetched_blocks.entry(local_block.height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(
|
||||
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
|
||||
local_block.hash
|
||||
} else {
|
||||
self.get_block_hash(local_block.height)?
|
||||
},
|
||||
),
|
||||
};
|
||||
// the updated hash (block hash at this height after the update), can either be:
|
||||
// 1. a block that already existed in `fetched_blocks`
|
||||
// 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH
|
||||
// 3. otherwise we can freshly fetch the block from remote, which is safe as it
|
||||
// is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the
|
||||
// remote tip
|
||||
let updated_hash = match fetched_blocks.entry(local_block.height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(
|
||||
if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH {
|
||||
local_block.hash
|
||||
} else {
|
||||
self.get_block_hash(local_block.height)?
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
// since we may introduce blocks below the point of agreement, we cannot break
|
||||
// here unconditionally - we only break if we guarantee there are no new heights
|
||||
// below our current local checkpoint
|
||||
if local_block.hash == updated_hash {
|
||||
earliest_agreement_cp = Some(local_cp);
|
||||
// since we may introduce blocks below the point of agreement, we cannot break
|
||||
// here unconditionally - we only break if we guarantee there are no new heights
|
||||
// below our current local checkpoint
|
||||
if local_block.hash == updated_hash {
|
||||
earliest_agreement_cp = Some(local_cp);
|
||||
|
||||
let first_new_height = *fetched_blocks
|
||||
.keys()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
if first_new_height >= local_block.height {
|
||||
break;
|
||||
}
|
||||
let first_new_height = *fetched_blocks
|
||||
.keys()
|
||||
.next()
|
||||
.expect("must have at least one new block");
|
||||
if first_new_height >= local_block.height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ pub struct Store<'a, C> {
|
||||
|
||||
impl<'a, C> PersistBackend<C> for Store<'a, C>
|
||||
where
|
||||
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
type WriteError = std::io::Error;
|
||||
|
||||
@ -33,30 +33,64 @@ where
|
||||
self.append_changeset(changeset)
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<C, Self::LoadError> {
|
||||
let (changeset, result) = self.aggregate_changesets();
|
||||
result.map(|_| changeset)
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
self.aggregate_changesets().map_err(|e| e.iter_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C> Store<'a, C>
|
||||
where
|
||||
C: Default + Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
/// Creates a new store from a [`File`].
|
||||
/// Create a new [`Store`] file in write-only mode; error if the file exists.
|
||||
///
|
||||
/// The file must have been opened with read and write permissions.
|
||||
/// `magic` is the prefixed bytes to write to the new file. This will be checked when opening
|
||||
/// the `Store` in the future with [`open`].
|
||||
///
|
||||
/// `magic` is the expected prefixed bytes of the file. If this does not match, an error will be
|
||||
/// returned.
|
||||
/// [`open`]: Store::open
|
||||
pub fn create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
if file_path.as_ref().exists() {
|
||||
// `io::Error` is used instead of a variant on `FileError` because there is already a
|
||||
// nightly-only `File::create_new` method
|
||||
return Err(FileError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"file already exists",
|
||||
)));
|
||||
}
|
||||
let mut f = OpenOptions::new()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(file_path)?;
|
||||
f.write_all(magic)?;
|
||||
Ok(Self {
|
||||
magic,
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open an existing [`Store`].
|
||||
///
|
||||
/// [`File`]: std::fs::File
|
||||
pub fn new(magic: &'a [u8], mut db_file: File) -> Result<Self, FileError> {
|
||||
db_file.rewind()?;
|
||||
/// Use [`create_new`] to create a new `Store`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the prefixed bytes of the opened file does not match the provided `magic`, the
|
||||
/// [`FileError::InvalidMagicBytes`] error variant will be returned.
|
||||
///
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut f = OpenOptions::new().read(true).write(true).open(file_path)?;
|
||||
|
||||
let mut magic_buf = vec![0_u8; magic.len()];
|
||||
db_file.read_exact(magic_buf.as_mut())?;
|
||||
|
||||
f.read_exact(&mut magic_buf)?;
|
||||
if magic_buf != magic {
|
||||
return Err(FileError::InvalidMagicBytes {
|
||||
got: magic_buf,
|
||||
@ -66,35 +100,26 @@ where
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
db_file,
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates or loads a store from `db_path`.
|
||||
/// Attempt to open existing [`Store`] file; create it if the file is non-existant.
|
||||
///
|
||||
/// If no file exists there, it will be created.
|
||||
/// Internally, this calls either [`open`] or [`create_new`].
|
||||
///
|
||||
/// Refer to [`new`] for documentation on the `magic` input.
|
||||
///
|
||||
/// [`new`]: Self::new
|
||||
pub fn new_from_path<P>(magic: &'a [u8], db_path: P) -> Result<Self, FileError>
|
||||
/// [`open`]: Store::open
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open_or_create_new<P>(magic: &'a [u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let already_exists = db_path.as_ref().exists();
|
||||
|
||||
let mut db_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(db_path)?;
|
||||
|
||||
if !already_exists {
|
||||
db_file.write_all(magic)?;
|
||||
if file_path.as_ref().exists() {
|
||||
Self::open(magic, file_path)
|
||||
} else {
|
||||
Self::create_new(magic, file_path)
|
||||
}
|
||||
|
||||
Self::new(magic, db_file)
|
||||
}
|
||||
|
||||
/// Iterates over the stored changeset from first to last, changing the seek position at each
|
||||
@ -122,16 +147,24 @@ where
|
||||
///
|
||||
/// **WARNING**: This method changes the write position of the underlying file. The next
|
||||
/// changeset will be written over the erroring entry (or the end of the file if none existed).
|
||||
pub fn aggregate_changesets(&mut self) -> (C, Result<(), IterError>) {
|
||||
let mut changeset = C::default();
|
||||
let result = (|| {
|
||||
for next_changeset in self.iter_changesets() {
|
||||
changeset.append(next_changeset?);
|
||||
pub fn aggregate_changesets(&mut self) -> Result<Option<C>, AggregateChangesetsError<C>> {
|
||||
let mut changeset = Option::<C>::None;
|
||||
for next_changeset in self.iter_changesets() {
|
||||
let next_changeset = match next_changeset {
|
||||
Ok(next_changeset) => next_changeset,
|
||||
Err(iter_error) => {
|
||||
return Err(AggregateChangesetsError {
|
||||
changeset,
|
||||
iter_error,
|
||||
})
|
||||
}
|
||||
};
|
||||
match &mut changeset {
|
||||
Some(changeset) => changeset.append(next_changeset),
|
||||
changeset => *changeset = Some(next_changeset),
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
(changeset, result)
|
||||
}
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Append a new changeset to the file and truncate the file to the end of the appended
|
||||
@ -162,6 +195,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for [`Store::aggregate_changesets`].
|
||||
#[derive(Debug)]
|
||||
pub struct AggregateChangesetsError<C> {
|
||||
/// The partially-aggregated changeset.
|
||||
pub changeset: Option<C>,
|
||||
|
||||
/// The error returned by [`EntryIter`].
|
||||
pub iter_error: IterError,
|
||||
}
|
||||
|
||||
impl<C> std::fmt::Display for AggregateChangesetsError<C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.iter_error, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
@ -182,13 +233,50 @@ mod test {
|
||||
#[derive(Debug)]
|
||||
struct TestTracker;
|
||||
|
||||
/// Check behavior of [`Store::create_new`] and [`Store::open`].
|
||||
#[test]
|
||||
fn construct_store() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("db_file");
|
||||
let _ = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect_err("must not open as file does not exist yet");
|
||||
let _ = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must create file");
|
||||
// cannot create new as file already exists
|
||||
let _ = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect_err("must fail as file already exists now");
|
||||
let _ = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must open as file exists now");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_or_create_new() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("db_file");
|
||||
let changeset = vec!["hello".to_string(), "world".to_string()];
|
||||
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must create");
|
||||
assert!(file_path.exists());
|
||||
db.append_changeset(&changeset).expect("must succeed");
|
||||
}
|
||||
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must recover");
|
||||
let recovered_changeset = db.aggregate_changesets().expect("must succeed");
|
||||
assert_eq!(recovered_changeset, Some(changeset));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_fails_if_file_is_too_short() {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(&TEST_MAGIC_BYTES[..TEST_MAGIC_BYTES_LEN - 1])
|
||||
.expect("should write");
|
||||
|
||||
match Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap()) {
|
||||
match Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()) {
|
||||
Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof),
|
||||
unexpected => panic!("unexpected result: {:?}", unexpected),
|
||||
};
|
||||
@ -202,7 +290,7 @@ mod test {
|
||||
file.write_all(invalid_magic_bytes.as_bytes())
|
||||
.expect("should write");
|
||||
|
||||
match Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap()) {
|
||||
match Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()) {
|
||||
Err(FileError::InvalidMagicBytes { got, .. }) => {
|
||||
assert_eq!(got, invalid_magic_bytes.as_bytes())
|
||||
}
|
||||
@ -221,8 +309,8 @@ mod test {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(&data).expect("should write");
|
||||
|
||||
let mut store = Store::<TestChangeSet>::new(&TEST_MAGIC_BYTES, file.reopen().unwrap())
|
||||
.expect("should open");
|
||||
let mut store =
|
||||
Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()).expect("should open");
|
||||
match store.iter_changesets().next() {
|
||||
Some(Err(IterError::Bincode(_))) => {}
|
||||
unexpected_res => panic!("unexpected result: {:?}", unexpected_res),
|
||||
|
@ -1 +0,0 @@
|
||||
|
@ -131,7 +131,7 @@ fn main() -> anyhow::Result<()> {
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
|
||||
let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0));
|
||||
let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)?);
|
||||
println!(
|
||||
"[{:>10}s] loaded local chain from changeset",
|
||||
start.elapsed().as_secs_f32()
|
||||
@ -170,10 +170,7 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
let chain_tip = chain.lock().unwrap().tip();
|
||||
let rpc_client = rpc_args.new_client()?;
|
||||
let mut emitter = match chain_tip {
|
||||
Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
|
||||
None => Emitter::from_height(&rpc_client, fallback_height),
|
||||
};
|
||||
let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
|
||||
|
||||
let mut last_db_commit = Instant::now();
|
||||
let mut last_print = Instant::now();
|
||||
@ -205,23 +202,22 @@ fn main() -> anyhow::Result<()> {
|
||||
// print synced-to height and current balance in intervals
|
||||
if last_print.elapsed() >= STDOUT_PRINT_DELAY {
|
||||
last_print = Instant::now();
|
||||
if let Some(synced_to) = chain.tip() {
|
||||
let balance = {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
println!(
|
||||
"[{:>10}s] synced to {} @ {} | total: {} sats",
|
||||
start.elapsed().as_secs_f32(),
|
||||
synced_to.hash(),
|
||||
synced_to.height(),
|
||||
balance.total()
|
||||
);
|
||||
}
|
||||
let synced_to = chain.tip();
|
||||
let balance = {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
println!(
|
||||
"[{:>10}s] synced to {} @ {} | total: {} sats",
|
||||
start.elapsed().as_secs_f32(),
|
||||
synced_to.hash(),
|
||||
synced_to.height(),
|
||||
balance.total()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,10 +249,7 @@ fn main() -> anyhow::Result<()> {
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND);
|
||||
let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> {
|
||||
let rpc_client = rpc_args.new_client()?;
|
||||
let mut emitter = match last_cp {
|
||||
Some(cp) => Emitter::from_checkpoint(&rpc_client, cp),
|
||||
None => Emitter::from_height(&rpc_client, fallback_height),
|
||||
};
|
||||
let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height);
|
||||
|
||||
let mut block_count = rpc_client.get_block_count()? as u32;
|
||||
tx.send(Emission::Tip(block_count))?;
|
||||
@ -335,24 +328,23 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY {
|
||||
last_print = Some(Instant::now());
|
||||
if let Some(synced_to) = chain.tip() {
|
||||
let balance = {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
println!(
|
||||
"[{:>10}s] synced to {} @ {} / {} | total: {} sats",
|
||||
start.elapsed().as_secs_f32(),
|
||||
synced_to.hash(),
|
||||
synced_to.height(),
|
||||
tip_height,
|
||||
balance.total()
|
||||
);
|
||||
}
|
||||
let synced_to = chain.tip();
|
||||
let balance = {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
println!(
|
||||
"[{:>10}s] synced to {} @ {} / {} | total: {} sats",
|
||||
start.elapsed().as_secs_f32(),
|
||||
synced_to.hash(),
|
||||
synced_to.height(),
|
||||
tip_height,
|
||||
balance.total()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -315,10 +315,8 @@ where
|
||||
version: 0x02,
|
||||
// because the temporary planning module does not support timelocks, we can use the chain
|
||||
// tip as the `lock_time` for anti-fee-sniping purposes
|
||||
lock_time: chain
|
||||
.get_chain_tip()?
|
||||
.and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok())
|
||||
.unwrap_or(absolute::LockTime::ZERO),
|
||||
lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
|
||||
.expect("invalid height"),
|
||||
input: selected_txos
|
||||
.iter()
|
||||
.map(|(_, utxo)| TxIn {
|
||||
@ -404,7 +402,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
|
||||
chain: &O,
|
||||
assets: &bdk_tmp_plan::Assets<K>,
|
||||
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
|
||||
let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
graph
|
||||
.graph()
|
||||
@ -509,7 +507,7 @@ where
|
||||
|
||||
let balance = graph.graph().try_balance(
|
||||
chain,
|
||||
chain.get_chain_tip()?.unwrap_or_default(),
|
||||
chain.get_chain_tip()?,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)?;
|
||||
@ -539,7 +537,7 @@ where
|
||||
Commands::TxOut { txout_cmd } => {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
|
||||
match txout_cmd {
|
||||
@ -683,13 +681,13 @@ where
|
||||
index.add_keychain(Keychain::Internal, internal_descriptor);
|
||||
}
|
||||
|
||||
let mut db_backend = match Store::<'m, C>::new_from_path(db_magic, &args.db_path) {
|
||||
let mut db_backend = match Store::<'m, C>::open_or_create_new(db_magic, &args.db_path) {
|
||||
Ok(db_backend) => db_backend,
|
||||
// we cannot return `err` directly as it has lifetime `'m`
|
||||
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
|
||||
};
|
||||
|
||||
let init_changeset = db_backend.load_from_persistence()?;
|
||||
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
|
||||
|
||||
Ok((
|
||||
args,
|
||||
|
@ -112,7 +112,7 @@ fn main() -> anyhow::Result<()> {
|
||||
graph
|
||||
});
|
||||
|
||||
let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain));
|
||||
let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)?);
|
||||
|
||||
let electrum_cmd = match &args.command {
|
||||
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
||||
@ -193,7 +193,7 @@ fn main() -> anyhow::Result<()> {
|
||||
// Get a short lock on the tracker to get the spks we're interested in
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
||||
unused_spks = true;
|
||||
|
@ -5,10 +5,10 @@ use std::{
|
||||
};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid},
|
||||
bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
local_chain::{self, LocalChain},
|
||||
Append, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
|
||||
@ -102,6 +102,8 @@ fn main() -> anyhow::Result<()> {
|
||||
let (args, keymap, index, db, init_changeset) =
|
||||
example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
|
||||
let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset;
|
||||
|
||||
// Contruct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in
|
||||
@ -113,8 +115,8 @@ fn main() -> anyhow::Result<()> {
|
||||
graph
|
||||
});
|
||||
let chain = Mutex::new({
|
||||
let mut chain = LocalChain::default();
|
||||
chain.apply_changeset(&init_chain_changeset);
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&init_chain_changeset)?;
|
||||
chain
|
||||
});
|
||||
|
||||
@ -234,7 +236,7 @@ fn main() -> anyhow::Result<()> {
|
||||
{
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
if *all_spks {
|
||||
let all_spks = graph
|
||||
@ -332,7 +334,7 @@ fn main() -> anyhow::Result<()> {
|
||||
(missing_block_heights, tip)
|
||||
};
|
||||
|
||||
println!("prev tip: {}", tip.as_ref().map_or(0, CheckPoint::height));
|
||||
println!("prev tip: {}", tip.height());
|
||||
println!("missing block heights: {:?}", missing_block_heights);
|
||||
|
||||
// Here, we actually fetch the missing blocks and create a `local_chain::Update`.
|
||||
|
@ -18,11 +18,11 @@ use bdk_file_store::Store;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let db_path = std::env::temp_dir().join("bdk-electrum-example");
|
||||
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
|
||||
let mut wallet = Wallet::new(
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
external_descriptor,
|
||||
Some(internal_descriptor),
|
||||
db,
|
||||
|
@ -16,11 +16,11 @@ const PARALLEL_REQUESTS: usize = 5;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
|
||||
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
|
||||
let mut wallet = Wallet::new(
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
external_descriptor,
|
||||
Some(internal_descriptor),
|
||||
db,
|
||||
|
@ -15,11 +15,11 @@ use bdk_file_store::Store;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let db_path = std::env::temp_dir().join("bdk-esplora-example");
|
||||
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
|
||||
let mut wallet = Wallet::new(
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
external_descriptor,
|
||||
Some(internal_descriptor),
|
||||
db,
|
||||
|
Loading…
x
Reference in New Issue
Block a user