diff --git a/Cargo.toml b/Cargo.toml index f9dbbf885..1c29bbaf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,6 @@ members = [ "example-crates/wallet_esplora_blocking", "example-crates/wallet_esplora_async", "example-crates/wallet_rpc", - "nursery/tmp_plan", - "nursery/coin_select" ] [workspace.package] diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 75c658510..d1833b071 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -2,7 +2,7 @@ use std::{ path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, - Arc, Mutex, + Arc, }, time::{Duration, Instant}, }; @@ -12,16 +12,13 @@ use bdk_bitcoind_rpc::{ 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"; @@ -36,11 +33,6 @@ const MEMPOOL_EMIT_DELAY: Duration = Duration::from_secs(30); /// Delay for committing to persistence. const DB_COMMIT_DELAY: Duration = Duration::from_secs(60); -type ChangeSet = ( - local_chain::ChangeSet, - indexed_tx_graph::ChangeSet, -); - #[derive(Debug)] enum Emission { Block(bdk_bitcoind_rpc::BlockEvent), @@ -111,52 +103,26 @@ enum RpcCommands { fn main() -> anyhow::Result<()> { let start = Instant::now(); + let example_cli::Init { args, - keymap, - index, + graph, + chain, db, - init_changeset, - } = example_cli::init::(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::(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)?; @@ -191,7 +157,12 @@ fn main() -> anyhow::Result<()> { .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 { @@ -220,7 +191,7 @@ fn main() -> anyhow::Result<()> { ) }; println!( - "[{:>10}s] synced to {} @ {} | total: {} sats", + "[{:>10}s] synced to {} @ {} | total: {}", start.elapsed().as_secs_f32(), synced_to.hash(), synced_to.height(), @@ -235,7 +206,11 @@ fn main() -> anyhow::Result<()> { ); { 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)?; } @@ -300,7 +275,7 @@ fn main() -> anyhow::Result<()> { 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 @@ -321,7 +296,13 @@ fn main() -> anyhow::Result<()> { 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(); @@ -348,7 +329,7 @@ fn main() -> anyhow::Result<()> { ) }; println!( - "[{:>10}s] synced to {} @ {} / {} | total: {} sats", + "[{:>10}s] synced to {} @ {} / {} | total: {}", start.elapsed().as_secs_f32(), synced_to.hash(), synced_to.height(), diff --git a/example-crates/example_cli/Cargo.toml b/example-crates/example_cli/Cargo.toml index c85d2e996..9b8c4debb 100644 --- a/example-crates/example_cli/Cargo.toml +++ b/example-crates/example_cli/Cargo.toml @@ -7,11 +7,10 @@ edition = "2021" [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" diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 568123d92..ee0c9b376 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -1,62 +1,88 @@ -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 = IndexedTxGraph>; -pub type KeychainChangeSet = ( - local_chain::ChangeSet, - indexed_tx_graph::ChangeSet, -); +pub use anyhow; +pub use clap; + +/// Alias for a `IndexedTxGraph` with specific `Anchor` and `Indexer`. +pub type KeychainTxGraph = IndexedTxGraph>; + +/// ChangeSet +#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ChangeSet { + /// Descriptor for recipient addresses. + pub descriptor: Option>, + /// Descriptor for change addresses. + pub change_descriptor: Option>, + /// Stores the network type of the transaction data. + pub network: Option, + /// Changes to the [`LocalChain`]. + pub local_chain: local_chain::ChangeSet, + /// Changes to [`TxGraph`](tx_graph::TxGraph). + pub tx_graph: tx_graph::ChangeSet, + /// Changes to [`KeychainTxOutIndex`]. + pub indexer: keychain_txout::ChangeSet, +} #[derive(Parser)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] pub struct Args { - #[clap(env = "DESCRIPTOR")] - pub descriptor: String, - #[clap(env = "CHANGE_DESCRIPTOR")] - pub change_descriptor: Option, - - #[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, } #[derive(Subcommand, Debug, Clone)] pub enum Commands { + /// 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, + }, #[clap(flatten)] ChainSpecific(CS), /// Address generation and inspection. @@ -72,70 +98,17 @@ pub enum Commands { #[clap(subcommand)] txout_cmd: TxOutCmd, }, - /// Send coins to an address. - Send { - /// Amount to send in satoshis - value: u64, - /// Destination address - address: Address, - #[clap(short, default_value = "bnb")] - coin_select: CoinSelectionAlgo, - #[clap(flatten)] - chain_specific: S, + /// PSBT operations + Psbt { + #[clap(subcommand)] + psbt_cmd: PsbtCmd, + }, + /// 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 { - 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)] @@ -173,6 +146,48 @@ pub enum TxOutCmd { }, } +#[derive(Subcommand, Debug, Clone)] +pub enum PsbtCmd { + /// Create a new PSBT. + New { + /// Amount to send in satoshis + value: u64, + /// Recipient address + address: Address, + /// Set max absolute timelock (from consensus value) + #[clap(long, short)] + after: Option, + /// Set max relative timelock (from consensus value) + #[clap(long, short)] + older: Option, + /// 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, + /// Private descriptor + #[clap(long, short = 'd')] + descriptor: Option, + }, + /// 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, )] @@ -181,7 +196,7 @@ pub enum Keychain { 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"), @@ -190,242 +205,228 @@ impl core::fmt::Display for Keychain { } } -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 { + 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( - graph: &mut KeychainTxGraph, +pub fn create_tx( + graph: &mut KeychainTxGraph, chain: &O, - keymap: &BTreeMap, + assets: &Assets, cs_algorithm: CoinSelectionAlgo, address: Address, value: u64, -) -> anyhow::Result<(Transaction, Option)> +) -> anyhow::Result<(Psbt, Option)> 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 = 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::>(); + _ => 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::::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::>(); - 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 = (bdk_tmp_plan::Plan, FullTxOut); +// Alias the elements of `planned_utxos` +pub type PlanUtxo = (Plan, FullTxOut); -pub fn planned_utxos( - graph: &KeychainTxGraph, +pub fn planned_utxos( + graph: &KeychainTxGraph, chain: &O, - assets: &bdk_tmp_plan::Assets, -) -> Result>, O::Error> { + assets: &Assets, +) -> Result, 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, _>> { + .filter_map(|r| -> Option> { let (k, i, full_txo) = match r { Err(err) => return Some(Err(err)), Ok(((k, i), full_txo)) => (k, i, full_txo), @@ -438,33 +439,25 @@ pub fn planned_utxos( - graph: &Mutex>, - db: &Mutex>, - chain: &Mutex, - keymap: &BTreeMap, +pub fn handle_commands( + graph: &Mutex, + chain: &Mutex, + db: &Mutex>, network: Network, broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>, cmd: Commands, -) -> anyhow::Result<()> -where - O::Error: std::error::Error + Send + Sync + 'static, - C: Default - + Merge - + DeserializeOwned - + Serialize - + From> - + 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(); @@ -481,12 +474,11 @@ where 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(()) } @@ -604,23 +596,47 @@ where } } } - 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 { @@ -629,119 +645,310 @@ where // 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 = [(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 -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 { + /// CLI args pub args: Args, - /// Descriptor keymap. - pub keymap: KeyMap, - /// Keychain-txout index. - pub index: KeychainTxOutIndex, - /// Persistence backend. - pub db: Mutex>, - /// Initial changeset. - pub init_changeset: C, + /// Indexed graph + pub graph: Mutex, + /// Local chain + pub chain: Mutex, + /// Database + pub db: Mutex>, + /// 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( +/// Loads from persistence or creates new +pub fn init_or_load( db_magic: &[u8], - db_default_path: &str, -) -> anyhow::Result> -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>> { let args = Args::::parse(); - let secp = Secp256k1::default(); - let mut index = KeychainTxOutIndex::::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::::parse_descriptor(&secp, &args.descriptor)?; - let _ = index.insert_descriptor(Keychain::External, descriptor)?; + match args.command { + // initialize new db + Commands::Init { .. } => initialize::(args, db_magic, db_path).map(|_| None), + // generate keys + Commands::Generate { network } => generate_bip86_helper(network).map(|_| None), + // try load + _ => { + let mut db = + Store::::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::::parse_descriptor(&secp, desc_str)) - .transpose()? +/// Initialize db backend. +fn initialize(args: Args, 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::::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::::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::::create_new(db_magic, db_path)?; + db.append_changeset(&changeset)?; + println!("New database {db_path}"); } - let mut db_backend = match Store::::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) -> 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 = [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) = + >::parse_descriptor(&secp, external_desc)?; + let (internal_descriptor, internal_keymap) = + >::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() + } } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 083a4fe46..cda8c5526 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -1,14 +1,9 @@ -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, }; @@ -17,9 +12,10 @@ use bdk_electrum::{ BdkElectrumClient, }; use example_cli::{ + self, anyhow::{self, Context}, clap::{self, Parser, Subcommand}, - Keychain, + ChangeSet, Keychain, }; const DB_MAGIC: &[u8] = b"bdk_example_electrum"; @@ -98,46 +94,28 @@ pub struct ScanOptions { pub batch_size: usize, } -type ChangeSet = ( - local_chain::ChangeSet, - indexed_tx_graph::ChangeSet, -); - fn main() -> anyhow::Result<()> { let example_cli::Init { args, - keymap, - index, + graph, + chain, db, - init_changeset, - } = example_cli::init::(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::(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(()) }, @@ -146,7 +124,7 @@ fn main() -> anyhow::Result<()> { } }; - 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()); @@ -244,7 +222,7 @@ fn main() -> anyhow::Result<()> { 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, ); @@ -345,7 +323,12 @@ fn main() -> anyhow::Result<()> { } 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(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index af6422689..608e58d11 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -1,40 +1,29 @@ 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, -); +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, @@ -73,8 +62,8 @@ impl EsploraCommands { #[derive(clap::Args, Debug, Clone)] pub struct EsploraArgs { - /// The esplora url endpoint to connect to e.g. `` - /// 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, } @@ -103,29 +92,14 @@ pub struct ScanOptions { fn main() -> anyhow::Result<()> { let example_cli::Init { args, - keymap, - index, + graph, + chain, db, - init_changeset, - } = example_cli::init::(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::(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). @@ -134,12 +108,11 @@ fn main() -> anyhow::Result<()> { 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(|_| ()) @@ -150,7 +123,7 @@ fn main() -> anyhow::Result<()> { } }; - 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. // @@ -264,7 +237,7 @@ fn main() -> anyhow::Result<()> { 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, ); @@ -361,6 +334,11 @@ fn main() -> anyhow::Result<()> { // 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(()) } diff --git a/nursery/coin_select/Cargo.toml b/nursery/coin_select/Cargo.toml deleted file mode 100644 index 0830ad93e..000000000 --- a/nursery/coin_select/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "bdk_coin_select" -version = "0.0.1" -authors = [ "LLFourn " ] - -[dependencies] -bdk_chain = { path = "../../crates/chain" } - -[features] -default = ["std"] -std = [] diff --git a/nursery/coin_select/src/bnb.rs b/nursery/coin_select/src/bnb.rs deleted file mode 100644 index d355894ab..000000000 --- a/nursery/coin_select/src/bnb.rs +++ /dev/null @@ -1,645 +0,0 @@ -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); - -/// [`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>; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - - let (strategy, score) = (self.strategy)(&self.state); - - let mut found_best = Option::::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 for BnbLimit { - fn from(v: usize) -> Self { - Self::Rounds(v) - } -} - -#[cfg(feature = "std")] -impl From 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: -/// Bitcoin Core Implementation: -/// -/// 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(limit: L, selector: CoinSelector) -> Option -where - L: Into, -{ - 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::>(); - 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| -> (BranchStrategy, Option) { - 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 { -// 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 = 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 = 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 = 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 = 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 = 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::>(); -// 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() {} -// } diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs deleted file mode 100644 index 729f9b23b..000000000 --- a/nursery/coin_select/src/coin_selector.rs +++ /dev/null @@ -1,617 +0,0 @@ -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, - /// 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, // 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, - selected: BTreeSet, -} - -impl<'a> CoinSelector<'a> { - pub fn candidate(&self, index: usize) -> &WeightedValue { - &self.candidates[index] - } - - pub fn new(candidates: &'a Vec, 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::(); - (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 + '_ { - self.selected - .iter() - .map(move |&index| (index, &self.candidates[index])) - } - - pub fn unselected(&self) -> impl Iterator + '_ { - self.candidates - .iter() - .enumerate() - .filter(move |(index, _)| !self.selected.contains(index)) - } - - pub fn selected_indexes(&self) -> impl Iterator + '_ { - self.selected.iter().cloned() - } - - pub fn unselected_indexes(&self) -> impl Iterator + '_ { - (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 { - let mut selection = self.finish(); - - if selection.is_ok() { - return selection; - } - - let unselected = self.unselected_indexes().collect::>(); - - for index in unselected { - self.select(index); - selection = self.finish(); - - if selection.is_ok() { - break; - } - } - - selection - } - - pub fn finish(&self) -> Result { - 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, - pub excess: u64, - pub excess_strategies: HashMap, -} - -#[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, - pub drain_value: Option, - pub fee: u64, - pub weight: u32, - pub waste: i64, -} - -impl Selection { - pub fn apply_selection<'a, T>( - &'a self, - candidates: &'a [T], - ) -> impl Iterator + '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::>(); - - 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::>(); - - 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() {} -} diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs deleted file mode 100644 index 3b8ae12f9..000000000 --- a/nursery/coin_select/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -#![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 -} diff --git a/nursery/tmp_plan/Cargo.toml b/nursery/tmp_plan/Cargo.toml deleted file mode 100644 index a97e5cb73..000000000 --- a/nursery/tmp_plan/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[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 = [] diff --git a/nursery/tmp_plan/README.md b/nursery/tmp_plan/README.md deleted file mode 100644 index 70cc100dc..000000000 --- a/nursery/tmp_plan/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# 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 diff --git a/nursery/tmp_plan/src/lib.rs b/nursery/tmp_plan/src/lib.rs deleted file mode 100644 index 08c28d846..000000000 --- a/nursery/tmp_plan/src/lib.rs +++ /dev/null @@ -1,441 +0,0 @@ -#![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, - 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 { - template: Vec>, - target: Target, - set_locktime: Option, - set_sequence: Option, -} - -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, - /// ECDSA signatures under their keys - pub ecdsa_sigs: BTreeMap, - /// SHA256 pre-images under their images - pub sha256_preimages: BTreeMap>, - /// hash160 pre-images under their images - pub hash160_preimages: BTreeMap>, - /// hash256 pre-images under their images - pub hash256_preimages: BTreeMap>, - /// ripemd160 pre-images under their images - pub ripemd160_preimages: BTreeMap>, -} - -impl Plan -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> = 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::>(); - - 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::() - } - None => 0, - }; - - script_sig_size * 4 + witness_size - } - - pub fn requirements(&self) -> Requirements { - match self.try_complete(&SatisfactionMaterial::default()) { - PlanState::Complete { .. } => Requirements::default(), - PlanState::Incomplete(requirements) => requirements, - } - } - - pub fn try_complete(&self, auth_data: &SatisfactionMaterial) -> PlanState { - 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::>(); - - if unsatisfied_items.is_empty() { - let mut witness = self - .template - .iter() - .flat_map(|step| step.to_witness_stack(&auth_data)) - .collect::>(); - 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 { - 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 { - self.set_locktime.clone() - } - - /// The minimum required sequence (height or time) on the input to satisfy the plan - pub fn required_sequence(&self) -> Option { - self.set_sequence.clone() - } - - /// The minimum required transaction version required on the transaction using the plan. - pub fn min_version(&self) -> Option { - if let Some(_) = self.set_sequence { - Some(2) - } else { - Some(1) - } - } -} - -/// The returned value from [`Plan::try_complete`]. -pub enum PlanState { - /// The plan is complete - Complete { - /// The script sig that should be set on the input - final_script_sig: Option, - /// The witness that should be set on the input - final_script_witness: Option, - }, - Incomplete(Requirements), -} - -#[derive(Clone, Debug)] -pub struct Assets { - pub keys: Vec, - pub txo_age: Option, - pub max_locktime: Option, - pub sha256: Vec, - pub hash256: Vec, - pub ripemd160: Vec, - pub hash160: Vec, -} - -impl Default for Assets { - 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; -} - -impl CanDerive for KeySource { - fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option { - 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 { - 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 { - 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( - desc: &Descriptor, - assets: &Assets, -) -> Option> -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), - } -} diff --git a/nursery/tmp_plan/src/plan_impls.rs b/nursery/tmp_plan/src/plan_impls.rs deleted file mode 100644 index 8bbcb5ee9..000000000 --- a/nursery/tmp_plan/src/plan_impls.rs +++ /dev/null @@ -1,325 +0,0 @@ -use bdk_chain::{bitcoin, miniscript}; -use bitcoin::locktime::absolute; -use miniscript::Terminal; - -use super::*; - -impl TermPlan { - fn combine(self, other: Self) -> Option { - 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 { -// pub(crate) fn plan_satisfaction(&self, assets: &Assets) -> Option> -// 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 { -// pub(crate) fn plan_satisfaction(&self, assets: &Assets) -> Option> -// 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( - tr: &miniscript::descriptor::Tr, - assets: &Assets, -) -> Option> -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::>(); - - 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 { - pub min_locktime: Option, - pub min_sequence: Option, - pub template: Vec>, -} - -impl TermPlan { - fn new(template: Vec>) -> Self { - TermPlan { - template, - ..Default::default() - } - } -} - -impl Default for TermPlan { - fn default() -> Self { - Self { - min_locktime: Default::default(), - min_sequence: Default::default(), - template: Default::default(), - } - } -} - -fn plan_steps( - term: &Terminal, - assets: &Assets, -) -> Option> { - 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!(), - } -} diff --git a/nursery/tmp_plan/src/requirements.rs b/nursery/tmp_plan/src/requirements.rs deleted file mode 100644 index 0d0097dbf..000000000 --- a/nursery/tmp_plan/src/requirements.rs +++ /dev/null @@ -1,237 +0,0 @@ -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 { - /// required signatures - pub signatures: RequiredSignatures, - /// required sha256 pre-images - pub sha256_images: HashSet, - /// required hash160 pre-images - pub hash160_images: HashSet, - /// required hash256 pre-images - pub hash256_images: HashSet, - /// required ripemd160 pre-images - pub ripemd160_images: HashSet, -} - -impl Default for RequiredSignatures { - fn default() -> Self { - RequiredSignatures::Legacy { - keys: Default::default(), - } - } -} - -impl Default for Requirements { - fn default() -> Self { - Self { - signatures: Default::default(), - sha256_images: Default::default(), - hash160_images: Default::default(), - hash256_images: Default::default(), - ripemd160_images: Default::default(), - } - } -} - -impl Requirements { - /// 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 { - /// Legacy ECDSA signatures are required - Legacy { keys: Vec> }, - /// Segwitv0 ECDSA signatures are required - Segwitv0 { keys: Vec> }, - /// A Taproot key spend signature is required - TapKey { - /// the internal key - plan_key: PlanKey, - /// The merkle root of the taproot output - merkle_root: Option, - }, - /// 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>, - }, -} - -#[derive(Clone, Debug)] -pub enum SigningError { - SigHashP2wpkh(sighash::P2wpkhError), - SigHashTaproot(sighash::TaprootError), - DerivationError(bip32::Error), -} - -impl From for SigningError { - fn from(v: sighash::TaprootError) -> Self { - Self::SigHashTaproot(v) - } -} - -impl From 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 for SigningError { - fn from(e: bip32::Error) -> Self { - Self::DerivationError(e) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SigningError {} - -impl RequiredSignatures { - pub fn sign_with_keymap>( - &self, - input_index: usize, - keymap: &KeyMap, - prevouts: &Prevouts<'_, impl core::borrow::Borrow>, - schnorr_sighashty: Option, - _ecdsa_sighashty: Option, - sighash_cache: &mut SighashCache, - auth_data: &mut SatisfactionMaterial, - secp: &Secp256k1, - ) -> Result { - 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) - } - } - } -} diff --git a/nursery/tmp_plan/src/template.rs b/nursery/tmp_plan/src/template.rs deleted file mode 100644 index 72d514140..000000000 --- a/nursery/tmp_plan/src/template.rs +++ /dev/null @@ -1,76 +0,0 @@ -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 { - Sign(PlanKey), - 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 { - /// 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 TemplateItem { - 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> { - 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()], - } - } -}