"example-crates/wallet_esplora_blocking",
"example-crates/wallet_esplora_async",
"example-crates/wallet_rpc",
- "nursery/tmp_plan",
- "nursery/coin_select"
]
[workspace.package]
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
- Arc, Mutex,
+ Arc,
},
time::{Duration, Instant},
};
Emitter,
};
use bdk_chain::{
- bitcoin::{constants::genesis_block, Block, Transaction},
- indexed_tx_graph,
- indexer::keychain_txout,
- local_chain::{self, LocalChain},
- ConfirmationBlockTime, IndexedTxGraph, Merge,
+ bitcoin::{Block, Transaction},
+ local_chain, Merge,
};
use example_cli::{
anyhow,
clap::{self, Args, Subcommand},
- Keychain,
+ ChangeSet, Keychain,
};
const DB_MAGIC: &[u8] = b"bdk_example_rpc";
/// Delay for committing to persistence.
const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
-type ChangeSet = (
- local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
-);
-
#[derive(Debug)]
enum Emission {
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
fn main() -> anyhow::Result<()> {
let start = Instant::now();
+
let example_cli::Init {
args,
- keymap,
- index,
+ graph,
+ chain,
db,
- init_changeset,
- } = example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
- println!(
- "[{:>10}s] loaded initial changeset from db",
- start.elapsed().as_secs_f32()
- );
- let (init_chain_changeset, init_graph_changeset) = init_changeset;
-
- let graph = Mutex::new({
- let mut graph = IndexedTxGraph::new(index);
- graph.apply_changeset(init_graph_changeset);
- graph
- });
- println!(
- "[{:>10}s] loaded indexed tx graph from changeset",
- start.elapsed().as_secs_f32()
- );
-
- let chain = Mutex::new(if init_chain_changeset.is_empty() {
- let genesis_hash = genesis_block(args.network).block_hash();
- let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
- let mut db = db.lock().unwrap();
- db.append_changeset(&(chain_changeset, Default::default()))?;
- chain
- } else {
- LocalChain::from_changeset(init_chain_changeset)?
- });
- println!(
- "[{:>10}s] loaded local chain from changeset",
- start.elapsed().as_secs_f32()
- );
+ network,
+ } = match example_cli::init_or_load::<RpcCommands, RpcArgs>(DB_MAGIC, DB_PATH)? {
+ Some(init) => init,
+ None => return Ok(()),
+ };
let rpc_cmd = match args.command {
example_cli::Commands::ChainSpecific(rpc_cmd) => rpc_cmd,
general_cmd => {
return example_cli::handle_commands(
&graph,
- &db,
&chain,
- &keymap,
- args.network,
+ &db,
+ network,
|rpc_args, tx| {
let client = rpc_args.new_client()?;
client.send_raw_transaction(tx)?;
.apply_update(emission.checkpoint)
.expect("must always apply as we receive blocks in order from emitter");
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
- db_stage.merge((chain_changeset, graph_changeset));
+ db_stage.merge(ChangeSet {
+ local_chain: chain_changeset,
+ tx_graph: graph_changeset.tx_graph,
+ indexer: graph_changeset.indexer,
+ ..Default::default()
+ });
// commit staged db changes in intervals
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
)
};
println!(
- "[{:>10}s] synced to {} @ {} | total: {} sats",
+ "[{:>10}s] synced to {} @ {} | total: {}",
start.elapsed().as_secs_f32(),
synced_to.hash(),
synced_to.height(),
);
{
let db = &mut *db.lock().unwrap();
- db_stage.merge((local_chain::ChangeSet::default(), graph_changeset));
+ db_stage.merge(ChangeSet {
+ tx_graph: graph_changeset.tx_graph,
+ indexer: graph_changeset.indexer,
+ ..Default::default()
+ });
if let Some(changeset) = db_stage.take() {
db.append_changeset(&changeset)?;
}
let mut graph = graph.lock().unwrap();
let mut chain = chain.lock().unwrap();
- let changeset = match emission {
+ let (chain_changeset, graph_changeset) = match emission {
Emission::Block(block_emission) => {
let height = block_emission.block_height();
let chain_changeset = chain
continue;
}
};
- db_stage.merge(changeset);
+
+ db_stage.merge(ChangeSet {
+ local_chain: chain_changeset,
+ tx_graph: graph_changeset.tx_graph,
+ indexer: graph_changeset.indexer,
+ ..Default::default()
+ });
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
let db = &mut *db.lock().unwrap();
)
};
println!(
- "[{:>10}s] synced to {} @ {} / {} | total: {} sats",
+ "[{:>10}s] synced to {} @ {} / {} | total: {}",
start.elapsed().as_secs_f32(),
synced_to.hash(),
synced_to.height(),
[dependencies]
bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]}
+bdk_coin_select = "0.3.0"
bdk_file_store = { path = "../../crates/file_store" }
-bdk_tmp_plan = { path = "../../nursery/tmp_plan" }
-bdk_coin_select = { path = "../../nursery/coin_select" }
-clap = { version = "3.2.23", features = ["derive", "env"] }
anyhow = "1"
+clap = { version = "3.2.23", features = ["derive", "env"] }
serde = { version = "1", features = ["derive"] }
-serde_json = { version = "^1.0" }
+serde_json = "1.0"
-pub use anyhow;
+use bdk_chain::ConfirmationBlockTime;
+use serde_json::json;
+use std::cmp;
+use std::collections::HashMap;
+use std::env;
+use std::fmt;
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use anyhow::bail;
use anyhow::Context;
-use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
-use bdk_file_store::Store;
-use serde::{de::DeserializeOwned, Serialize};
-use std::fmt::Debug;
-use std::{cmp::Reverse, collections::BTreeMap, path::PathBuf, sync::Mutex, time::Duration};
-
+use bdk_chain::bitcoin::{
+ absolute,
+ address::NetworkUnchecked,
+ bip32, consensus, constants,
+ hex::DisplayHex,
+ relative,
+ secp256k1::{rand::prelude::*, Secp256k1},
+ transaction, Address, Amount, Network, NetworkKind, PrivateKey, Psbt, PublicKey, Sequence,
+ Transaction, TxIn, TxOut,
+};
+use bdk_chain::miniscript::{
+ descriptor::{DescriptorSecretKey, SinglePubKey},
+ plan::{Assets, Plan},
+ psbt::PsbtExt,
+ Descriptor, DescriptorPublicKey,
+};
use bdk_chain::{
- bitcoin::{
- absolute, address,
- secp256k1::Secp256k1,
- sighash::{Prevouts, SighashCache},
- transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut,
- },
- indexed_tx_graph::{self, IndexedTxGraph},
+ indexed_tx_graph,
indexer::keychain_txout::{self, KeychainTxOutIndex},
- local_chain,
- miniscript::{
- descriptor::{DescriptorSecretKey, KeyMap},
- Descriptor, DescriptorPublicKey,
- },
- Anchor, ChainOracle, DescriptorExt, FullTxOut, Merge,
+ local_chain::{self, LocalChain},
+ tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge,
};
-pub use bdk_file_store;
-pub use clap;
-
+use bdk_coin_select::{
+ metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target,
+ TargetFee, TargetOutputs,
+};
+use bdk_file_store::Store;
use clap::{Parser, Subcommand};
-pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
-pub type KeychainChangeSet<A> = (
- local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet>,
-);
+pub use anyhow;
+pub use clap;
+
+/// Alias for a `IndexedTxGraph` with specific `Anchor` and `Indexer`.
+pub type KeychainTxGraph = IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<Keychain>>;
+
+/// ChangeSet
+#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct ChangeSet {
+ /// Descriptor for recipient addresses.
+ pub descriptor: Option<Descriptor<DescriptorPublicKey>>,
+ /// Descriptor for change addresses.
+ pub change_descriptor: Option<Descriptor<DescriptorPublicKey>>,
+ /// Stores the network type of the transaction data.
+ pub network: Option<Network>,
+ /// Changes to the [`LocalChain`].
+ pub local_chain: local_chain::ChangeSet,
+ /// Changes to [`TxGraph`](tx_graph::TxGraph).
+ pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
+ /// Changes to [`KeychainTxOutIndex`].
+ pub indexer: keychain_txout::ChangeSet,
+}
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
pub struct Args<CS: clap::Subcommand, S: clap::Args> {
- #[clap(env = "DESCRIPTOR")]
- pub descriptor: String,
- #[clap(env = "CHANGE_DESCRIPTOR")]
- pub change_descriptor: Option<String>,
-
- #[clap(env = "BITCOIN_NETWORK", long, default_value = "signet")]
- pub network: Network,
-
- #[clap(env = "BDK_DB_PATH", long, default_value = ".bdk_example_db")]
- pub db_path: PathBuf,
-
- #[clap(env = "BDK_CP_LIMIT", long, default_value = "20")]
- pub cp_limit: usize,
-
#[clap(subcommand)]
pub command: Commands<CS, S>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
+ /// Initialize a new data store.
+ Init {
+ /// Network
+ #[clap(long, short, default_value = "signet")]
+ network: Network,
+ /// Descriptor
+ #[clap(env = "DESCRIPTOR")]
+ descriptor: String,
+ /// Change descriptor
+ #[clap(long, short, env = "CHANGE_DESCRIPTOR")]
+ change_descriptor: Option<String>,
+ },
#[clap(flatten)]
ChainSpecific(CS),
/// Address generation and inspection.
#[clap(subcommand)]
txout_cmd: TxOutCmd,
},
- /// Send coins to an address.
- Send {
- /// Amount to send in satoshis
- value: u64,
- /// Destination address
- address: Address<address::NetworkUnchecked>,
- #[clap(short, default_value = "bnb")]
- coin_select: CoinSelectionAlgo,
- #[clap(flatten)]
- chain_specific: S,
+ /// PSBT operations
+ Psbt {
+ #[clap(subcommand)]
+ psbt_cmd: PsbtCmd<S>,
+ },
+ /// Generate new BIP86 descriptors.
+ Generate {
+ /// Network
+ #[clap(long, short, default_value = "signet")]
+ network: Network,
},
-}
-
-#[derive(Clone, Debug)]
-pub enum CoinSelectionAlgo {
- LargestFirst,
- SmallestFirst,
- OldestFirst,
- NewestFirst,
- BranchAndBound,
-}
-
-impl Default for CoinSelectionAlgo {
- fn default() -> Self {
- Self::LargestFirst
- }
-}
-
-impl core::str::FromStr for CoinSelectionAlgo {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- use CoinSelectionAlgo::*;
- Ok(match s {
- "largest-first" => LargestFirst,
- "smallest-first" => SmallestFirst,
- "oldest-first" => OldestFirst,
- "newest-first" => NewestFirst,
- "bnb" => BranchAndBound,
- unknown => {
- return Err(anyhow::anyhow!(
- "unknown coin selection algorithm '{}'",
- unknown
- ))
- }
- })
- }
-}
-
-impl core::fmt::Display for CoinSelectionAlgo {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- use CoinSelectionAlgo::*;
- write!(
- f,
- "{}",
- match self {
- LargestFirst => "largest-first",
- SmallestFirst => "smallest-first",
- OldestFirst => "oldest-first",
- NewestFirst => "newest-first",
- BranchAndBound => "bnb",
- }
- )
- }
}
#[derive(Subcommand, Debug, Clone)]
},
}
+#[derive(Subcommand, Debug, Clone)]
+pub enum PsbtCmd<S: clap::Args> {
+ /// Create a new PSBT.
+ New {
+ /// Amount to send in satoshis
+ value: u64,
+ /// Recipient address
+ address: Address<NetworkUnchecked>,
+ /// Set max absolute timelock (from consensus value)
+ #[clap(long, short)]
+ after: Option<u32>,
+ /// Set max relative timelock (from consensus value)
+ #[clap(long, short)]
+ older: Option<u32>,
+ /// Coin selection algorithm
+ #[clap(long, short, default_value = "bnb")]
+ coin_select: CoinSelectionAlgo,
+ /// Debug print the PSBT
+ #[clap(long, short)]
+ debug: bool,
+ },
+ /// Sign with a hot signer
+ Sign {
+ /// PSBT
+ #[clap(long)]
+ psbt: Option<String>,
+ /// Private descriptor
+ #[clap(long, short = 'd')]
+ descriptor: Option<String>,
+ },
+ /// Extract transaction
+ Extract {
+ /// PSBT
+ psbt: String,
+ /// Whether to try broadcasting the tx
+ #[clap(long, short = 'b')]
+ try_broadcast: bool,
+ #[clap(flatten)]
+ chain_specific: S,
+ },
+}
+
#[derive(
Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
)]
Internal,
}
-impl core::fmt::Display for Keychain {
+impl fmt::Display for Keychain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Keychain::External => write!(f, "external"),
}
}
-pub struct CreateTxChange {
- pub index_changeset: keychain_txout::ChangeSet,
+#[derive(Clone, Debug, Default)]
+pub enum CoinSelectionAlgo {
+ LargestFirst,
+ SmallestFirst,
+ OldestFirst,
+ NewestFirst,
+ #[default]
+ BranchAndBound,
+}
+
+impl FromStr for CoinSelectionAlgo {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ use CoinSelectionAlgo::*;
+ Ok(match s {
+ "largest-first" => LargestFirst,
+ "smallest-first" => SmallestFirst,
+ "oldest-first" => OldestFirst,
+ "newest-first" => NewestFirst,
+ "bnb" => BranchAndBound,
+ unknown => bail!("unknown coin selection algorithm '{}'", unknown),
+ })
+ }
+}
+
+impl fmt::Display for CoinSelectionAlgo {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use CoinSelectionAlgo::*;
+ write!(
+ f,
+ "{}",
+ match self {
+ LargestFirst => "largest-first",
+ SmallestFirst => "smallest-first",
+ OldestFirst => "oldest-first",
+ NewestFirst => "newest-first",
+ BranchAndBound => "bnb",
+ }
+ )
+ }
+}
+
+// Records changes to the internal keychain when we
+// have to include a change output during tx creation.
+#[derive(Debug)]
+pub struct ChangeInfo {
pub change_keychain: Keychain,
+ pub indexer: keychain_txout::ChangeSet,
pub index: u32,
}
-pub fn create_tx<A: Anchor, O: ChainOracle>(
- graph: &mut KeychainTxGraph<A>,
+pub fn create_tx<O: ChainOracle>(
+ graph: &mut KeychainTxGraph,
chain: &O,
- keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
+ assets: &Assets,
cs_algorithm: CoinSelectionAlgo,
address: Address,
value: u64,
-) -> anyhow::Result<(Transaction, Option<CreateTxChange>)>
+) -> anyhow::Result<(Psbt, Option<ChangeInfo>)>
where
O::Error: std::error::Error + Send + Sync + 'static,
{
let mut changeset = keychain_txout::ChangeSet::default();
- let assets = bdk_tmp_plan::Assets {
- keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
- ..Default::default()
- };
-
- // TODO use planning module
- let mut candidates = planned_utxos(graph, chain, &assets)?;
+ // get planned utxos
+ let mut plan_utxos = planned_utxos(graph, chain, assets)?;
- // apply coin selection algorithm
+ // sort utxos if cs-algo requires it
match cs_algorithm {
CoinSelectionAlgo::LargestFirst => {
- candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value))
- }
- CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value),
- CoinSelectionAlgo::OldestFirst => {
- candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone())
+ plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.txout.value))
}
+ CoinSelectionAlgo::SmallestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.txout.value),
+ CoinSelectionAlgo::OldestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.chain_position),
CoinSelectionAlgo::NewestFirst => {
- candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone()))
+ plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.chain_position))
}
- CoinSelectionAlgo::BranchAndBound => {}
+ CoinSelectionAlgo::BranchAndBound => plan_utxos.shuffle(&mut thread_rng()),
}
- // turn the txos we chose into weight and value
- let wv_candidates = candidates
+ // build candidate set
+ let candidates: Vec<Candidate> = plan_utxos
.iter()
.map(|(plan, utxo)| {
- WeightedValue::new(
+ Candidate::new(
utxo.txout.value.to_sat(),
- plan.expected_weight() as _,
+ plan.satisfaction_weight() as u32,
plan.witness_version().is_some(),
)
})
.collect();
+ // create recipient output(s)
let mut outputs = vec![TxOut {
value: Amount::from_sat(value),
script_pubkey: address.script_pubkey(),
}];
- let internal_keychain = if graph
+ let (change_keychain, _) = graph
.index
.keychains()
- .any(|(k, _)| k == Keychain::Internal)
- {
- Keychain::Internal
- } else {
- Keychain::External
- };
+ .last()
+ .expect("must have a keychain");
- let ((change_index, change_script), change_changeset) = graph
+ let ((change_index, change_script), index_changeset) = graph
.index
- .next_unused_spk(internal_keychain)
+ .next_unused_spk(change_keychain)
.expect("Must exist");
- changeset.merge(change_changeset);
-
- let change_plan = bdk_tmp_plan::plan_satisfaction(
- &graph
- .index
- .keychains()
- .find(|(k, _)| *k == internal_keychain)
- .expect("must exist")
- .1
- .at_derivation_index(change_index)
- .expect("change_index can't be hardened"),
- &assets,
- )
- .expect("failed to obtain change plan");
+ changeset.merge(index_changeset);
let mut change_output = TxOut {
value: Amount::ZERO,
script_pubkey: change_script,
};
- let cs_opts = CoinSelectorOpt {
- target_feerate: 0.5,
- min_drain_value: graph
- .index
- .keychains()
- .find(|(k, _)| *k == internal_keychain)
- .expect("must exist")
- .1
- .dust_value(),
- ..CoinSelectorOpt::fund_outputs(
- &outputs,
- &change_output,
- change_plan.expected_weight() as u32,
- )
+ let change_desc = graph
+ .index
+ .keychains()
+ .find(|(k, _)| k == &change_keychain)
+ .expect("must exist")
+ .1;
+
+ let min_drain_value = change_desc.dust_value();
+
+ let target = Target {
+ outputs: TargetOutputs::fund_outputs(
+ outputs
+ .iter()
+ .map(|output| (output.weight().to_wu() as u32, output.value.to_sat())),
+ ),
+ fee: TargetFee::default(),
};
- // TODO: How can we make it easy to shuffle in order of inputs and outputs here?
- // apply coin selection by saying we need to fund these outputs
- let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts);
+ let change_policy = ChangePolicy {
+ min_value: min_drain_value,
+ drain_weights: DrainWeights::TR_KEYSPEND,
+ };
- // just select coins in the order provided until we have enough
- // only use the first result (least waste)
- let selection = match cs_algorithm {
+ // run coin selection
+ let mut selector = CoinSelector::new(&candidates);
+ match cs_algorithm {
CoinSelectionAlgo::BranchAndBound => {
- coin_select_bnb(Duration::from_secs(10), coin_selector.clone())
- .map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())?
+ let metric = LowestFee {
+ target,
+ long_term_feerate: FeeRate::from_sat_per_vb(10.0),
+ change_policy,
+ };
+ match selector.run_bnb(metric, 10_000) {
+ Ok(_) => {}
+ Err(_) => selector
+ .select_until_target_met(target)
+ .context("selecting coins")?,
+ }
}
- _ => coin_selector.select_until_finished()?,
- };
- let (_, selection_meta) = selection.best_strategy();
-
- // get the selected utxos
- let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
+ _ => selector
+ .select_until_target_met(target)
+ .context("selecting coins")?,
+ }
- if let Some(drain_value) = selection_meta.drain_value {
- change_output.value = Amount::from_sat(drain_value);
- // if the selection tells us to use change and the change value is sufficient, we add it as an output
- outputs.push(change_output)
+ // get the selected plan utxos
+ let selected: Vec<_> = selector.apply_selection(&plan_utxos).collect();
+
+ // if the selection tells us to use change and the change value is sufficient, we add it as an output
+ let mut change_info = Option::<ChangeInfo>::None;
+ let drain = selector.drain(target, change_policy);
+ if drain.value > min_drain_value {
+ change_output.value = Amount::from_sat(drain.value);
+ outputs.push(change_output);
+ change_info = Some(ChangeInfo {
+ change_keychain,
+ indexer: changeset,
+ index: change_index,
+ });
+ outputs.shuffle(&mut thread_rng());
}
- let mut transaction = Transaction {
+ let unsigned_tx = Transaction {
version: transaction::Version::TWO,
- // because the temporary planning module does not support timelocks, we can use the chain
- // tip as the `lock_time` for anti-fee-sniping purposes
- lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
- .expect("invalid height"),
- input: selected_txos
+ lock_time: assets
+ .absolute_timelock
+ .unwrap_or(absolute::LockTime::from_height(
+ chain.get_chain_tip()?.height,
+ )?),
+ input: selected
.iter()
- .map(|(_, utxo)| TxIn {
+ .map(|(plan, utxo)| TxIn {
previous_output: utxo.outpoint,
- sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
+ sequence: plan
+ .relative_timelock
+ .map_or(Sequence::ENABLE_RBF_NO_LOCKTIME, Sequence::from),
..Default::default()
})
.collect(),
output: outputs,
};
- let prevouts = selected_txos
- .iter()
- .map(|(_, utxo)| utxo.txout.clone())
- .collect::<Vec<_>>();
- let sighash_prevouts = Prevouts::All(&prevouts);
-
- // first, set tx values for the plan so that we don't change them while signing
- for (i, (plan, _)) in selected_txos.iter().enumerate() {
- if let Some(sequence) = plan.required_sequence() {
- transaction.input[i].sequence = sequence
- }
- }
-
- // create a short lived transaction
- let _sighash_tx = transaction.clone();
- let mut sighash_cache = SighashCache::new(&_sighash_tx);
-
- for (i, (plan, _)) in selected_txos.iter().enumerate() {
- let requirements = plan.requirements();
- let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default();
- assert!(
- !requirements.requires_hash_preimages(),
- "can't have hash pre-images since we didn't provide any."
- );
- assert!(
- requirements.signatures.sign_with_keymap(
- i,
- keymap,
- &sighash_prevouts,
- None,
- None,
- &mut sighash_cache,
- &mut auth_data,
- &Secp256k1::default(),
- )?,
- "we should have signed with this input."
- );
-
- match plan.try_complete(&auth_data) {
- bdk_tmp_plan::PlanState::Complete {
- final_script_sig,
- final_script_witness,
- } => {
- if let Some(witness) = final_script_witness {
- transaction.input[i].witness = witness;
- }
-
- if let Some(script_sig) = final_script_sig {
- transaction.input[i].script_sig = script_sig;
- }
- }
- bdk_tmp_plan::PlanState::Incomplete(_) => {
- return Err(anyhow::anyhow!(
- "we weren't able to complete the plan with our keys."
- ));
- }
- }
+ // update psbt with plan
+ let mut psbt = Psbt::from_unsigned_tx(unsigned_tx)?;
+ for (i, (plan, utxo)) in selected.iter().enumerate() {
+ let psbt_input = &mut psbt.inputs[i];
+ plan.update_psbt_input(psbt_input);
+ psbt_input.witness_utxo = Some(utxo.txout.clone());
}
- let change_info = if selection_meta.drain_value.is_some() {
- Some(CreateTxChange {
- index_changeset: changeset,
- change_keychain: internal_keychain,
- index: change_index,
- })
- } else {
- None
- };
-
- Ok((transaction, change_info))
+ Ok((psbt, change_info))
}
-// Alias the elements of `Result` of `planned_utxos`
-pub type PlannedUtxo<K, A> = (bdk_tmp_plan::Plan<K>, FullTxOut<A>);
+// Alias the elements of `planned_utxos`
+pub type PlanUtxo = (Plan, FullTxOut<ConfirmationBlockTime>);
-pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
- graph: &KeychainTxGraph<A>,
+pub fn planned_utxos<O: ChainOracle>(
+ graph: &KeychainTxGraph,
chain: &O,
- assets: &bdk_tmp_plan::Assets<K>,
-) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
+ assets: &Assets,
+) -> Result<Vec<PlanUtxo>, O::Error> {
let chain_tip = chain.get_chain_tip()?;
let outpoints = graph.index.outpoints();
graph
.graph()
.try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())
- .filter_map(|r| -> Option<Result<PlannedUtxo<K, A>, _>> {
+ .filter_map(|r| -> Option<Result<PlanUtxo, _>> {
let (k, i, full_txo) = match r {
Err(err) => return Some(Err(err)),
Ok(((k, i), full_txo)) => (k, i, full_txo),
.1
.at_derivation_index(i)
.expect("i can't be hardened");
- let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
+
+ let plan = desc.plan(assets).ok()?;
+
Some(Ok((plan, full_txo)))
})
.collect()
}
-pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainOracle, C>(
- graph: &Mutex<KeychainTxGraph<A>>,
- db: &Mutex<Store<C>>,
- chain: &Mutex<O>,
- keymap: &BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
+pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
+ graph: &Mutex<KeychainTxGraph>,
+ chain: &Mutex<LocalChain>,
+ db: &Mutex<Store<ChangeSet>>,
network: Network,
broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
cmd: Commands<CS, S>,
-) -> anyhow::Result<()>
-where
- O::Error: std::error::Error + Send + Sync + 'static,
- C: Default
- + Merge
- + DeserializeOwned
- + Serialize
- + From<KeychainChangeSet<A>>
- + Send
- + Sync
- + Debug,
-{
+) -> anyhow::Result<()> {
match cmd {
+ Commands::Init { .. } => unreachable!("handled by init command"),
+ Commands::Generate { .. } => unreachable!("handled by generate command"),
Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
Commands::Address { addr_cmd } => {
let graph = &mut *graph.lock().unwrap();
let ((spk_i, spk), index_changeset) =
spk_chooser(index, Keychain::External).expect("Must exist");
let db = &mut *db.lock().unwrap();
- db.append_changeset(&C::from((
- local_chain::ChangeSet::default(),
- indexed_tx_graph::ChangeSet::from(index_changeset),
- )))?;
- let addr = Address::from_script(spk.as_script(), network)
- .context("failed to derive address")?;
+ db.append_changeset(&ChangeSet {
+ indexer: index_changeset,
+ ..Default::default()
+ })?;
+ let addr = Address::from_script(spk.as_script(), network)?;
println!("[address @ {}] {}", spk_i, addr);
Ok(())
}
}
}
}
- Commands::Send {
- value,
- address,
- coin_select,
- chain_specific,
- } => {
- let chain = &*chain.lock().unwrap();
- let address = address.require_network(network)?;
- let (transaction, change_index) = {
- let graph = &mut *graph.lock().unwrap();
- // take mutable ref to construct tx -- it is only open for a short time while building it.
- let (tx, change_info) =
- create_tx(graph, chain, keymap, coin_select, address, value)?;
-
- if let Some(CreateTxChange {
- index_changeset,
+ Commands::Psbt { psbt_cmd } => match psbt_cmd {
+ PsbtCmd::New {
+ value,
+ address,
+ after,
+ older,
+ coin_select,
+ debug,
+ } => {
+ let address = address.require_network(network)?;
+
+ let (psbt, change_info) = {
+ let mut graph = graph.lock().unwrap();
+ let chain = chain.lock().unwrap();
+
+ // collect assets we can sign for
+ let mut assets = Assets::new();
+ if let Some(n) = after {
+ assets = assets.after(absolute::LockTime::from_consensus(n));
+ }
+ if let Some(n) = older {
+ assets = assets.older(relative::LockTime::from_consensus(n)?);
+ }
+ for (_, desc) in graph.index.keychains() {
+ match desc {
+ Descriptor::Wpkh(wpkh) => {
+ assets = assets.add(wpkh.clone().into_inner());
+ }
+ Descriptor::Tr(tr) => {
+ assets = assets.add(tr.internal_key().clone());
+ }
+ _ => bail!("unsupported descriptor type"),
+ }
+ }
+
+ create_tx(&mut graph, &*chain, &assets, coin_select, address, value)?
+ };
+
+ if let Some(ChangeInfo {
change_keychain,
+ indexer,
index,
}) = change_info
{
// If we're unable to persist this, then we don't want to broadcast.
{
let db = &mut *db.lock().unwrap();
- db.append_changeset(&C::from((
- local_chain::ChangeSet::default(),
- indexed_tx_graph::ChangeSet::from(index_changeset),
- )))?;
+ db.append_changeset(&ChangeSet {
+ indexer,
+ ..Default::default()
+ })?;
}
// We don't want other callers/threads to use this address while we're using it
// but we also don't want to scan the tx we just created because it's not
// technically in the blockchain yet.
- graph.index.mark_used(change_keychain, index);
- (tx, Some((change_keychain, index)))
+ graph
+ .lock()
+ .unwrap()
+ .index
+ .mark_used(change_keychain, index);
+ }
+
+ if debug {
+ dbg!(psbt);
} else {
- (tx, None)
+ // print base64 encoded psbt
+ let fee = psbt.fee()?.to_sat();
+ let mut obj = serde_json::Map::new();
+ obj.insert("psbt".to_string(), json!(psbt.to_string()));
+ obj.insert("fee".to_string(), json!(fee));
+ println!("{}", serde_json::to_string_pretty(&obj)?);
+ };
+
+ Ok(())
+ }
+ PsbtCmd::Sign { psbt, descriptor } => {
+ let mut psbt = Psbt::from_str(&psbt.unwrap_or_default())?;
+
+ let desc_str = match descriptor {
+ Some(s) => s,
+ None => env::var("DESCRIPTOR").context("unable to sign")?,
+ };
+
+ let secp = Secp256k1::new();
+ let (_, keymap) = Descriptor::parse_descriptor(&secp, &desc_str)?;
+ if keymap.is_empty() {
+ bail!("unable to sign")
}
- };
- match (broadcast)(chain_specific, &transaction) {
- Ok(_) => {
- println!("Broadcasted Tx : {}", transaction.compute_txid());
+ // note: we're only looking at the first entry in the keymap
+ // the idea is to find something that impls `GetKey`
+ let sign_res = match keymap.iter().next().expect("not empty") {
+ (DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
+ let pk = match single_pub.key {
+ SinglePubKey::FullKey(pk) => pk,
+ SinglePubKey::XOnly(_) => unimplemented!("single xonly pubkey"),
+ };
+ let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
+ psbt.sign(&keys, &secp)
+ }
+ (_, DescriptorSecretKey::XPrv(k)) => psbt.sign(&k.xkey, &secp),
+ _ => unimplemented!("multi xkey signer"),
+ };
- let keychain_changeset = graph.lock().unwrap().insert_tx(transaction);
+ let _ = sign_res
+ .map_err(|errors| anyhow::anyhow!("failed to sign PSBT {:?}", errors))?;
- // We know the tx is at least unconfirmed now. Note if persisting here fails,
- // it's not a big deal since we can always find it again form
- // blockchain.
- db.lock().unwrap().append_changeset(&C::from((
- local_chain::ChangeSet::default(),
- keychain_changeset,
- )))?;
- Ok(())
- }
- Err(e) => {
- if let Some((keychain, index)) = change_index {
- // We failed to broadcast, so allow our change address to be used in the future
- graph.lock().unwrap().index.unmark_used(keychain, index);
+ let mut obj = serde_json::Map::new();
+ obj.insert("psbt".to_string(), json!(psbt.to_string()));
+ println!("{}", serde_json::to_string_pretty(&obj)?);
+
+ Ok(())
+ }
+ PsbtCmd::Extract {
+ try_broadcast,
+ chain_specific,
+ psbt,
+ } => {
+ let mut psbt = Psbt::from_str(&psbt)?;
+ psbt.finalize_mut(&Secp256k1::new())
+ .map_err(|errors| anyhow::anyhow!("failed to finalize PSBT {errors:?}"))?;
+
+ let tx = psbt.extract_tx()?;
+
+ if try_broadcast {
+ let mut graph = graph.lock().unwrap();
+
+ match broadcast(chain_specific, &tx) {
+ Ok(_) => {
+ println!("Broadcasted Tx: {}", tx.compute_txid());
+
+ let changeset = graph.insert_tx(tx);
+
+ // We know the tx is at least unconfirmed now. Note if persisting here fails,
+ // it's not a big deal since we can always find it again from the
+ // blockchain.
+ db.lock().unwrap().append_changeset(&ChangeSet {
+ tx_graph: changeset.tx_graph,
+ indexer: changeset.indexer,
+ ..Default::default()
+ })?;
+ }
+ Err(e) => {
+ // We failed to broadcast, so allow our change address to be used in the future
+ let (change_keychain, _) = graph
+ .index
+ .keychains()
+ .last()
+ .expect("must have a keychain");
+ let change_index = tx.output.iter().find_map(|txout| {
+ let spk = txout.script_pubkey.clone();
+ match graph.index.index_of_spk(spk) {
+ Some(&(keychain, index)) if keychain == change_keychain => {
+ Some((keychain, index))
+ }
+ _ => None,
+ }
+ });
+ if let Some((keychain, index)) = change_index {
+ graph.index.unmark_used(keychain, index);
+ }
+ bail!(e);
+ }
}
- Err(e)
+ } else {
+ // encode raw tx hex
+ let hex = consensus::serialize(&tx).to_lower_hex_string();
+ let mut obj = serde_json::Map::new();
+ obj.insert("tx".to_string(), json!(hex));
+ println!("{}", serde_json::to_string_pretty(&obj)?);
}
+
+ Ok(())
}
- }
+ },
}
}
-/// The initial state returned by [`init`].
-pub struct Init<CS: clap::Subcommand, S: clap::Args, C>
-where
- C: Default + Merge + Serialize + DeserializeOwned + Debug + Send + Sync + 'static,
-{
- /// Arguments parsed by the cli.
+/// The initial state returned by [`init_or_load`].
+pub struct Init<CS: clap::Subcommand, S: clap::Args> {
+ /// CLI args
pub args: Args<CS, S>,
- /// Descriptor keymap.
- pub keymap: KeyMap,
- /// Keychain-txout index.
- pub index: KeychainTxOutIndex<Keychain>,
- /// Persistence backend.
- pub db: Mutex<Store<C>>,
- /// Initial changeset.
- pub init_changeset: C,
+ /// Indexed graph
+ pub graph: Mutex<KeychainTxGraph>,
+ /// Local chain
+ pub chain: Mutex<LocalChain>,
+ /// Database
+ pub db: Mutex<Store<ChangeSet>>,
+ /// Network
+ pub network: Network,
}
-/// Parses command line arguments and initializes all components, creating
-/// a file store with the given parameters, or loading one if it exists.
-pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
+/// Loads from persistence or creates new
+pub fn init_or_load<CS: clap::Subcommand, S: clap::Args>(
db_magic: &[u8],
- db_default_path: &str,
-) -> anyhow::Result<Init<CS, S, C>>
-where
- C: Default
- + Merge
- + Serialize
- + DeserializeOwned
- + Debug
- + core::marker::Send
- + core::marker::Sync
- + 'static,
-{
- if std::env::var("BDK_DB_PATH").is_err() {
- std::env::set_var("BDK_DB_PATH", db_default_path);
- }
+ db_path: &str,
+) -> anyhow::Result<Option<Init<CS, S>>> {
let args = Args::<CS, S>::parse();
- let secp = Secp256k1::default();
- let mut index = KeychainTxOutIndex::<Keychain>::default();
-
- // TODO: descriptors are already stored in the db, so we shouldn't re-insert
- // them in the index here. However, the keymap is not stored in the database.
- let (descriptor, mut keymap) =
- Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
- let _ = index.insert_descriptor(Keychain::External, descriptor)?;
+ match args.command {
+ // initialize new db
+ Commands::Init { .. } => initialize::<CS, S>(args, db_magic, db_path).map(|_| None),
+ // generate keys
+ Commands::Generate { network } => generate_bip86_helper(network).map(|_| None),
+ // try load
+ _ => {
+ let mut db =
+ Store::<ChangeSet>::open(db_magic, db_path).context("could not open file store")?;
+ let changeset = db.aggregate_changesets()?.expect("db must not be empty");
+
+ let network = changeset.network.expect("changeset network");
+
+ let chain = Mutex::new({
+ let (mut chain, _) =
+ LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
+ chain.apply_changeset(&changeset.local_chain)?;
+ chain
+ });
+
+ let graph = Mutex::new({
+ // insert descriptors and apply loaded changeset
+ let mut index = KeychainTxOutIndex::default();
+ if let Some(desc) = changeset.descriptor {
+ index.insert_descriptor(Keychain::External, desc)?;
+ }
+ if let Some(change_desc) = changeset.change_descriptor {
+ index.insert_descriptor(Keychain::Internal, change_desc)?;
+ }
+ let mut graph = KeychainTxGraph::new(index);
+ graph.apply_changeset(indexed_tx_graph::ChangeSet {
+ tx_graph: changeset.tx_graph,
+ indexer: changeset.indexer,
+ });
+ graph
+ });
+
+ let db = Mutex::new(db);
+
+ Ok(Some(Init {
+ args,
+ graph,
+ chain,
+ db,
+ network,
+ }))
+ }
+ }
+}
- if let Some((internal_descriptor, internal_keymap)) = args
- .change_descriptor
- .as_ref()
- .map(|desc_str| Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, desc_str))
- .transpose()?
+/// Initialize db backend.
+fn initialize<CS, S>(args: Args<CS, S>, db_magic: &[u8], db_path: &str) -> anyhow::Result<()>
+where
+ CS: clap::Subcommand,
+ S: clap::Args,
+{
+ if let Commands::Init {
+ network,
+ descriptor,
+ change_descriptor,
+ } = args.command
{
- keymap.extend(internal_keymap);
- let _ = index.insert_descriptor(Keychain::Internal, internal_descriptor)?;
+ let mut changeset = ChangeSet::default();
+
+ // parse descriptors
+ let secp = Secp256k1::new();
+ let mut index = KeychainTxOutIndex::default();
+ let (descriptor, _) =
+ Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &descriptor)?;
+ let _ = index.insert_descriptor(Keychain::External, descriptor.clone())?;
+ changeset.descriptor = Some(descriptor);
+
+ if let Some(desc) = change_descriptor {
+ let (change_descriptor, _) =
+ Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &desc)?;
+ let _ = index.insert_descriptor(Keychain::Internal, change_descriptor.clone())?;
+ changeset.change_descriptor = Some(change_descriptor);
+ }
+
+ // create new
+ let (_, chain_changeset) =
+ LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
+ changeset.network = Some(network);
+ changeset.local_chain = chain_changeset;
+ let mut db = Store::<ChangeSet>::create_new(db_magic, db_path)?;
+ db.append_changeset(&changeset)?;
+ println!("New database {db_path}");
}
- let mut db_backend = match Store::<C>::open_or_create_new(db_magic, &args.db_path) {
- Ok(db_backend) => db_backend,
- // we cannot return `err` directly as it has lifetime `'m`
- Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
+ Ok(())
+}
+
+/// Generate BIP86 descriptors.
+fn generate_bip86_helper(network: impl Into<NetworkKind>) -> anyhow::Result<()> {
+ let secp = Secp256k1::new();
+ let mut seed = [0x00; 32];
+ thread_rng().fill_bytes(&mut seed);
+
+ let m = bip32::Xpriv::new_master(network, &seed)?;
+ let fp = m.fingerprint(&secp);
+ let path = if m.network.is_mainnet() {
+ "86h/0h/0h"
+ } else {
+ "86h/1h/0h"
};
- let init_changeset = db_backend.aggregate_changesets()?.unwrap_or_default();
+ let descriptors: Vec<String> = [0, 1]
+ .iter()
+ .map(|i| format!("tr([{fp}]{m}/{path}/{i}/*)"))
+ .collect();
+ let external_desc = &descriptors[0];
+ let internal_desc = &descriptors[1];
+ let (descriptor, keymap) =
+ <Descriptor<DescriptorPublicKey>>::parse_descriptor(&secp, external_desc)?;
+ let (internal_descriptor, internal_keymap) =
+ <Descriptor<DescriptorPublicKey>>::parse_descriptor(&secp, internal_desc)?;
+ println!("Public");
+ println!("{}", descriptor);
+ println!("{}", internal_descriptor);
+ println!("\nPrivate");
+ println!("{}", descriptor.to_string_with_secret(&keymap));
+ println!(
+ "{}",
+ internal_descriptor.to_string_with_secret(&internal_keymap)
+ );
+
+ Ok(())
+}
- Ok(Init {
- args,
- keymap,
- index,
- db: Mutex::new(db_backend),
- init_changeset,
- })
+impl Merge for ChangeSet {
+ fn merge(&mut self, other: Self) {
+ if other.descriptor.is_some() {
+ self.descriptor = other.descriptor;
+ }
+ if other.change_descriptor.is_some() {
+ self.change_descriptor = other.change_descriptor;
+ }
+ if other.network.is_some() {
+ self.network = other.network;
+ }
+ Merge::merge(&mut self.local_chain, other.local_chain);
+ Merge::merge(&mut self.tx_graph, other.tx_graph);
+ Merge::merge(&mut self.indexer, other.indexer);
+ }
+
+ fn is_empty(&self) -> bool {
+ self.descriptor.is_none()
+ && self.change_descriptor.is_none()
+ && self.network.is_none()
+ && self.local_chain.is_empty()
+ && self.tx_graph.is_empty()
+ && self.indexer.is_empty()
+ }
}
-use std::{
- io::{self, Write},
- sync::Mutex,
-};
+use std::io::{self, Write};
use bdk_chain::{
- bitcoin::{constants::genesis_block, Address, Network, Txid},
+ bitcoin::{Address, Network, Txid},
collections::BTreeSet,
- indexed_tx_graph::{self, IndexedTxGraph},
- indexer::keychain_txout,
- local_chain::{self, LocalChain},
+ indexed_tx_graph,
spk_client::{FullScanRequest, SyncRequest},
ConfirmationBlockTime, Merge,
};
BdkElectrumClient,
};
use example_cli::{
+ self,
anyhow::{self, Context},
clap::{self, Parser, Subcommand},
- Keychain,
+ ChangeSet, Keychain,
};
const DB_MAGIC: &[u8] = b"bdk_example_electrum";
pub batch_size: usize,
}
-type ChangeSet = (
- local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
-);
-
fn main() -> anyhow::Result<()> {
let example_cli::Init {
args,
- keymap,
- index,
+ graph,
+ chain,
db,
- init_changeset,
- } = example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
-
- let (disk_local_chain, disk_tx_graph) = init_changeset;
-
- let graph = Mutex::new({
- let mut graph = IndexedTxGraph::new(index);
- graph.apply_changeset(disk_tx_graph);
- graph
- });
-
- let chain = Mutex::new({
- let genesis_hash = genesis_block(args.network).block_hash();
- let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
- chain.apply_changeset(&disk_local_chain)?;
- chain
- });
+ network,
+ } = match example_cli::init_or_load::<ElectrumCommands, ElectrumArgs>(DB_MAGIC, DB_PATH)? {
+ Some(init) => init,
+ None => return Ok(()),
+ };
let electrum_cmd = match &args.command {
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
general_cmd => {
return example_cli::handle_commands(
&graph,
- &db,
&chain,
- &keymap,
- args.network,
+ &db,
+ network,
|electrum_args, tx| {
- let client = electrum_args.client(args.network)?;
+ let client = electrum_args.client(network)?;
client.transaction_broadcast(tx)?;
Ok(())
},
}
};
- let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(args.network)?);
+ let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(network)?);
// Tell the electrum client about the txs we've already got locally so it doesn't re-download them
client.populate_tx_cache(&*graph.lock().unwrap());
request.chain_spks(unused_spks.into_iter().map(move |((k, spk_i), spk)| {
eprint!(
"Checking if address {} {}:{} has been used",
- Address::from_script(&spk, args.network).unwrap(),
+ Address::from_script(&spk, network).unwrap(),
k,
spk_i,
);
}
indexed_tx_graph_changeset.merge(graph.apply_update(graph_update));
- (chain_changeset, indexed_tx_graph_changeset)
+ ChangeSet {
+ local_chain: chain_changeset,
+ tx_graph: indexed_tx_graph_changeset.tx_graph,
+ indexer: indexed_tx_graph_changeset.indexer,
+ ..Default::default()
+ }
};
let mut db = db.lock().unwrap();
use std::{
collections::BTreeSet,
io::{self, Write},
- sync::Mutex,
};
use bdk_chain::{
- bitcoin::{constants::genesis_block, Address, Network, Txid},
- indexed_tx_graph::{self, IndexedTxGraph},
- indexer::keychain_txout,
- local_chain::{self, LocalChain},
+ bitcoin::{Address, Network, Txid},
spk_client::{FullScanRequest, SyncRequest},
- ConfirmationBlockTime, Merge,
+ Merge,
};
-
use bdk_esplora::{esplora_client, EsploraExt};
-
use example_cli::{
anyhow::{self, Context},
clap::{self, Parser, Subcommand},
- Keychain,
+ ChangeSet, Keychain,
};
const DB_MAGIC: &[u8] = b"bdk_example_esplora";
-const DB_PATH: &str = "bdk_example_esplora.db";
-
-type ChangeSet = (
- local_chain::ChangeSet,
- indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
-);
+const DB_PATH: &str = ".bdk_example_esplora.db";
#[derive(Subcommand, Debug, Clone)]
enum EsploraCommands {
/// Scans the addresses in the wallet using the esplora API.
Scan {
/// When a gap this large has been found for a keychain, it will stop.
- #[clap(long, default_value = "5")]
+ #[clap(long, short = 'g', default_value = "10")]
stop_gap: usize,
#[clap(flatten)]
scan_options: ScanOptions,
#[derive(clap::Args, Debug, Clone)]
pub struct EsploraArgs {
- /// The esplora url endpoint to connect to e.g. `<https://blockstream.info/api>`
- /// If not provided it'll be set to a default for the network provided
+ /// The esplora url endpoint to connect to.
+ #[clap(long, short = 'u', env = "ESPLORA_SERVER")]
esplora_url: Option<String>,
}
fn main() -> anyhow::Result<()> {
let example_cli::Init {
args,
- keymap,
- index,
+ graph,
+ chain,
db,
- init_changeset,
- } = example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
-
- let genesis_hash = genesis_block(args.network).block_hash();
-
- let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset;
-
- // Construct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in
- // `Mutex` to display how they can be used in a multithreaded context. Technically the mutexes
- // aren't strictly needed here.
- let graph = Mutex::new({
- let mut graph = IndexedTxGraph::new(index);
- graph.apply_changeset(init_indexed_tx_graph_changeset);
- graph
- });
- let chain = Mutex::new({
- let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
- chain.apply_changeset(&init_chain_changeset)?;
- chain
- });
+ network,
+ } = match example_cli::init_or_load::<EsploraCommands, EsploraArgs>(DB_MAGIC, DB_PATH)? {
+ Some(init) => init,
+ None => return Ok(()),
+ };
let esplora_cmd = match &args.command {
// These are commands that are handled by this example (sync, scan).
general_cmd => {
return example_cli::handle_commands(
&graph,
- &db,
&chain,
- &keymap,
- args.network,
+ &db,
+ network,
|esplora_args, tx| {
- let client = esplora_args.client(args.network)?;
+ let client = esplora_args.client(network)?;
client
.broadcast(tx)
.map(|_| ())
}
};
- let client = esplora_cmd.esplora_args().client(args.network)?;
+ let client = esplora_cmd.esplora_args().client(network)?;
// Prepare the `IndexedTxGraph` and `LocalChain` updates based on whether we are scanning or
// syncing.
//
request.chain_spks(unused_spks.into_iter().map(move |((k, i), spk)| {
eprint!(
"Checking if address {} {}:{} has been used",
- Address::from_script(&spk, args.network).unwrap(),
+ Address::from_script(&spk, network).unwrap(),
k,
i,
);
// We persist the changes
let mut db = db.lock().unwrap();
- db.append_changeset(&(local_chain_changeset, indexed_tx_graph_changeset))?;
+ db.append_changeset(&ChangeSet {
+ local_chain: local_chain_changeset,
+ tx_graph: indexed_tx_graph_changeset.tx_graph,
+ indexer: indexed_tx_graph_changeset.indexer,
+ ..Default::default()
+ })?;
Ok(())
}
+++ /dev/null
-[package]
-name = "bdk_coin_select"
-version = "0.0.1"
-authors = [ "LLFourn <lloyd.fourn@gmail.com>" ]
-
-[dependencies]
-bdk_chain = { path = "../../crates/chain" }
-
-[features]
-default = ["std"]
-std = []
+++ /dev/null
-use super::*;
-
-/// Strategy in which we should branch.
-pub enum BranchStrategy {
- /// We continue exploring subtrees of this node, starting with the inclusion branch.
- Continue,
- /// We continue exploring ONLY the omission branch of this node, skipping the inclusion branch.
- SkipInclusion,
- /// We skip both the inclusion and omission branches of this node.
- SkipBoth,
-}
-
-impl BranchStrategy {
- pub fn will_continue(&self) -> bool {
- matches!(self, Self::Continue | Self::SkipInclusion)
- }
-}
-
-/// Closure to decide the branching strategy, alongside a score (if the current selection is a
-/// candidate solution).
-pub type DecideStrategy<'c, S> = dyn Fn(&Bnb<'c, S>) -> (BranchStrategy, Option<S>);
-
-/// [`Bnb`] represents the current state of the BnB algorithm.
-pub struct Bnb<'c, S> {
- pub pool: Vec<(usize, &'c WeightedValue)>,
- pub pool_pos: usize,
- pub best_score: S,
-
- pub selection: CoinSelector<'c>,
- pub rem_abs: u64,
- pub rem_eff: i64,
-}
-
-impl<'c, S: Ord> Bnb<'c, S> {
- /// Creates a new [`Bnb`].
- pub fn new(selector: CoinSelector<'c>, pool: Vec<(usize, &'c WeightedValue)>, max: S) -> Self {
- let (rem_abs, rem_eff) = pool.iter().fold((0, 0), |(abs, eff), (_, c)| {
- (
- abs + c.value,
- eff + c.effective_value(selector.opts.target_feerate),
- )
- });
-
- Self {
- pool,
- pool_pos: 0,
- best_score: max,
- selection: selector,
- rem_abs,
- rem_eff,
- }
- }
-
- /// Turns our [`Bnb`] state into an iterator.
- ///
- /// `strategy` should assess our current selection/node and determine the branching strategy and
- /// whether this selection is a candidate solution (if so, return the selection score).
- pub fn into_iter<'f>(self, strategy: &'f DecideStrategy<'c, S>) -> BnbIter<'c, 'f, S> {
- BnbIter {
- state: self,
- done: false,
- strategy,
- }
- }
-
- /// Attempt to backtrack to the previously selected node's omission branch, return false
- /// otherwise (no more solutions).
- pub fn backtrack(&mut self) -> bool {
- (0..self.pool_pos).rev().any(|pos| {
- let (index, candidate) = self.pool[pos];
-
- if self.selection.is_selected(index) {
- // deselect the last `pos`, so the next round will check the omission branch
- self.pool_pos = pos;
- self.selection.deselect(index);
- true
- } else {
- self.rem_abs += candidate.value;
- self.rem_eff += candidate.effective_value(self.selection.opts.target_feerate);
- false
- }
- })
- }
-
- /// Continue down this branch and skip the inclusion branch if specified.
- pub fn forward(&mut self, skip: bool) {
- let (index, candidate) = self.pool[self.pool_pos];
- self.rem_abs -= candidate.value;
- self.rem_eff -= candidate.effective_value(self.selection.opts.target_feerate);
-
- if !skip {
- self.selection.select(index);
- }
- }
-
- /// Compare the advertised score with the current best. The new best will be the smaller value. Return true
- /// if best is replaced.
- pub fn advertise_new_score(&mut self, score: S) -> bool {
- if score <= self.best_score {
- self.best_score = score;
- return true;
- }
- false
- }
-}
-
-pub struct BnbIter<'c, 'f, S> {
- state: Bnb<'c, S>,
- done: bool,
-
- /// Check our current selection (node) and returns the branching strategy alongside a score
- /// (if the current selection is a candidate solution).
- strategy: &'f DecideStrategy<'c, S>,
-}
-
-impl<'c, 'f, S: Ord + Copy + Display> Iterator for BnbIter<'c, 'f, S> {
- type Item = Option<CoinSelector<'c>>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.done {
- return None;
- }
-
- let (strategy, score) = (self.strategy)(&self.state);
-
- let mut found_best = Option::<CoinSelector>::None;
-
- if let Some(score) = score {
- if self.state.advertise_new_score(score) {
- found_best = Some(self.state.selection.clone());
- }
- }
-
- debug_assert!(
- !strategy.will_continue() || self.state.pool_pos < self.state.pool.len(),
- "Faulty strategy implementation! Strategy suggested that we continue traversing, however, we have already reached the end of the candidates pool! pool_len={}, pool_pos={}",
- self.state.pool.len(), self.state.pool_pos,
- );
-
- match strategy {
- BranchStrategy::Continue => {
- self.state.forward(false);
- }
- BranchStrategy::SkipInclusion => {
- self.state.forward(true);
- }
- BranchStrategy::SkipBoth => {
- if !self.state.backtrack() {
- self.done = true;
- }
- }
- };
-
- // increment selection pool position for next round
- self.state.pool_pos += 1;
-
- if found_best.is_some() || !self.done {
- Some(found_best)
- } else {
- // we have traversed all branches
- None
- }
- }
-}
-
-/// Determines how we should limit rounds of branch and bound.
-pub enum BnbLimit {
- Rounds(usize),
- #[cfg(feature = "std")]
- Duration(core::time::Duration),
-}
-
-impl From<usize> for BnbLimit {
- fn from(v: usize) -> Self {
- Self::Rounds(v)
- }
-}
-
-#[cfg(feature = "std")]
-impl From<core::time::Duration> for BnbLimit {
- fn from(v: core::time::Duration) -> Self {
- Self::Duration(v)
- }
-}
-
-/// This is a variation of the Branch and Bound Coin Selection algorithm designed by Murch (as seen
-/// in Bitcoin Core).
-///
-/// The differences are as follows:
-/// * In addition to working with effective values, we also work with absolute values.
-/// This way, we can use bounds of the absolute values to enforce `min_absolute_fee` (which is used by
-/// RBF), and `max_extra_target` (which can be used to increase the possible solution set, given
-/// that the sender is okay with sending extra to the receiver).
-///
-/// Murch's Master Thesis: <https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
-/// Bitcoin Core Implementation: <https://github.com/bitcoin/bitcoin/blob/23.x/src/wallet/coinselection.cpp#L65>
-///
-/// TODO: Another optimization we could do is figure out candidates with the smallest waste, and
-/// if we find a result with waste equal to this, we can just break.
-pub fn coin_select_bnb<L>(limit: L, selector: CoinSelector) -> Option<CoinSelector>
-where
- L: Into<BnbLimit>,
-{
- let opts = selector.opts;
-
- // prepare the pool of candidates to select from:
- // * filter out candidates with negative/zero effective values
- // * sort candidates by descending effective value
- let pool = {
- let mut pool = selector
- .unselected()
- .filter(|(_, c)| c.effective_value(opts.target_feerate) > 0)
- .collect::<Vec<_>>();
- pool.sort_unstable_by(|(_, a), (_, b)| {
- let a = a.effective_value(opts.target_feerate);
- let b = b.effective_value(opts.target_feerate);
- b.cmp(&a)
- });
- pool
- };
-
- let feerate_decreases = opts.target_feerate > opts.long_term_feerate();
-
- let target_abs = opts.target_value.unwrap_or(0) + opts.min_absolute_fee;
- let target_eff = selector.effective_target();
-
- let upper_bound_abs = target_abs + (opts.drain_weight as f32 * opts.target_feerate) as u64;
- let upper_bound_eff = target_eff + opts.drain_waste();
-
- let strategy = move |bnb: &Bnb<i64>| -> (BranchStrategy, Option<i64>) {
- let selected_abs = bnb.selection.selected_absolute_value();
- let selected_eff = bnb.selection.selected_effective_value();
-
- // backtrack if the remaining value is not enough to reach the target
- if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff {
- return (BranchStrategy::SkipBoth, None);
- }
-
- // backtrack if the selected value has already surpassed upper bounds
- if selected_abs > upper_bound_abs && selected_eff > upper_bound_eff {
- return (BranchStrategy::SkipBoth, None);
- }
-
- let selected_waste = bnb.selection.selected_waste();
-
- // when feerate decreases, waste without excess is guaranteed to increase with each
- // selection. So if we have already surpassed the best score, we can backtrack.
- if feerate_decreases && selected_waste > bnb.best_score {
- return (BranchStrategy::SkipBoth, None);
- }
-
- // solution?
- if selected_abs >= target_abs && selected_eff >= target_eff {
- let waste = selected_waste + bnb.selection.current_excess();
- return (BranchStrategy::SkipBoth, Some(waste));
- }
-
- // early bailout optimization:
- // If the candidate at the previous position is NOT selected and has the same weight and
- // value as the current candidate, we can skip selecting the current candidate.
- if bnb.pool_pos > 0 && !bnb.selection.is_empty() {
- let (_, candidate) = bnb.pool[bnb.pool_pos];
- let (prev_index, prev_candidate) = bnb.pool[bnb.pool_pos - 1];
-
- if !bnb.selection.is_selected(prev_index)
- && candidate.value == prev_candidate.value
- && candidate.weight == prev_candidate.weight
- {
- return (BranchStrategy::SkipInclusion, None);
- }
- }
-
- // check out the inclusion branch first
- (BranchStrategy::Continue, None)
- };
-
- // determine the sum of absolute and effective values for the current selection
- let (selected_abs, selected_eff) = selector.selected().fold((0, 0), |(abs, eff), (_, c)| {
- (
- abs + c.value,
- eff + c.effective_value(selector.opts.target_feerate),
- )
- });
-
- let bnb = Bnb::new(selector, pool, i64::MAX);
-
- // not enough to select anyway
- if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff {
- return None;
- }
-
- match limit.into() {
- BnbLimit::Rounds(rounds) => {
- bnb.into_iter(&strategy)
- .take(rounds)
- .reduce(|b, c| if c.is_some() { c } else { b })
- }
- #[cfg(feature = "std")]
- BnbLimit::Duration(duration) => {
- let start = std::time::SystemTime::now();
- bnb.into_iter(&strategy)
- .take_while(|_| start.elapsed().expect("failed to get system time") <= duration)
- .reduce(|b, c| if c.is_some() { c } else { b })
- }
- }?
-}
-
-// #[cfg(all(test, feature = "miniscript"))]
-// mod test {
-// use bitcoin::secp256k1::Secp256k1;
-//
-// use crate::coin_select::{evaluate_cs::evaluate, ExcessStrategyKind};
-//
-// use super::{
-// coin_select_bnb,
-// evaluate_cs::{Evaluation, EvaluationError},
-// tester::Tester,
-// CoinSelector, CoinSelectorOpt, Vec, WeightedValue,
-// };
-//
-// fn tester() -> Tester {
-// const DESC_STR: &str = "tr(xprv9uBuvtdjghkz8D1qzsSXS9Vs64mqrUnXqzNccj2xcvnCHPpXKYE1U2Gbh9CDHk8UPyF2VuXpVkDA7fk5ZP4Hd9KnhUmTscKmhee9Dp5sBMK)";
-// Tester::new(&Secp256k1::default(), DESC_STR)
-// }
-//
-// fn evaluate_bnb(
-// initial_selector: CoinSelector,
-// max_tries: usize,
-// ) -> Result<Evaluation, EvaluationError> {
-// evaluate(initial_selector, |cs| {
-// coin_select_bnb(max_tries, cs.clone()).map_or(false, |new_cs| {
-// *cs = new_cs;
-// true
-// })
-// })
-// }
-//
-// #[test]
-// fn not_enough_coins() {
-// let t = tester();
-// let candidates: Vec<WeightedValue> = vec![
-// t.gen_candidate(0, 100_000).into(),
-// t.gen_candidate(1, 100_000).into(),
-// ];
-// let opts = t.gen_opts(200_000);
-// let selector = CoinSelector::new(&candidates, &opts);
-// assert!(!coin_select_bnb(10_000, selector).is_some());
-// }
-//
-// #[test]
-// fn exactly_enough_coins_preselected() {
-// let t = tester();
-// let candidates: Vec<WeightedValue> = vec![
-// t.gen_candidate(0, 100_000).into(), // to preselect
-// t.gen_candidate(1, 100_000).into(), // to preselect
-// t.gen_candidate(2, 100_000).into(),
-// ];
-// let opts = CoinSelectorOpt {
-// target_feerate: 0.0,
-// ..t.gen_opts(200_000)
-// };
-// let selector = {
-// let mut selector = CoinSelector::new(&candidates, &opts);
-// selector.select(0); // preselect
-// selector.select(1); // preselect
-// selector
-// };
-//
-// let evaluation = evaluate_bnb(selector, 10_000).expect("eval failed");
-// println!("{}", evaluation);
-// assert_eq!(evaluation.solution.selected, (0..=1).collect());
-// assert_eq!(evaluation.solution.excess_strategies.len(), 1);
-// assert_eq!(
-// evaluation.feerate_offset(ExcessStrategyKind::ToFee).floor(),
-// 0.0
-// );
-// }
-//
-// /// `cost_of_change` acts as the upper-bound in Bnb; we check whether these boundaries are
-// /// enforced in code
-// #[test]
-// fn cost_of_change() {
-// let t = tester();
-// let candidates: Vec<WeightedValue> = vec![
-// t.gen_candidate(0, 200_000).into(),
-// t.gen_candidate(1, 200_000).into(),
-// t.gen_candidate(2, 200_000).into(),
-// ];
-//
-// // lowest and highest possible `recipient_value` opts for derived `drain_waste`, assuming
-// // that we want 2 candidates selected
-// let (lowest_opts, highest_opts) = {
-// let opts = t.gen_opts(0);
-//
-// let fee_from_inputs =
-// (candidates[0].weight as f32 * opts.target_feerate).ceil() as u64 * 2;
-// let fee_from_template =
-// ((opts.base_weight + 2) as f32 * opts.target_feerate).ceil() as u64;
-//
-// let lowest_opts = CoinSelectorOpt {
-// target_value: Some(
-// 400_000 - fee_from_inputs - fee_from_template - opts.drain_waste() as u64,
-// ),
-// ..opts
-// };
-//
-// let highest_opts = CoinSelectorOpt {
-// target_value: Some(400_000 - fee_from_inputs - fee_from_template),
-// ..opts
-// };
-//
-// (lowest_opts, highest_opts)
-// };
-//
-// // test lowest possible target we can select
-// let lowest_eval = evaluate_bnb(CoinSelector::new(&candidates, &lowest_opts), 10_000);
-// assert!(lowest_eval.is_ok());
-// let lowest_eval = lowest_eval.unwrap();
-// println!("LB {}", lowest_eval);
-// assert_eq!(lowest_eval.solution.selected.len(), 2);
-// assert_eq!(lowest_eval.solution.excess_strategies.len(), 1);
-// assert_eq!(
-// lowest_eval
-// .feerate_offset(ExcessStrategyKind::ToFee)
-// .floor(),
-// 0.0
-// );
-//
-// // test the highest possible target we can select
-// let highest_eval = evaluate_bnb(CoinSelector::new(&candidates, &highest_opts), 10_000);
-// assert!(highest_eval.is_ok());
-// let highest_eval = highest_eval.unwrap();
-// println!("UB {}", highest_eval);
-// assert_eq!(highest_eval.solution.selected.len(), 2);
-// assert_eq!(highest_eval.solution.excess_strategies.len(), 1);
-// assert_eq!(
-// highest_eval
-// .feerate_offset(ExcessStrategyKind::ToFee)
-// .floor(),
-// 0.0
-// );
-//
-// // test lower out of bounds
-// let loob_opts = CoinSelectorOpt {
-// target_value: lowest_opts.target_value.map(|v| v - 1),
-// ..lowest_opts
-// };
-// let loob_eval = evaluate_bnb(CoinSelector::new(&candidates, &loob_opts), 10_000);
-// assert!(loob_eval.is_err());
-// println!("Lower OOB: {}", loob_eval.unwrap_err());
-//
-// // test upper out of bounds
-// let uoob_opts = CoinSelectorOpt {
-// target_value: highest_opts.target_value.map(|v| v + 1),
-// ..highest_opts
-// };
-// let uoob_eval = evaluate_bnb(CoinSelector::new(&candidates, &uoob_opts), 10_000);
-// assert!(uoob_eval.is_err());
-// println!("Upper OOB: {}", uoob_eval.unwrap_err());
-// }
-//
-// #[test]
-// fn try_select() {
-// let t = tester();
-// let candidates: Vec<WeightedValue> = vec![
-// t.gen_candidate(0, 300_000).into(),
-// t.gen_candidate(1, 300_000).into(),
-// t.gen_candidate(2, 300_000).into(),
-// t.gen_candidate(3, 200_000).into(),
-// t.gen_candidate(4, 200_000).into(),
-// ];
-// let make_opts = |v: u64| -> CoinSelectorOpt {
-// CoinSelectorOpt {
-// target_feerate: 0.0,
-// ..t.gen_opts(v)
-// }
-// };
-//
-// let test_cases = vec![
-// (make_opts(100_000), false, 0),
-// (make_opts(200_000), true, 1),
-// (make_opts(300_000), true, 1),
-// (make_opts(500_000), true, 2),
-// (make_opts(1_000_000), true, 4),
-// (make_opts(1_200_000), false, 0),
-// (make_opts(1_300_000), true, 5),
-// (make_opts(1_400_000), false, 0),
-// ];
-//
-// for (opts, expect_solution, expect_selected) in test_cases {
-// let res = evaluate_bnb(CoinSelector::new(&candidates, &opts), 10_000);
-// assert_eq!(res.is_ok(), expect_solution);
-//
-// match res {
-// Ok(eval) => {
-// println!("{}", eval);
-// assert_eq!(eval.feerate_offset(ExcessStrategyKind::ToFee), 0.0);
-// assert_eq!(eval.solution.selected.len(), expect_selected as _);
-// }
-// Err(err) => println!("expected failure: {}", err),
-// }
-// }
-// }
-//
-// #[test]
-// fn early_bailout_optimization() {
-// let t = tester();
-//
-// // target: 300_000
-// // candidates: 2x of 125_000, 1000x of 100_000, 1x of 50_000
-// // expected solution: 2x 125_000, 1x 50_000
-// // set bnb max tries: 1100, should succeed
-// let candidates = {
-// let mut candidates: Vec<WeightedValue> = vec![
-// t.gen_candidate(0, 125_000).into(),
-// t.gen_candidate(1, 125_000).into(),
-// t.gen_candidate(2, 50_000).into(),
-// ];
-// (3..3 + 1000_u32)
-// .for_each(|index| candidates.push(t.gen_candidate(index, 100_000).into()));
-// candidates
-// };
-// let opts = CoinSelectorOpt {
-// target_feerate: 0.0,
-// ..t.gen_opts(300_000)
-// };
-//
-// let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 1100);
-// assert!(result.is_ok());
-//
-// let eval = result.unwrap();
-// println!("{}", eval);
-// assert_eq!(eval.solution.selected, (0..=2).collect());
-// }
-//
-// #[test]
-// fn should_exhaust_iteration() {
-// static MAX_TRIES: usize = 1000;
-// let t = tester();
-// let candidates = (0..MAX_TRIES + 1)
-// .map(|index| t.gen_candidate(index as _, 10_000).into())
-// .collect::<Vec<WeightedValue>>();
-// let opts = t.gen_opts(10_001 * MAX_TRIES as u64);
-// let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), MAX_TRIES);
-// assert!(result.is_err());
-// println!("error as expected: {}", result.unwrap_err());
-// }
-//
-// /// Solution should have fee >= min_absolute_fee (or no solution at all)
-// #[test]
-// fn min_absolute_fee() {
-// let t = tester();
-// let candidates = {
-// let mut candidates = Vec::new();
-// t.gen_weighted_values(&mut candidates, 5, 10_000);
-// t.gen_weighted_values(&mut candidates, 5, 20_000);
-// t.gen_weighted_values(&mut candidates, 5, 30_000);
-// t.gen_weighted_values(&mut candidates, 10, 10_300);
-// t.gen_weighted_values(&mut candidates, 10, 10_500);
-// t.gen_weighted_values(&mut candidates, 10, 10_700);
-// t.gen_weighted_values(&mut candidates, 10, 10_900);
-// t.gen_weighted_values(&mut candidates, 10, 11_000);
-// t.gen_weighted_values(&mut candidates, 10, 12_000);
-// t.gen_weighted_values(&mut candidates, 10, 13_000);
-// candidates
-// };
-// let mut opts = CoinSelectorOpt {
-// min_absolute_fee: 1,
-// ..t.gen_opts(100_000)
-// };
-//
-// (1..=120_u64).for_each(|fee_factor| {
-// opts.min_absolute_fee = fee_factor * 31;
-//
-// let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 21_000);
-// match result {
-// Ok(result) => {
-// println!("Solution {}", result);
-// let fee = result.solution.excess_strategies[&ExcessStrategyKind::ToFee].fee;
-// assert!(fee >= opts.min_absolute_fee);
-// assert_eq!(result.solution.excess_strategies.len(), 1);
-// }
-// Err(err) => {
-// println!("No Solution: {}", err);
-// }
-// }
-// });
-// }
-//
-// /// For a decreasing feerate (long-term feerate is lower than effective feerate), we should
-// /// select less. For increasing feerate (long-term feerate is higher than effective feerate), we
-// /// should select more.
-// #[test]
-// fn feerate_difference() {
-// let t = tester();
-// let candidates = {
-// let mut candidates = Vec::new();
-// t.gen_weighted_values(&mut candidates, 10, 2_000);
-// t.gen_weighted_values(&mut candidates, 10, 5_000);
-// t.gen_weighted_values(&mut candidates, 10, 20_000);
-// candidates
-// };
-//
-// let decreasing_feerate_opts = CoinSelectorOpt {
-// target_feerate: 1.25,
-// long_term_feerate: Some(0.25),
-// ..t.gen_opts(100_000)
-// };
-//
-// let increasing_feerate_opts = CoinSelectorOpt {
-// target_feerate: 0.25,
-// long_term_feerate: Some(1.25),
-// ..t.gen_opts(100_000)
-// };
-//
-// let decreasing_res = evaluate_bnb(
-// CoinSelector::new(&candidates, &decreasing_feerate_opts),
-// 21_000,
-// )
-// .expect("no result");
-// let decreasing_len = decreasing_res.solution.selected.len();
-//
-// let increasing_res = evaluate_bnb(
-// CoinSelector::new(&candidates, &increasing_feerate_opts),
-// 21_000,
-// )
-// .expect("no result");
-// let increasing_len = increasing_res.solution.selected.len();
-//
-// println!("decreasing_len: {}", decreasing_len);
-// println!("increasing_len: {}", increasing_len);
-// assert!(decreasing_len < increasing_len);
-// }
-//
-// /// TODO: UNIMPLEMENTED TESTS:
-// /// * Excess strategies:
-// /// * We should always have `ExcessStrategy::ToFee`.
-// /// * We should only have `ExcessStrategy::ToRecipient` when `max_extra_target > 0`.
-// /// * We should only have `ExcessStrategy::ToDrain` when `drain_value >= min_drain_value`.
-// /// * Fuzz
-// /// * Solution feerate should never be lower than target feerate
-// /// * Solution fee should never be lower than `min_absolute_fee`.
-// /// * Preselected should always remain selected
-// fn _todo() {}
-// }
+++ /dev/null
-use super::*;
-
-/// A [`WeightedValue`] represents an input candidate for [`CoinSelector`]. This can either be a
-/// single UTXO, or a group of UTXOs that should be spent together.
-#[derive(Debug, Clone, Copy)]
-pub struct WeightedValue {
- /// Total value of the UTXO(s) that this [`WeightedValue`] represents.
- pub value: u64,
- /// Total weight of including this/these UTXO(s).
- /// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`,
- /// `scriptWitness` should all be included.
- pub weight: u32,
- /// The total number of inputs; so we can calculate extra `varint` weight due to `vin` length changes.
- pub input_count: usize,
- /// Whether this [`WeightedValue`] contains at least one segwit spend.
- pub is_segwit: bool,
-}
-
-impl WeightedValue {
- /// Create a new [`WeightedValue`] that represents a single input.
- ///
- /// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen +
- /// scriptWitness`.
- pub fn new(value: u64, satisfaction_weight: u32, is_segwit: bool) -> WeightedValue {
- let weight = TXIN_BASE_WEIGHT + satisfaction_weight;
- WeightedValue {
- value,
- weight,
- input_count: 1,
- is_segwit,
- }
- }
-
- /// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`.
- pub fn effective_value(&self, effective_feerate: f32) -> i64 {
- // We prefer undershooting the candidate's effective value (so we over-estimate the fee of a
- // candidate). If we overshoot the candidate's effective value, it may be possible to find a
- // solution which does not meet the target feerate.
- self.value as i64 - (self.weight as f32 * effective_feerate).ceil() as i64
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct CoinSelectorOpt {
- /// The value we need to select.
- /// If the value is `None`, then the selection will be complete if it can pay for the drain
- /// output and satisfy the other constraints (e.g., minimum fees).
- pub target_value: Option<u64>,
- /// Additional leeway for the target value.
- pub max_extra_target: u64, // TODO: Maybe out of scope here?
-
- /// The feerate we should try and achieve in sats per weight unit.
- pub target_feerate: f32,
- /// The feerate
- pub long_term_feerate: Option<f32>, // TODO: Maybe out of scope? (waste)
- /// The minimum absolute fee. I.e., needed for RBF.
- pub min_absolute_fee: u64,
-
- /// The weight of the template transaction, including fixed fields and outputs.
- pub base_weight: u32,
- /// Additional weight if we include the drain (change) output.
- pub drain_weight: u32,
- /// Weight of spending the drain (change) output in the future.
- pub spend_drain_weight: u32, // TODO: Maybe out of scope? (waste)
-
- /// Minimum value allowed for a drain (change) output.
- pub min_drain_value: u64,
-}
-
-impl CoinSelectorOpt {
- fn from_weights(base_weight: u32, drain_weight: u32, spend_drain_weight: u32) -> Self {
- // 0.25 sats/wu == 1 sat/vb
- let target_feerate = 0.25_f32;
-
- // set `min_drain_value` to dust limit
- let min_drain_value =
- 3 * ((drain_weight + spend_drain_weight) as f32 * target_feerate) as u64;
-
- Self {
- target_value: None,
- max_extra_target: 0,
- target_feerate,
- long_term_feerate: None,
- min_absolute_fee: 0,
- base_weight,
- drain_weight,
- spend_drain_weight,
- min_drain_value,
- }
- }
-
- pub fn fund_outputs(
- txouts: &[TxOut],
- drain_output: &TxOut,
- drain_satisfaction_weight: u32,
- ) -> Self {
- let mut tx = Transaction {
- input: vec![],
- version: transaction::Version::ONE,
- lock_time: absolute::LockTime::ZERO,
- output: txouts.to_vec(),
- };
- let base_weight = tx.weight();
- // Calculating drain_weight like this instead of using .weight()
- // allows us to take into account the output len varint increase that
- // might happen when adding a new output
- let drain_weight = {
- tx.output.push(drain_output.clone());
- tx.weight() - base_weight
- };
- Self {
- target_value: if txouts.is_empty() {
- None
- } else {
- Some(txouts.iter().map(|txout| txout.value.to_sat()).sum())
- },
- ..Self::from_weights(
- base_weight.to_wu() as u32,
- drain_weight.to_wu() as u32,
- TXIN_BASE_WEIGHT + drain_satisfaction_weight,
- )
- }
- }
-
- pub fn long_term_feerate(&self) -> f32 {
- self.long_term_feerate.unwrap_or(self.target_feerate)
- }
-
- pub fn drain_waste(&self) -> i64 {
- (self.drain_weight as f32 * self.target_feerate
- + self.spend_drain_weight as f32 * self.long_term_feerate()) as i64
- }
-}
-
-/// [`CoinSelector`] selects and deselects from a set of candidates.
-#[derive(Debug, Clone)]
-pub struct CoinSelector<'a> {
- pub opts: &'a CoinSelectorOpt,
- pub candidates: &'a Vec<WeightedValue>,
- selected: BTreeSet<usize>,
-}
-
-impl<'a> CoinSelector<'a> {
- pub fn candidate(&self, index: usize) -> &WeightedValue {
- &self.candidates[index]
- }
-
- pub fn new(candidates: &'a Vec<WeightedValue>, opts: &'a CoinSelectorOpt) -> Self {
- Self {
- candidates,
- selected: Default::default(),
- opts,
- }
- }
-
- pub fn select(&mut self, index: usize) -> bool {
- assert!(index < self.candidates.len());
- self.selected.insert(index)
- }
-
- pub fn deselect(&mut self, index: usize) -> bool {
- self.selected.remove(&index)
- }
-
- pub fn is_selected(&self, index: usize) -> bool {
- self.selected.contains(&index)
- }
-
- pub fn is_empty(&self) -> bool {
- self.selected.is_empty()
- }
-
- /// Weight sum of all selected inputs.
- pub fn selected_weight(&self) -> u32 {
- self.selected
- .iter()
- .map(|&index| self.candidates[index].weight)
- .sum()
- }
-
- /// Effective value sum of all selected inputs.
- pub fn selected_effective_value(&self) -> i64 {
- self.selected
- .iter()
- .map(|&index| self.candidates[index].effective_value(self.opts.target_feerate))
- .sum()
- }
-
- /// Absolute value sum of all selected inputs.
- pub fn selected_absolute_value(&self) -> u64 {
- self.selected
- .iter()
- .map(|&index| self.candidates[index].value)
- .sum()
- }
-
- /// Waste sum of all selected inputs.
- pub fn selected_waste(&self) -> i64 {
- (self.selected_weight() as f32 * (self.opts.target_feerate - self.opts.long_term_feerate()))
- as i64
- }
-
- /// Current weight of template tx + selected inputs.
- pub fn current_weight(&self) -> u32 {
- let witness_header_extra_weight = self
- .selected()
- .find(|(_, wv)| wv.is_segwit)
- .map(|_| 2)
- .unwrap_or(0);
- let vin_count_varint_extra_weight = {
- let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::<usize>();
- (varint_size(input_count) - 1) * 4
- };
- self.opts.base_weight
- + self.selected_weight()
- + witness_header_extra_weight
- + vin_count_varint_extra_weight
- }
-
- /// Current excess.
- pub fn current_excess(&self) -> i64 {
- self.selected_effective_value() - self.effective_target()
- }
-
- /// This is the effective target value.
- pub fn effective_target(&self) -> i64 {
- let (has_segwit, max_input_count) = self
- .candidates
- .iter()
- .fold((false, 0_usize), |(is_segwit, input_count), c| {
- (is_segwit || c.is_segwit, input_count + c.input_count)
- });
-
- let effective_base_weight = self.opts.base_weight
- + if has_segwit { 2_u32 } else { 0_u32 }
- + (varint_size(max_input_count) - 1) * 4;
-
- self.opts.target_value.unwrap_or(0) as i64
- + (effective_base_weight as f32 * self.opts.target_feerate).ceil() as i64
- }
-
- pub fn selected_count(&self) -> usize {
- self.selected.len()
- }
-
- pub fn selected(&self) -> impl Iterator<Item = (usize, &'a WeightedValue)> + '_ {
- self.selected
- .iter()
- .map(move |&index| (index, &self.candidates[index]))
- }
-
- pub fn unselected(&self) -> impl Iterator<Item = (usize, &'a WeightedValue)> + '_ {
- self.candidates
- .iter()
- .enumerate()
- .filter(move |(index, _)| !self.selected.contains(index))
- }
-
- pub fn selected_indexes(&self) -> impl Iterator<Item = usize> + '_ {
- self.selected.iter().cloned()
- }
-
- pub fn unselected_indexes(&self) -> impl Iterator<Item = usize> + '_ {
- (0..self.candidates.len()).filter(move |index| !self.selected.contains(index))
- }
-
- pub fn all_selected(&self) -> bool {
- self.selected.len() == self.candidates.len()
- }
-
- pub fn select_all(&mut self) {
- self.selected = (0..self.candidates.len()).collect();
- }
-
- pub fn select_until_finished(&mut self) -> Result<Selection, SelectionError> {
- let mut selection = self.finish();
-
- if selection.is_ok() {
- return selection;
- }
-
- let unselected = self.unselected_indexes().collect::<Vec<_>>();
-
- for index in unselected {
- self.select(index);
- selection = self.finish();
-
- if selection.is_ok() {
- break;
- }
- }
-
- selection
- }
-
- pub fn finish(&self) -> Result<Selection, SelectionError> {
- let weight_without_drain = self.current_weight();
- let weight_with_drain = weight_without_drain + self.opts.drain_weight;
-
- let fee_without_drain =
- (weight_without_drain as f32 * self.opts.target_feerate).ceil() as u64;
- let fee_with_drain = (weight_with_drain as f32 * self.opts.target_feerate).ceil() as u64;
-
- let inputs_minus_outputs = {
- let target_value = self.opts.target_value.unwrap_or(0);
- let selected = self.selected_absolute_value();
-
- // find the largest unsatisfied constraint (if any), and return the error of that constraint
- // "selected" should always be greater than or equal to these selected values
- [
- (
- SelectionConstraint::TargetValue,
- target_value.saturating_sub(selected),
- ),
- (
- SelectionConstraint::TargetFee,
- (target_value + fee_without_drain).saturating_sub(selected),
- ),
- (
- SelectionConstraint::MinAbsoluteFee,
- (target_value + self.opts.min_absolute_fee).saturating_sub(selected),
- ),
- (
- SelectionConstraint::MinDrainValue,
- // when we have no target value (hence no recipient txouts), we need to ensure
- // the selected amount can satisfy requirements for a drain output (so we at least have one txout)
- if self.opts.target_value.is_none() {
- (fee_with_drain + self.opts.min_drain_value).saturating_sub(selected)
- } else {
- 0
- },
- ),
- ]
- .iter()
- .filter(|&(_, v)| v > &0)
- .max_by_key(|&(_, v)| v)
- .map_or(Ok(()), |(constraint, missing)| {
- Err(SelectionError {
- selected,
- missing: *missing,
- constraint: *constraint,
- })
- })?;
-
- selected - target_value
- };
-
- let fee_without_drain = fee_without_drain.max(self.opts.min_absolute_fee);
- let fee_with_drain = fee_with_drain.max(self.opts.min_absolute_fee);
-
- let excess_without_drain = inputs_minus_outputs - fee_without_drain;
- let input_waste = self.selected_waste();
-
- // begin preparing excess strategies for final selection
- let mut excess_strategies = HashMap::new();
-
- // only allow `ToFee` and `ToRecipient` excess strategies when we have a `target_value`,
- // otherwise, we will result in a result with no txouts, or attempt to add value to an output
- // that does not exist.
- if self.opts.target_value.is_some() {
- // no drain, excess to fee
- excess_strategies.insert(
- ExcessStrategyKind::ToFee,
- ExcessStrategy {
- recipient_value: self.opts.target_value,
- drain_value: None,
- fee: fee_without_drain + excess_without_drain,
- weight: weight_without_drain,
- waste: input_waste + excess_without_drain as i64,
- },
- );
-
- // no drain, send the excess to the recipient
- // if `excess == 0`, this result will be the same as the previous, so don't consider it
- // if `max_extra_target == 0`, there is no leeway for this strategy
- if excess_without_drain > 0 && self.opts.max_extra_target > 0 {
- let extra_recipient_value =
- core::cmp::min(self.opts.max_extra_target, excess_without_drain);
- let extra_fee = excess_without_drain - extra_recipient_value;
- excess_strategies.insert(
- ExcessStrategyKind::ToRecipient,
- ExcessStrategy {
- recipient_value: self.opts.target_value.map(|v| v + extra_recipient_value),
- drain_value: None,
- fee: fee_without_drain + extra_fee,
- weight: weight_without_drain,
- waste: input_waste + extra_fee as i64,
- },
- );
- }
- }
-
- // with drain
- if fee_with_drain >= self.opts.min_absolute_fee
- && inputs_minus_outputs >= fee_with_drain + self.opts.min_drain_value
- {
- excess_strategies.insert(
- ExcessStrategyKind::ToDrain,
- ExcessStrategy {
- recipient_value: self.opts.target_value,
- drain_value: Some(inputs_minus_outputs.saturating_sub(fee_with_drain)),
- fee: fee_with_drain,
- weight: weight_with_drain,
- waste: input_waste + self.opts.drain_waste(),
- },
- );
- }
-
- debug_assert!(
- !excess_strategies.is_empty(),
- "should have at least one excess strategy."
- );
-
- Ok(Selection {
- selected: self.selected.clone(),
- excess: excess_without_drain,
- excess_strategies,
- })
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct SelectionError {
- selected: u64,
- missing: u64,
- constraint: SelectionConstraint,
-}
-
-impl core::fmt::Display for SelectionError {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- let SelectionError {
- selected,
- missing,
- constraint,
- } = self;
- write!(
- f,
- "insufficient coins selected; selected={}, missing={}, unsatisfied_constraint={:?}",
- selected, missing, constraint
- )
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for SelectionError {}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum SelectionConstraint {
- /// The target is not met
- TargetValue,
- /// The target fee (given the feerate) is not met
- TargetFee,
- /// Min absolute fee is not met
- MinAbsoluteFee,
- /// Min drain value is not met
- MinDrainValue,
-}
-
-impl core::fmt::Display for SelectionConstraint {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- SelectionConstraint::TargetValue => core::write!(f, "target_value"),
- SelectionConstraint::TargetFee => core::write!(f, "target_fee"),
- SelectionConstraint::MinAbsoluteFee => core::write!(f, "min_absolute_fee"),
- SelectionConstraint::MinDrainValue => core::write!(f, "min_drain_value"),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Selection {
- pub selected: BTreeSet<usize>,
- pub excess: u64,
- pub excess_strategies: HashMap<ExcessStrategyKind, ExcessStrategy>,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
-pub enum ExcessStrategyKind {
- ToFee,
- ToRecipient,
- ToDrain,
-}
-
-#[derive(Clone, Copy, Debug)]
-pub struct ExcessStrategy {
- pub recipient_value: Option<u64>,
- pub drain_value: Option<u64>,
- pub fee: u64,
- pub weight: u32,
- pub waste: i64,
-}
-
-impl Selection {
- pub fn apply_selection<'a, T>(
- &'a self,
- candidates: &'a [T],
- ) -> impl Iterator<Item = &'a T> + 'a {
- self.selected.iter().map(move |i| &candidates[*i])
- }
-
- /// Returns the [`ExcessStrategy`] that results in the least waste.
- pub fn best_strategy(&self) -> (&ExcessStrategyKind, &ExcessStrategy) {
- self.excess_strategies
- .iter()
- .min_by_key(|&(_, a)| a.waste)
- .expect("selection has no excess strategy")
- }
-}
-
-impl core::fmt::Display for ExcessStrategyKind {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- ExcessStrategyKind::ToFee => core::write!(f, "to_fee"),
- ExcessStrategyKind::ToRecipient => core::write!(f, "to_recipient"),
- ExcessStrategyKind::ToDrain => core::write!(f, "to_drain"),
- }
- }
-}
-
-impl ExcessStrategy {
- /// Returns feerate in sats/wu.
- pub fn feerate(&self) -> f32 {
- self.fee as f32 / self.weight as f32
- }
-}
-
-#[cfg(test)]
-mod test {
- use crate::{ExcessStrategyKind, SelectionConstraint};
-
- use super::{CoinSelector, CoinSelectorOpt, WeightedValue};
-
- /// Ensure `target_value` is respected. Can't have any disrespect.
- #[test]
- fn target_value_respected() {
- let target_value = 1000_u64;
-
- let candidates = (500..1500_u64)
- .map(|value| WeightedValue {
- value,
- weight: 100,
- input_count: 1,
- is_segwit: false,
- })
- .collect::<super::Vec<_>>();
-
- let opts = CoinSelectorOpt {
- target_value: Some(target_value),
- max_extra_target: 0,
- target_feerate: 0.00,
- long_term_feerate: None,
- min_absolute_fee: 0,
- base_weight: 10,
- drain_weight: 10,
- spend_drain_weight: 10,
- min_drain_value: 10,
- };
-
- for (index, v) in candidates.iter().enumerate() {
- let mut selector = CoinSelector::new(&candidates, &opts);
- assert!(selector.select(index));
-
- let res = selector.finish();
- if v.value < opts.target_value.unwrap_or(0) {
- let err = res.expect_err("should have failed");
- assert_eq!(err.selected, v.value);
- assert_eq!(err.missing, target_value - v.value);
- assert_eq!(err.constraint, SelectionConstraint::MinAbsoluteFee);
- } else {
- let sel = res.expect("should have succeeded");
- assert_eq!(sel.excess, v.value - opts.target_value.unwrap_or(0));
- }
- }
- }
-
- #[test]
- fn drain_all() {
- let candidates = (0..100)
- .map(|_| WeightedValue {
- value: 666,
- weight: 166,
- input_count: 1,
- is_segwit: false,
- })
- .collect::<super::Vec<_>>();
-
- let opts = CoinSelectorOpt {
- target_value: None,
- max_extra_target: 0,
- target_feerate: 0.25,
- long_term_feerate: None,
- min_absolute_fee: 0,
- base_weight: 10,
- drain_weight: 100,
- spend_drain_weight: 66,
- min_drain_value: 1000,
- };
-
- let selection = CoinSelector::new(&candidates, &opts)
- .select_until_finished()
- .expect("should succeed");
-
- assert!(selection.selected.len() > 1);
- assert_eq!(selection.excess_strategies.len(), 1);
-
- let (kind, strategy) = selection.best_strategy();
- assert_eq!(*kind, ExcessStrategyKind::ToDrain);
- assert!(strategy.recipient_value.is_none());
- assert!(strategy.drain_value.is_some());
- }
-
- /// TODO: Tests to add:
- /// * `finish` should ensure at least `target_value` is selected.
- /// * actual feerate should be equal or higher than `target_feerate`.
- /// * actual drain value should be equal to or higher than `min_drain_value` (or else no drain).
- fn _todo() {}
-}
+++ /dev/null
-#![no_std]
-
-#[cfg(feature = "std")]
-extern crate std;
-
-#[macro_use]
-extern crate alloc;
-extern crate bdk_chain;
-
-use alloc::vec::Vec;
-use bdk_chain::{
- bitcoin,
- collections::{BTreeSet, HashMap},
-};
-use bitcoin::{absolute, transaction, Transaction, TxOut};
-use core::fmt::{Debug, Display};
-
-mod coin_selector;
-pub use coin_selector::*;
-
-mod bnb;
-pub use bnb::*;
-
-/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4). This does not include
-/// `scriptSigLen` or `scriptSig`.
-pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4;
-
-/// Helper to calculate varint size. `v` is the value the varint represents.
-// Shamelessly copied from
-// https://github.com/rust-bitcoin/rust-miniscript/blob/d5615acda1a7fdc4041a11c1736af139b8c7ebe8/src/util.rs#L8
-pub(crate) fn varint_size(v: usize) -> u32 {
- bitcoin::VarInt(v as u64).size() as u32
-}
+++ /dev/null
-[package]
-name = "bdk_tmp_plan"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-bdk_chain = { path = "../../crates/chain", features = ["miniscript"] }
-
-[features]
-default = ["std"]
-std = []
+++ /dev/null
-# Temporary planning module
-
-A temporary place to hold the planning module until https://github.com/rust-bitcoin/rust-miniscript/pull/481 is merged and released
+++ /dev/null
-#![allow(unused)]
-#![allow(missing_docs)]
-#![allow(clippy::all)] // FIXME
-//! A spending plan or *plan* for short is a representation of a particular spending path on a
-//! descriptor. This allows us to analayze a choice of spending path without producing any
-//! signatures or other witness data for it.
-//!
-//! To make a plan you provide the descriptor with "assets" like which keys you are able to use, hash
-//! pre-images you have access to, the current block height etc.
-//!
-//! Once you've got a plan it can tell you its expected satisfaction weight which can be useful for
-//! doing coin selection. Furthermore it provides which subset of those keys and hash pre-images you
-//! will actually need as well as what locktime or sequence number you need to set.
-//!
-//! Once you've obstained signatures, hash pre-images etc required by the plan, it can create a
-//! witness/script_sig for the input.
-use bdk_chain::{bitcoin, collections::*, miniscript};
-use bitcoin::{
- absolute,
- bip32::{DerivationPath, Fingerprint, KeySource},
- ecdsa,
- hashes::{hash160, ripemd160, sha256},
- secp256k1::Secp256k1,
- taproot::{self, LeafVersion, TapLeafHash},
- transaction::Sequence,
- ScriptBuf, TxIn, Witness, WitnessVersion,
-};
-use miniscript::{
- descriptor::{InnerXKey, Tr},
- hash256, DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, ScriptContext, ToPublicKey,
-};
-
-pub(crate) fn varint_len(v: usize) -> usize {
- bitcoin::VarInt(v as u64).size() as usize
-}
-
-mod plan_impls;
-mod requirements;
-mod template;
-pub use requirements::*;
-pub use template::PlanKey;
-use template::TemplateItem;
-
-#[derive(Clone, Debug)]
-enum TrSpend {
- KeySpend,
- LeafSpend {
- script: ScriptBuf,
- leaf_version: LeafVersion,
- },
-}
-
-#[derive(Clone, Debug)]
-enum Target {
- Legacy,
- Segwitv0 {
- script_code: ScriptBuf,
- },
- Segwitv1 {
- tr: Tr<DefiniteDescriptorKey>,
- tr_plan: TrSpend,
- },
-}
-
-impl Target {}
-
-#[derive(Clone, Debug)]
-/// A plan represents a particular spending path for a descriptor.
-///
-/// See the module level documentation for more info.
-pub struct Plan<AK> {
- template: Vec<TemplateItem<AK>>,
- target: Target,
- set_locktime: Option<absolute::LockTime>,
- set_sequence: Option<Sequence>,
-}
-
-impl Default for Target {
- fn default() -> Self {
- Target::Legacy
- }
-}
-
-#[derive(Clone, Debug, Default)]
-/// Signatures and hash pre-images that can be used to complete a plan.
-pub struct SatisfactionMaterial {
- /// Schnorr signautres under their keys
- pub schnorr_sigs: BTreeMap<DefiniteDescriptorKey, taproot::Signature>,
- /// ECDSA signatures under their keys
- pub ecdsa_sigs: BTreeMap<DefiniteDescriptorKey, ecdsa::Signature>,
- /// SHA256 pre-images under their images
- pub sha256_preimages: BTreeMap<sha256::Hash, Vec<u8>>,
- /// hash160 pre-images under their images
- pub hash160_preimages: BTreeMap<hash160::Hash, Vec<u8>>,
- /// hash256 pre-images under their images
- pub hash256_preimages: BTreeMap<hash256::Hash, Vec<u8>>,
- /// ripemd160 pre-images under their images
- pub ripemd160_preimages: BTreeMap<ripemd160::Hash, Vec<u8>>,
-}
-
-impl<Ak> Plan<Ak>
-where
- Ak: Clone,
-{
- /// The expected satisfaction weight for the plan if it is completed.
- pub fn expected_weight(&self) -> usize {
- let script_sig_size = match self.target {
- Target::Legacy => unimplemented!(), // self
- // .template
- // .iter()
- // .map(|step| {
- // let size = step.expected_size();
- // size + push_opcode_size(size)
- // })
- // .sum()
- Target::Segwitv0 { .. } | Target::Segwitv1 { .. } => 1,
- };
- let witness_elem_sizes: Option<Vec<usize>> = match &self.target {
- Target::Legacy => None,
- Target::Segwitv0 { .. } => Some(
- self.template
- .iter()
- .map(|step| step.expected_size())
- .collect(),
- ),
- Target::Segwitv1 { tr, tr_plan } => {
- let mut witness_elems = self
- .template
- .iter()
- .map(|step| step.expected_size())
- .collect::<Vec<_>>();
-
- if let TrSpend::LeafSpend {
- script,
- leaf_version,
- } = tr_plan
- {
- let control_block = tr
- .spend_info()
- .control_block(&(script.clone(), *leaf_version))
- .expect("must exist");
- witness_elems.push(script.len());
- witness_elems.push(control_block.size());
- }
-
- Some(witness_elems)
- }
- };
-
- let witness_size: usize = match witness_elem_sizes {
- Some(elems) => {
- varint_len(elems.len())
- + elems
- .into_iter()
- .map(|elem| varint_len(elem) + elem)
- .sum::<usize>()
- }
- None => 0,
- };
-
- script_sig_size * 4 + witness_size
- }
-
- pub fn requirements(&self) -> Requirements<Ak> {
- match self.try_complete(&SatisfactionMaterial::default()) {
- PlanState::Complete { .. } => Requirements::default(),
- PlanState::Incomplete(requirements) => requirements,
- }
- }
-
- pub fn try_complete(&self, auth_data: &SatisfactionMaterial) -> PlanState<Ak> {
- let unsatisfied_items = self
- .template
- .iter()
- .filter(|step| match step {
- TemplateItem::Sign(key) => {
- !auth_data.schnorr_sigs.contains_key(&key.descriptor_key)
- }
- TemplateItem::Hash160(image) => !auth_data.hash160_preimages.contains_key(image),
- TemplateItem::Hash256(image) => !auth_data.hash256_preimages.contains_key(image),
- TemplateItem::Sha256(image) => !auth_data.sha256_preimages.contains_key(image),
- TemplateItem::Ripemd160(image) => {
- !auth_data.ripemd160_preimages.contains_key(image)
- }
- TemplateItem::Pk { .. } | TemplateItem::One | TemplateItem::Zero => false,
- })
- .collect::<Vec<_>>();
-
- if unsatisfied_items.is_empty() {
- let mut witness = self
- .template
- .iter()
- .flat_map(|step| step.to_witness_stack(&auth_data))
- .collect::<Vec<_>>();
- match &self.target {
- Target::Segwitv0 { .. } => todo!(),
- Target::Legacy => todo!(),
- Target::Segwitv1 {
- tr_plan: TrSpend::KeySpend,
- ..
- } => PlanState::Complete {
- final_script_sig: None,
- final_script_witness: Some(Witness::from(witness)),
- },
- Target::Segwitv1 {
- tr,
- tr_plan:
- TrSpend::LeafSpend {
- script,
- leaf_version,
- },
- } => {
- let spend_info = tr.spend_info();
- let control_block = spend_info
- .control_block(&(script.clone(), *leaf_version))
- .expect("must exist");
- witness.push(script.clone().into_bytes());
- witness.push(control_block.serialize());
-
- PlanState::Complete {
- final_script_sig: None,
- final_script_witness: Some(Witness::from(witness)),
- }
- }
- }
- } else {
- let mut requirements = Requirements::default();
-
- match &self.target {
- Target::Legacy => {
- todo!()
- }
- Target::Segwitv0 { .. } => {
- todo!()
- }
- Target::Segwitv1 { tr, tr_plan } => {
- let spend_info = tr.spend_info();
- match tr_plan {
- TrSpend::KeySpend => match &self.template[..] {
- [TemplateItem::Sign(ref plan_key)] => {
- requirements.signatures = RequiredSignatures::TapKey {
- merkle_root: spend_info.merkle_root(),
- plan_key: plan_key.clone(),
- };
- }
- _ => unreachable!("tapkey spend will always have only one sign step"),
- },
- TrSpend::LeafSpend {
- script,
- leaf_version,
- } => {
- let leaf_hash = TapLeafHash::from_script(&script, *leaf_version);
- requirements.signatures = RequiredSignatures::TapScript {
- leaf_hash,
- plan_keys: vec![],
- }
- }
- }
- }
- }
-
- let required_signatures = match requirements.signatures {
- RequiredSignatures::Legacy { .. } => todo!(),
- RequiredSignatures::Segwitv0 { .. } => todo!(),
- RequiredSignatures::TapKey { .. } => return PlanState::Incomplete(requirements),
- RequiredSignatures::TapScript {
- plan_keys: ref mut keys,
- ..
- } => keys,
- };
-
- for step in unsatisfied_items {
- match step {
- TemplateItem::Sign(plan_key) => {
- required_signatures.push(plan_key.clone());
- }
- TemplateItem::Hash160(image) => {
- requirements.hash160_images.insert(image.clone());
- }
- TemplateItem::Hash256(image) => {
- requirements.hash256_images.insert(image.clone());
- }
- TemplateItem::Sha256(image) => {
- requirements.sha256_images.insert(image.clone());
- }
- TemplateItem::Ripemd160(image) => {
- requirements.ripemd160_images.insert(image.clone());
- }
- TemplateItem::Pk { .. } | TemplateItem::One | TemplateItem::Zero => { /* no requirements */
- }
- }
- }
-
- PlanState::Incomplete(requirements)
- }
- }
-
- /// Witness version for the plan
- pub fn witness_version(&self) -> Option<WitnessVersion> {
- match self.target {
- Target::Legacy => None,
- Target::Segwitv0 { .. } => Some(WitnessVersion::V0),
- Target::Segwitv1 { .. } => Some(WitnessVersion::V1),
- }
- }
-
- /// The minimum required locktime height or time on the transaction using the plan.
- pub fn required_locktime(&self) -> Option<absolute::LockTime> {
- self.set_locktime.clone()
- }
-
- /// The minimum required sequence (height or time) on the input to satisfy the plan
- pub fn required_sequence(&self) -> Option<Sequence> {
- self.set_sequence.clone()
- }
-
- /// The minimum required transaction version required on the transaction using the plan.
- pub fn min_version(&self) -> Option<u32> {
- if let Some(_) = self.set_sequence {
- Some(2)
- } else {
- Some(1)
- }
- }
-}
-
-/// The returned value from [`Plan::try_complete`].
-pub enum PlanState<Ak> {
- /// The plan is complete
- Complete {
- /// The script sig that should be set on the input
- final_script_sig: Option<ScriptBuf>,
- /// The witness that should be set on the input
- final_script_witness: Option<Witness>,
- },
- Incomplete(Requirements<Ak>),
-}
-
-#[derive(Clone, Debug)]
-pub struct Assets<K> {
- pub keys: Vec<K>,
- pub txo_age: Option<Sequence>,
- pub max_locktime: Option<absolute::LockTime>,
- pub sha256: Vec<sha256::Hash>,
- pub hash256: Vec<hash256::Hash>,
- pub ripemd160: Vec<ripemd160::Hash>,
- pub hash160: Vec<hash160::Hash>,
-}
-
-impl<K> Default for Assets<K> {
- fn default() -> Self {
- Self {
- keys: Default::default(),
- txo_age: Default::default(),
- max_locktime: Default::default(),
- sha256: Default::default(),
- hash256: Default::default(),
- ripemd160: Default::default(),
- hash160: Default::default(),
- }
- }
-}
-
-pub trait CanDerive {
- fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath>;
-}
-
-impl CanDerive for KeySource {
- fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath> {
- match DescriptorPublicKey::from(key.clone()) {
- DescriptorPublicKey::Single(single_pub) => {
- path_to_child(self, single_pub.origin.as_ref()?, None)
- }
- DescriptorPublicKey::XPub(dxk) => {
- let origin = dxk.origin.clone().unwrap_or_else(|| {
- let secp = Secp256k1::signing_only();
- (dxk.xkey.xkey_fingerprint(&secp), DerivationPath::master())
- });
-
- path_to_child(self, &origin, Some(&dxk.derivation_path))
- }
- DescriptorPublicKey::MultiXPub(_) => {
- // This crate will be replaced by
- // https://github.com/rust-bitcoin/rust-miniscript/pull/481 anyways
- todo!();
- }
- }
- }
-}
-
-impl CanDerive for DescriptorPublicKey {
- fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath> {
- match (self, DescriptorPublicKey::from(key.clone())) {
- (parent, child) if parent == &child => Some(DerivationPath::master()),
- (DescriptorPublicKey::XPub(parent), _) => {
- let origin = parent.origin.clone().unwrap_or_else(|| {
- let secp = Secp256k1::signing_only();
- (
- parent.xkey.xkey_fingerprint(&secp),
- DerivationPath::master(),
- )
- });
- KeySource::from(origin).can_derive(key)
- }
- _ => None,
- }
- }
-}
-
-fn path_to_child(
- parent: &KeySource,
- child_origin: &(Fingerprint, DerivationPath),
- child_derivation: Option<&DerivationPath>,
-) -> Option<DerivationPath> {
- if parent.0 == child_origin.0 {
- let mut remaining_derivation =
- DerivationPath::from(child_origin.1[..].strip_prefix(&parent.1[..])?);
- remaining_derivation =
- remaining_derivation.extend(child_derivation.unwrap_or(&DerivationPath::master()));
- Some(remaining_derivation)
- } else {
- None
- }
-}
-
-pub fn plan_satisfaction<Ak>(
- desc: &Descriptor<DefiniteDescriptorKey>,
- assets: &Assets<Ak>,
-) -> Option<Plan<Ak>>
-where
- Ak: CanDerive + Clone,
-{
- match desc {
- Descriptor::Bare(_) => todo!(),
- Descriptor::Pkh(_) => todo!(),
- Descriptor::Wpkh(_) => todo!(),
- Descriptor::Sh(_) => todo!(),
- Descriptor::Wsh(_) => todo!(),
- Descriptor::Tr(tr) => crate::plan_impls::plan_satisfaction_tr(tr, assets),
- }
-}
+++ /dev/null
-use bdk_chain::{bitcoin, miniscript};
-use bitcoin::locktime::absolute;
-use miniscript::Terminal;
-
-use super::*;
-
-impl<Ak> TermPlan<Ak> {
- fn combine(self, other: Self) -> Option<Self> {
- let min_locktime = {
- match (self.min_locktime, other.min_locktime) {
- (Some(lhs), Some(rhs)) => {
- if lhs.is_same_unit(rhs) {
- Some(if lhs.to_consensus_u32() > rhs.to_consensus_u32() {
- lhs
- } else {
- rhs
- })
- } else {
- return None;
- }
- }
- _ => self.min_locktime.or(other.min_locktime),
- }
- };
-
- let min_sequence = {
- match (self.min_sequence, other.min_sequence) {
- (Some(lhs), Some(rhs)) => {
- if lhs.is_height_locked() == rhs.is_height_locked() {
- Some(if lhs.to_consensus_u32() > rhs.to_consensus_u32() {
- lhs
- } else {
- rhs
- })
- } else {
- return None;
- }
- }
- _ => self.min_sequence.or(other.min_sequence),
- }
- };
-
- let mut template = self.template;
- template.extend(other.template);
-
- Some(Self {
- min_locktime,
- min_sequence,
- template,
- })
- }
-
- pub(crate) fn expected_size(&self) -> usize {
- self.template.iter().map(|step| step.expected_size()).sum()
- }
-}
-
-// impl crate::descriptor::Pkh<DefiniteDescriptorKey> {
-// pub(crate) fn plan_satisfaction<Ak>(&self, assets: &Assets<Ak>) -> Option<Plan<Ak>>
-// where
-// Ak: CanDerive + Clone,
-// {
-// let (asset_key, derivation_hint) = assets.keys.iter().find_map(|asset_key| {
-// let derivation_hint = asset_key.can_derive(self.as_inner())?;
-// Some((asset_key, derivation_hint))
-// })?;
-
-// Some(Plan {
-// template: vec![TemplateItem::Sign(PlanKey {
-// asset_key: asset_key.clone(),
-// descriptor_key: self.as_inner().clone(),
-// derivation_hint,
-// })],
-// target: Target::Legacy,
-// set_locktime: None,
-// set_sequence: None,
-// })
-// }
-// }
-
-// impl crate::descriptor::Wpkh<DefiniteDescriptorKey> {
-// pub(crate) fn plan_satisfaction<Ak>(&self, assets: &Assets<Ak>) -> Option<Plan<Ak>>
-// where
-// Ak: CanDerive + Clone,
-// {
-// let (asset_key, derivation_hint) = assets.keys.iter().find_map(|asset_key| {
-// let derivation_hint = asset_key.can_derive(self.as_inner())?;
-// Some((asset_key, derivation_hint))
-// })?;
-
-// Some(Plan {
-// template: vec![TemplateItem::Sign(PlanKey {
-// asset_key: asset_key.clone(),
-// descriptor_key: self.as_inner().clone(),
-// derivation_hint,
-// })],
-// target: Target::Segwitv0,
-// set_locktime: None,
-// set_sequence: None,
-// })
-// }
-// }
-
-pub(crate) fn plan_satisfaction_tr<Ak>(
- tr: &miniscript::descriptor::Tr<DefiniteDescriptorKey>,
- assets: &Assets<Ak>,
-) -> Option<Plan<Ak>>
-where
- Ak: CanDerive + Clone,
-{
- let key_path_spend = assets.keys.iter().find_map(|asset_key| {
- let derivation_hint = asset_key.can_derive(tr.internal_key())?;
- Some((asset_key, derivation_hint))
- });
-
- if let Some((asset_key, derivation_hint)) = key_path_spend {
- return Some(Plan {
- template: vec![TemplateItem::Sign(PlanKey {
- asset_key: asset_key.clone(),
- descriptor_key: tr.internal_key().clone(),
- derivation_hint,
- })],
- target: Target::Segwitv1 {
- tr: tr.clone(),
- tr_plan: TrSpend::KeySpend,
- },
- set_locktime: None,
- set_sequence: None,
- });
- }
-
- let mut plans = tr
- .iter_scripts()
- .filter_map(|(_, ms)| Some((ms, (plan_steps(&ms.node, assets)?))))
- .collect::<Vec<_>>();
-
- plans.sort_by_cached_key(|(_, plan)| plan.expected_size());
-
- let (script, best_plan) = plans.into_iter().next()?;
-
- Some(Plan {
- target: Target::Segwitv1 {
- tr: tr.clone(),
- tr_plan: TrSpend::LeafSpend {
- script: script.encode(),
- leaf_version: LeafVersion::TapScript,
- },
- },
- set_locktime: best_plan.min_locktime.clone(),
- set_sequence: best_plan.min_sequence.clone(),
- template: best_plan.template,
- })
-}
-
-#[derive(Debug)]
-struct TermPlan<Ak> {
- pub min_locktime: Option<absolute::LockTime>,
- pub min_sequence: Option<Sequence>,
- pub template: Vec<TemplateItem<Ak>>,
-}
-
-impl<Ak> TermPlan<Ak> {
- fn new(template: Vec<TemplateItem<Ak>>) -> Self {
- TermPlan {
- template,
- ..Default::default()
- }
- }
-}
-
-impl<Ak> Default for TermPlan<Ak> {
- fn default() -> Self {
- Self {
- min_locktime: Default::default(),
- min_sequence: Default::default(),
- template: Default::default(),
- }
- }
-}
-
-fn plan_steps<Ak: Clone + CanDerive, Ctx: ScriptContext>(
- term: &Terminal<DefiniteDescriptorKey, Ctx>,
- assets: &Assets<Ak>,
-) -> Option<TermPlan<Ak>> {
- match term {
- Terminal::True => Some(TermPlan::new(vec![])),
- Terminal::False => return None,
- Terminal::PkH(key) => {
- let (asset_key, derivation_hint) = assets
- .keys
- .iter()
- .find_map(|asset_key| Some((asset_key, asset_key.can_derive(key)?)))?;
- Some(TermPlan::new(vec![
- TemplateItem::Sign(PlanKey {
- asset_key: asset_key.clone(),
- derivation_hint,
- descriptor_key: key.clone(),
- }),
- TemplateItem::Pk { key: key.clone() },
- ]))
- }
- Terminal::PkK(key) => {
- let (asset_key, derivation_hint) = assets
- .keys
- .iter()
- .find_map(|asset_key| Some((asset_key, asset_key.can_derive(key)?)))?;
- Some(TermPlan::new(vec![TemplateItem::Sign(PlanKey {
- asset_key: asset_key.clone(),
- derivation_hint,
- descriptor_key: key.clone(),
- })]))
- }
- Terminal::RawPkH(_pk_hash) => {
- /* TODO */
- None
- }
- Terminal::After(locktime) => {
- let max_locktime = assets.max_locktime?;
- let locktime = absolute::LockTime::from(*locktime);
- let (height, time) = match max_locktime {
- absolute::LockTime::Blocks(height) => {
- (height, absolute::Time::from_consensus(0).unwrap())
- }
- absolute::LockTime::Seconds(seconds) => (absolute::Height::ZERO, seconds),
- };
- if max_locktime.is_satisfied_by(height, time) {
- Some(TermPlan {
- min_locktime: Some(locktime),
- ..Default::default()
- })
- } else {
- None
- }
- }
- Terminal::Older(older) => {
- // FIXME: older should be a height or time not a sequence.
- let max_sequence = assets.txo_age?;
- //TODO: this whole thing is probably wrong but upstream should provide a way of
- // doing it properly.
- if max_sequence.is_height_locked() == older.is_height_locked() {
- if max_sequence.to_consensus_u32() >= older.to_consensus_u32() {
- Some(TermPlan {
- min_sequence: Some((*older).into()),
- ..Default::default()
- })
- } else {
- None
- }
- } else {
- None
- }
- }
- Terminal::Sha256(image) => {
- if assets.sha256.contains(&image) {
- Some(TermPlan::new(vec![TemplateItem::Sha256(image.clone())]))
- } else {
- None
- }
- }
- Terminal::Hash256(image) => {
- if assets.hash256.contains(image) {
- Some(TermPlan::new(vec![TemplateItem::Hash256(image.clone())]))
- } else {
- None
- }
- }
- Terminal::Ripemd160(image) => {
- if assets.ripemd160.contains(&image) {
- Some(TermPlan::new(vec![TemplateItem::Ripemd160(image.clone())]))
- } else {
- None
- }
- }
- Terminal::Hash160(image) => {
- if assets.hash160.contains(&image) {
- Some(TermPlan::new(vec![TemplateItem::Hash160(image.clone())]))
- } else {
- None
- }
- }
- Terminal::Alt(ms)
- | Terminal::Swap(ms)
- | Terminal::Check(ms)
- | Terminal::Verify(ms)
- | Terminal::NonZero(ms)
- | Terminal::ZeroNotEqual(ms) => plan_steps(&ms.node, assets),
- Terminal::DupIf(ms) => {
- let mut plan = plan_steps(&ms.node, assets)?;
- plan.template.push(TemplateItem::One);
- Some(plan)
- }
- Terminal::AndV(l, r) | Terminal::AndB(l, r) => {
- let lhs = plan_steps(&l.node, assets)?;
- let rhs = plan_steps(&r.node, assets)?;
- lhs.combine(rhs)
- }
- Terminal::AndOr(_, _, _) => todo!(),
- Terminal::OrB(_, _) => todo!(),
- Terminal::OrD(_, _) => todo!(),
- Terminal::OrC(_, _) => todo!(),
- Terminal::OrI(lhs, rhs) => {
- let lplan = plan_steps(&lhs.node, assets).map(|mut plan| {
- plan.template.push(TemplateItem::One);
- plan
- });
- let rplan = plan_steps(&rhs.node, assets).map(|mut plan| {
- plan.template.push(TemplateItem::Zero);
- plan
- });
- match (lplan, rplan) {
- (Some(lplan), Some(rplan)) => {
- if lplan.expected_size() <= rplan.expected_size() {
- Some(lplan)
- } else {
- Some(rplan)
- }
- }
- (lplan, rplan) => lplan.or(rplan),
- }
- }
- Terminal::Thresh(_) => todo!(),
- Terminal::Multi(_) => todo!(),
- Terminal::MultiA(_) => todo!(),
- }
-}
+++ /dev/null
-use bdk_chain::{bitcoin, collections::*, miniscript};
-use core::ops::Deref;
-
-use bitcoin::{
- bip32,
- hashes::{hash160, ripemd160, sha256, Hash},
- key::XOnlyPublicKey,
- secp256k1::{Keypair, Message, PublicKey, Signing, Verification},
- sighash,
- sighash::{EcdsaSighashType, Prevouts, SighashCache, TapSighashType},
- taproot, Transaction, TxOut,
-};
-
-use super::*;
-use miniscript::{
- descriptor::{DescriptorSecretKey, KeyMap},
- hash256,
-};
-
-#[derive(Clone, Debug)]
-/// Signatures and hash pre-images that must be provided to complete the plan.
-pub struct Requirements<Ak> {
- /// required signatures
- pub signatures: RequiredSignatures<Ak>,
- /// required sha256 pre-images
- pub sha256_images: HashSet<sha256::Hash>,
- /// required hash160 pre-images
- pub hash160_images: HashSet<hash160::Hash>,
- /// required hash256 pre-images
- pub hash256_images: HashSet<hash256::Hash>,
- /// required ripemd160 pre-images
- pub ripemd160_images: HashSet<ripemd160::Hash>,
-}
-
-impl<Ak> Default for RequiredSignatures<Ak> {
- fn default() -> Self {
- RequiredSignatures::Legacy {
- keys: Default::default(),
- }
- }
-}
-
-impl<Ak> Default for Requirements<Ak> {
- fn default() -> Self {
- Self {
- signatures: Default::default(),
- sha256_images: Default::default(),
- hash160_images: Default::default(),
- hash256_images: Default::default(),
- ripemd160_images: Default::default(),
- }
- }
-}
-
-impl<Ak> Requirements<Ak> {
- /// Whether any hash pre-images are required in the plan
- pub fn requires_hash_preimages(&self) -> bool {
- !(self.sha256_images.is_empty()
- && self.hash160_images.is_empty()
- && self.hash256_images.is_empty()
- && self.ripemd160_images.is_empty())
- }
-}
-
-/// The signatures required to complete the plan
-#[derive(Clone, Debug)]
-pub enum RequiredSignatures<Ak> {
- /// Legacy ECDSA signatures are required
- Legacy { keys: Vec<PlanKey<Ak>> },
- /// Segwitv0 ECDSA signatures are required
- Segwitv0 { keys: Vec<PlanKey<Ak>> },
- /// A Taproot key spend signature is required
- TapKey {
- /// the internal key
- plan_key: PlanKey<Ak>,
- /// The merkle root of the taproot output
- merkle_root: Option<taproot::TapNodeHash>,
- },
- /// Taproot script path signatures are required
- TapScript {
- /// The leaf hash of the script being used
- leaf_hash: TapLeafHash,
- /// The keys in the script that require signatures
- plan_keys: Vec<PlanKey<Ak>>,
- },
-}
-
-#[derive(Clone, Debug)]
-pub enum SigningError {
- SigHashP2wpkh(sighash::P2wpkhError),
- SigHashTaproot(sighash::TaprootError),
- DerivationError(bip32::Error),
-}
-
-impl From<sighash::TaprootError> for SigningError {
- fn from(v: sighash::TaprootError) -> Self {
- Self::SigHashTaproot(v)
- }
-}
-
-impl From<sighash::P2wpkhError> for SigningError {
- fn from(v: sighash::P2wpkhError) -> Self {
- Self::SigHashP2wpkh(v)
- }
-}
-
-impl core::fmt::Display for SigningError {
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- SigningError::SigHashP2wpkh(e) => e.fmt(f),
- SigningError::SigHashTaproot(e) => e.fmt(f),
- SigningError::DerivationError(e) => e.fmt(f),
- }
- }
-}
-
-impl From<bip32::Error> for SigningError {
- fn from(e: bip32::Error) -> Self {
- Self::DerivationError(e)
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for SigningError {}
-
-impl RequiredSignatures<DescriptorPublicKey> {
- pub fn sign_with_keymap<T: core::borrow::Borrow<Transaction>>(
- &self,
- input_index: usize,
- keymap: &KeyMap,
- prevouts: &Prevouts<'_, impl core::borrow::Borrow<TxOut>>,
- schnorr_sighashty: Option<TapSighashType>,
- _ecdsa_sighashty: Option<EcdsaSighashType>,
- sighash_cache: &mut SighashCache<T>,
- auth_data: &mut SatisfactionMaterial,
- secp: &Secp256k1<impl Signing + Verification>,
- ) -> Result<bool, SigningError> {
- match self {
- RequiredSignatures::Legacy { .. } | RequiredSignatures::Segwitv0 { .. } => todo!(),
- RequiredSignatures::TapKey {
- plan_key,
- merkle_root,
- } => {
- let schnorr_sighashty = schnorr_sighashty.unwrap_or(TapSighashType::Default);
- let sighash = sighash_cache.taproot_key_spend_signature_hash(
- input_index,
- prevouts,
- schnorr_sighashty,
- )?;
- let secret_key = match keymap.get(&plan_key.asset_key) {
- Some(secret_key) => secret_key,
- None => return Ok(false),
- };
- let secret_key = match secret_key {
- DescriptorSecretKey::Single(single) => single.key.inner,
- DescriptorSecretKey::XPrv(xprv) => {
- xprv.xkey
- .derive_priv(&secp, &plan_key.derivation_hint)?
- .private_key
- }
- DescriptorSecretKey::MultiXPrv(_) => {
- // This crate will be replaced by
- // https://github.com/rust-bitcoin/rust-miniscript/pull/481 anyways
- todo!();
- }
- };
-
- let pubkey = PublicKey::from_secret_key(&secp, &secret_key);
- let x_only_pubkey = XOnlyPublicKey::from(pubkey);
-
- let tweak =
- taproot::TapTweakHash::from_key_and_tweak(x_only_pubkey, merkle_root.clone());
- let keypair = Keypair::from_secret_key(&secp, &secret_key.clone())
- .add_xonly_tweak(&secp, &tweak.to_scalar())
- .unwrap();
-
- let msg = Message::from_digest(sighash.to_byte_array());
- let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
-
- let bitcoin_sig = taproot::Signature {
- signature: sig,
- sighash_type: schnorr_sighashty,
- };
-
- auth_data
- .schnorr_sigs
- .insert(plan_key.descriptor_key.clone(), bitcoin_sig);
- Ok(true)
- }
- RequiredSignatures::TapScript {
- leaf_hash,
- plan_keys,
- } => {
- let sighash_type = schnorr_sighashty.unwrap_or(TapSighashType::Default);
- let sighash = sighash_cache.taproot_script_spend_signature_hash(
- input_index,
- prevouts,
- *leaf_hash,
- sighash_type,
- )?;
-
- let mut modified = false;
-
- for plan_key in plan_keys {
- if let Some(secret_key) = keymap.get(&plan_key.asset_key) {
- let secret_key = match secret_key {
- DescriptorSecretKey::Single(single) => single.key.inner,
- DescriptorSecretKey::XPrv(xprv) => {
- xprv.xkey
- .derive_priv(&secp, &plan_key.derivation_hint)?
- .private_key
- }
- DescriptorSecretKey::MultiXPrv(_) => {
- // This crate will be replaced by
- // https://github.com/rust-bitcoin/rust-miniscript/pull/481 anyways
- todo!();
- }
- };
- let keypair = Keypair::from_secret_key(&secp, &secret_key.clone());
- let msg = Message::from_digest(sighash.to_byte_array());
- let signature = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
- let bitcoin_sig = taproot::Signature {
- signature,
- sighash_type,
- };
-
- auth_data
- .schnorr_sigs
- .insert(plan_key.descriptor_key.clone(), bitcoin_sig);
- modified = true;
- }
- }
- Ok(modified)
- }
- }
- }
-}
+++ /dev/null
-use bdk_chain::{bitcoin, miniscript};
-use bitcoin::{
- bip32::DerivationPath,
- hashes::{hash160, ripemd160, sha256},
-};
-
-use super::*;
-use crate::{hash256, varint_len, DefiniteDescriptorKey};
-
-#[derive(Clone, Debug)]
-pub(crate) enum TemplateItem<Ak> {
- Sign(PlanKey<Ak>),
- Pk { key: DefiniteDescriptorKey },
- One,
- Zero,
- Sha256(sha256::Hash),
- Hash256(hash256::Hash),
- Ripemd160(ripemd160::Hash),
- Hash160(hash160::Hash),
-}
-
-/// A plan key contains the asset key originally provided along with key in the descriptor it
-/// purports to be able to derive for along with a "hint" on how to derive it.
-#[derive(Clone, Debug)]
-pub struct PlanKey<Ak> {
- /// The key the planner will sign with
- pub asset_key: Ak,
- /// A hint from how to get from the asset key to the concrete key we need to sign with.
- pub derivation_hint: DerivationPath,
- /// The key that was in the descriptor that we are satisfying with the signature from the asset
- /// key.
- pub descriptor_key: DefiniteDescriptorKey,
-}
-
-impl<Ak> TemplateItem<Ak> {
- pub fn expected_size(&self) -> usize {
- match self {
- TemplateItem::Sign { .. } => 64, /* size of sig TODO: take into consideration sighash flag */
- TemplateItem::Pk { .. } => 32,
- TemplateItem::One => varint_len(1),
- TemplateItem::Zero => 0, /* zero means an empty witness element */
- // I'm not sure if it should be 32 here (it's a 20 byte hash) but that's what other
- // parts of the code were doing.
- TemplateItem::Hash160(_) | TemplateItem::Ripemd160(_) => 32,
- TemplateItem::Sha256(_) | TemplateItem::Hash256(_) => 32,
- }
- }
-
- // this can only be called if we are sure that auth_data has what we need
- pub(super) fn to_witness_stack(&self, auth_data: &SatisfactionMaterial) -> Vec<Vec<u8>> {
- match self {
- TemplateItem::Sign(plan_key) => {
- vec![auth_data
- .schnorr_sigs
- .get(&plan_key.descriptor_key)
- .unwrap()
- .to_vec()]
- }
- TemplateItem::One => vec![vec![1]],
- TemplateItem::Zero => vec![vec![]],
- TemplateItem::Sha256(image) => {
- vec![auth_data.sha256_preimages.get(image).unwrap().to_vec()]
- }
- TemplateItem::Hash160(image) => {
- vec![auth_data.hash160_preimages.get(image).unwrap().to_vec()]
- }
- TemplateItem::Ripemd160(image) => {
- vec![auth_data.ripemd160_preimages.get(image).unwrap().to_vec()]
- }
- TemplateItem::Hash256(image) => {
- vec![auth_data.hash256_preimages.get(image).unwrap().to_vec()]
- }
- TemplateItem::Pk { key } => vec![key.to_public_key().to_bytes()],
- }
- }
-}