feat!: LocalChain with hardwired genesis checkpoint

This ensures that `LocalChain` will always have a tip. The `ChainOracle`
trait's `get_chain_tip` method no longer needs to return an option.
This commit is contained in:
志宇
2023-10-12 16:55:32 +08:00
parent d6a0cf0795
commit 5998a22819
19 changed files with 562 additions and 452 deletions

View File

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