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