diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a873d60..061719a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,14 +14,17 @@ jobs: strategy: matrix: rust: - - toolchain: stable - - toolchain: 1.63.0 + - version: stable + - version: 1.63.0 steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@v1 with: - toolchain: ${{ matrix.rust.toolchain }} + toolchain: ${{ matrix.rust.version }} + - name: Pin dependencies for MSRV + if: matrix.rust.version == '1.63.0' + run: ./ci/pin-msrv.sh - name: Test run: cargo test --no-fail-fast --all-features diff --git a/Cargo.toml b/Cargo.toml index 7444842..9caf3c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,23 @@ readme = "README.md" [dependencies] miniscript = { version = "12", default-features = false } +bdk_coin_select = "0.4.0" [dev-dependencies] anyhow = "1" -bdk_chain = { version = "0.21" } bdk_tx = { path = "." } bitcoin = { version = "0.32", features = ["rand-std"] } +bdk_testenv = "0.11.1" +bdk_bitcoind_rpc = "0.18.0" +bdk_chain = { version = "0.21" } [features] default = ["std"] std = ["miniscript/std"] + +[[example]] +name = "synopsis" + +[[example]] +name = "common" +crate-type = ["lib"] diff --git a/README.md b/README.md index e334c2c..3c4fa94 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# `bdk-tx` +# `bdk_tx` This is a transaction building library based on `rust-miniscript` that lets you build, update, and finalize PSBTs with minimal dependencies. @@ -7,26 +7,9 @@ Because the project builds upon [miniscript] we support [descriptors] natively. Refer to [BIP174], [BIP370], and [BIP371] to learn more about partially signed bitcoin transactions (PSBT). -## Example +**Note:** +The library is unstable and API changes should be expected. Check the [examples] directory for detailed usage examples. -To get started see the `DataProvider` trait and the methods for adding inputs and outputs. - -```rust -use bdk_tx::Builder; -use bdk_tx::DataProvider; - -impl DataProvider for MyType { ... } - -let mut builder = Builder::new(); -builder.add_input(plan_utxo); -builder.add_output(script_pubkey, amount); -let (mut psbt, finalizer) = builder.build_tx(data_provider)?; - -// Your PSBT signing flow... - -let result = finalizer.finalize(&mut psbt)?; -assert!(result.is_finalized()); -``` ## Contributing Found a bug, have an issue or a feature request? Feel free to open an issue on GitHub. This library is open source licensed under MIT. @@ -36,3 +19,4 @@ Found a bug, have an issue or a feature request? Feel free to open an issue on G [BIP174]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki [BIP370]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki [BIP371]: https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki +[examples]: ./examples diff --git a/ci/pin-msrv.sh b/ci/pin-msrv.sh new file mode 100755 index 0000000..ba4d917 --- /dev/null +++ b/ci/pin-msrv.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -x +set -euo pipefail + +# Script to pin dependencies for MSRV + +# cargo clean + +# rm -f Cargo.lock + +# rustup default 1.63.0 + +cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5" +cargo update -p time --precise "0.3.20" +cargo update -p home --precise "0.5.5" +cargo update -p flate2 --precise "1.0.35" +cargo update -p once_cell --precise "1.20.3" +cargo update -p bzip2-sys --precise "0.1.12" +cargo update -p ring --precise "0.17.12" +cargo update -p once_cell --precise "1.20.3" +cargo update -p base64ct --precise "1.6.0" +cargo update -p minreq --precise "2.13.2" diff --git a/examples/common.rs b/examples/common.rs new file mode 100644 index 0000000..27ef00c --- /dev/null +++ b/examples/common.rs @@ -0,0 +1,170 @@ +use std::sync::Arc; + +use bdk_bitcoind_rpc::Emitter; +use bdk_chain::{bdk_core, Anchor, Balance, ChainPosition, ConfirmationBlockTime}; +use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv}; +use bdk_tx::{CanonicalUnspents, Input, InputCandidates, RbfParams, TxStatus, TxWithStatus}; +use bitcoin::{absolute, Address, BlockHash, OutPoint, Transaction, Txid}; +use miniscript::{ + plan::{Assets, Plan}, + Descriptor, DescriptorPublicKey, ForEachKey, +}; + +const EXTERNAL: &str = "external"; +const INTERNAL: &str = "internal"; + +pub struct Wallet { + pub chain: bdk_chain::local_chain::LocalChain, + pub graph: bdk_chain::IndexedTxGraph< + bdk_core::ConfirmationBlockTime, + bdk_chain::keychain_txout::KeychainTxOutIndex<&'static str>, + >, +} + +impl Wallet { + pub fn new( + genesis_hash: BlockHash, + external: Descriptor, + internal: Descriptor, + ) -> anyhow::Result { + let mut indexer = bdk_chain::keychain_txout::KeychainTxOutIndex::default(); + indexer.insert_descriptor(EXTERNAL, external)?; + indexer.insert_descriptor(INTERNAL, internal)?; + let graph = bdk_chain::IndexedTxGraph::new(indexer); + let (chain, _) = bdk_chain::local_chain::LocalChain::from_genesis_hash(genesis_hash); + Ok(Self { chain, graph }) + } + + pub fn sync(&mut self, env: &TestEnv) -> anyhow::Result<()> { + let client = env.rpc_client(); + let last_cp = self.chain.tip(); + let mut emitter = Emitter::new(client, last_cp, 0); + while let Some(event) = emitter.next_block()? { + let _ = self + .graph + .apply_block_relevant(&event.block, event.block_height()); + let _ = self.chain.apply_update(event.checkpoint); + } + let mempool = emitter.mempool()?; + let _ = self.graph.batch_insert_relevant_unconfirmed(mempool); + Ok(()) + } + + pub fn next_address(&mut self) -> Option
{ + let ((_, spk), _) = self.graph.index.next_unused_spk(EXTERNAL)?; + Address::from_script(&spk, bitcoin::consensus::Params::REGTEST).ok() + } + + pub fn balance(&self) -> Balance { + let outpoints = self.graph.index.outpoints().clone(); + self.graph.graph().balance( + &self.chain, + self.chain.tip().block_id(), + outpoints, + |_, _| true, + ) + } + + /// TODO: Add to chain sources. + pub fn tip_info( + &self, + client: &impl RpcApi, + ) -> anyhow::Result<(absolute::Height, absolute::Time)> { + let tip = self.chain.tip().block_id(); + let tip_info = client.get_block_header_info(&tip.hash)?; + let tip_height = absolute::Height::from_consensus(tip.height)?; + let tip_time = + absolute::Time::from_consensus(tip_info.median_time.unwrap_or(tip_info.time) as _)?; + Ok((tip_height, tip_time)) + } + + // TODO: Maybe create an `AssetsBuilder` or `AssetsExt` that makes it easier to add + // assets from descriptors, etc. + pub fn assets(&self) -> Assets { + let index = &self.graph.index; + let tip = self.chain.tip().block_id(); + Assets::new() + .after(absolute::LockTime::from_height(tip.height).expect("must be valid height")) + .add({ + let mut pks = vec![]; + for (_, desc) in index.keychains() { + desc.for_each_key(|k| { + pks.extend(k.clone().into_single_keys()); + true + }); + } + pks + }) + } + + pub fn plan_of_output(&self, outpoint: OutPoint, assets: &Assets) -> Option { + let index = &self.graph.index; + let ((k, i), _txout) = index.txout(outpoint)?; + let desc = index.get_descriptor(k)?.at_derivation_index(i).ok()?; + let plan = desc.plan(assets).ok()?; + Some(plan) + } + + pub fn canonical_txs(&self) -> impl Iterator>> + '_ { + pub fn status_from_position(pos: ChainPosition) -> Option { + match pos { + bdk_chain::ChainPosition::Confirmed { anchor, .. } => Some(TxStatus { + height: absolute::Height::from_consensus( + anchor.confirmation_height_upper_bound(), + ) + .expect("must convert to height"), + time: absolute::Time::from_consensus(anchor.confirmation_time as _) + .expect("must convert from time"), + }), + bdk_chain::ChainPosition::Unconfirmed { .. } => None, + } + } + self.graph + .graph() + .list_canonical_txs(&self.chain, self.chain.tip().block_id()) + .map(|c_tx| (c_tx.tx_node.tx, status_from_position(c_tx.chain_position))) + } + + pub fn all_candidates(&self) -> bdk_tx::InputCandidates { + let index = &self.graph.index; + let assets = self.assets(); + let canon_utxos = CanonicalUnspents::new(self.canonical_txs()); + let can_select = canon_utxos.try_get_unspents( + index + .outpoints() + .iter() + .filter_map(|(_, op)| Some((*op, self.plan_of_output(*op, &assets)?))), + ); + InputCandidates::new([], can_select) + } + + pub fn rbf_candidates( + &self, + replace: impl IntoIterator, + tip_height: absolute::Height, + ) -> anyhow::Result<(bdk_tx::InputCandidates, RbfParams)> { + let index = &self.graph.index; + let assets = self.assets(); + let mut canon_utxos = CanonicalUnspents::new(self.canonical_txs()); + + // Exclude txs that reside-in `rbf_set`. + let rbf_set = canon_utxos.extract_replacements(replace)?; + let must_select = rbf_set + .must_select_largest_input_of_each_original_tx(&canon_utxos)? + .into_iter() + .map(|op| canon_utxos.try_get_unspent(op, self.plan_of_output(op, &assets)?)) + .collect::>>() + .ok_or(anyhow::anyhow!( + "failed to find input of tx we are intending to replace" + ))?; + + let can_select = index.outpoints().iter().filter_map(|(_, op)| { + canon_utxos.try_get_unspent(*op, self.plan_of_output(*op, &assets)?) + }); + Ok(( + InputCandidates::new(must_select, can_select) + .filter(rbf_set.candidate_filter(tip_height)), + rbf_set.selector_rbf_params(), + )) + } +} diff --git a/examples/synopsis.rs b/examples/synopsis.rs new file mode 100644 index 0000000..7a8ba86 --- /dev/null +++ b/examples/synopsis.rs @@ -0,0 +1,181 @@ +use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv}; +use bdk_tx::{ + filter_unspendable_now, group_by_spk, selection_algorithm_lowest_fee_bnb, ChangePolicyType, + Output, PsbtParams, SelectorParams, Signer, +}; +use bitcoin::{key::Secp256k1, Amount, FeeRate, Sequence}; +use miniscript::Descriptor; + +mod common; + +use common::Wallet; + +fn main() -> anyhow::Result<()> { + let secp = Secp256k1::new(); + let (external, external_keymap) = + Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[3])?; + let (internal, internal_keymap) = + Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[4])?; + + let signer = Signer(external_keymap.into_iter().chain(internal_keymap).collect()); + + let env = TestEnv::new()?; + let genesis_hash = env.genesis_hash()?; + env.mine_blocks(101, None)?; + + let mut wallet = Wallet::new(genesis_hash, external, internal.clone())?; + wallet.sync(&env)?; + + let addr = wallet.next_address().expect("must derive address"); + + let txid = env.send(&addr, Amount::ONE_BTC)?; + env.mine_blocks(1, None)?; + wallet.sync(&env)?; + println!("Received {}", txid); + println!("Balance (confirmed): {}", wallet.balance()); + + let txid = env.send(&addr, Amount::ONE_BTC)?; + wallet.sync(&env)?; + println!("Received {txid}"); + println!("Balance (pending): {}", wallet.balance()); + + let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?; + let longterm_feerate = FeeRate::from_sat_per_vb_unchecked(1); + + let recipient_addr = env + .rpc_client() + .get_new_address(None, None)? + .assume_checked(); + + // Okay now create tx. + let selection = wallet + .all_candidates() + .regroup(group_by_spk()) + .filter(filter_unspendable_now(tip_height, tip_time)) + .into_selection( + selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000), + SelectorParams::new( + FeeRate::from_sat_per_vb_unchecked(10), + vec![Output::with_script( + recipient_addr.script_pubkey(), + Amount::from_sat(21_000_000), + )], + internal.at_derivation_index(0)?, + bdk_tx::ChangePolicyType::NoDustAndLeastWaste { longterm_feerate }, + ), + )?; + + let mut psbt = selection.create_psbt(PsbtParams { + fallback_sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + })?; + let finalizer = selection.into_finalizer(); + + let _ = psbt.sign(&signer, &secp); + let res = finalizer.finalize(&mut psbt); + assert!(res.is_finalized()); + + let tx = psbt.extract_tx()?; + assert_eq!(tx.input.len(), 2); + let fee = wallet.graph.graph().calculate_fee(&tx)?; + println!( + "ORIGINAL TX: inputs={}, outputs={}, fee={}, feerate={}", + tx.input.len(), + tx.output.len(), + fee, + ((fee.to_sat() as f32) / (tx.weight().to_vbytes_ceil() as f32)), + ); + + // We will try bump this tx fee. + let txid = env.rpc_client().send_raw_transaction(&tx)?; + println!("tx broadcasted: {}", txid); + wallet.sync(&env)?; + println!("Balance (send tx): {}", wallet.balance()); + + // Try cancel a tx. + // We follow all the rules as specified by + // https://github.com/bitcoin/bitcoin/blob/master/doc/policy/mempool-replacements.md#current-replace-by-fee-policy + println!("OKAY LET's TRY CANCEL {}", txid); + { + let original_tx = wallet + .graph + .graph() + .get_tx_node(txid) + .expect("must find tx"); + assert_eq!(txid, original_tx.txid); + + // We canonicalize first. + // + // This ensures all input candidates are of a consistent UTXO set. + // The canonicalization is modified by excluding the original txs and their + // descendants. This way, the prevouts of the original txs are avaliable for spending + // and we won't end up picking outputs of the original txs. + // + // Additionally, we need to guarantee atleast one prevout of each original tx is picked, + // otherwise we may not actually replace the original txs. The policy used here is to + // choose the largest value prevout of each original tx. + // + // Filters out unconfirmed input candidates unless it was already an input of an + // original tx we are replacing (as mentioned in rule 2 of Bitcoin Core Mempool + // Replacement Policy). + let (rbf_candidates, rbf_params) = wallet.rbf_candidates([txid], tip_height)?; + + let selection = rbf_candidates + // Do coin selection. + .into_selection( + // Coin selection algorithm. + selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000), + SelectorParams { + // This is just a lower-bound feerate. The actual result will be much higher to + // satisfy mempool-replacement policy. + target_feerate: FeeRate::from_sat_per_vb_unchecked(1), + // We cancel the tx by specifying no target outputs. This way, all excess returns + // to our change output (unless if the prevouts picked are so small that it will + // be less wasteful to have no output, however that will not be a valid tx). + // If you only want to fee bump, put the original txs' recipients here. + target_outputs: vec![], + change_descriptor: internal.at_derivation_index(1)?, + change_policy: ChangePolicyType::NoDustAndLeastWaste { longterm_feerate }, + // This ensures that we satisfy mempool-replacement policy rules 4 and 6. + replace: Some(rbf_params), + }, + )?; + + let mut psbt = selection.create_psbt(PsbtParams { + // Not strictly necessary, but it may help us replace the tx faster. + fallback_sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + })?; + println!( + "selected inputs: {:?}", + selection + .inputs + .iter() + .map(|input| input.prev_outpoint()) + .collect::>() + ); + + let finalizer = selection.into_finalizer(); + psbt.sign(&signer, &secp).expect("failed to sign"); + assert!( + finalizer.finalize(&mut psbt).is_finalized(), + "must finalize" + ); + + let tx = psbt.extract_tx()?; + let fee = wallet.graph.graph().calculate_fee(&tx)?; + println!( + "REPLACEMENT TX: inputs={}, outputs={}, fee={}, feerate={}", + tx.input.len(), + tx.output.len(), + fee, + ((fee.to_sat() as f32) / (tx.weight().to_vbytes_ceil() as f32)), + ); + let txid = env.rpc_client().send_raw_transaction(&tx)?; + println!("tx broadcasted: {}", txid); + wallet.sync(&env)?; + println!("Balance (RBF): {}", wallet.balance()); + } + + Ok(()) +} diff --git a/src/builder.rs b/src/builder.rs deleted file mode 100644 index 4a316ca..0000000 --- a/src/builder.rs +++ /dev/null @@ -1,1047 +0,0 @@ -use alloc::vec::Vec; -use core::fmt; - -use bitcoin::{ - absolute, transaction, Amount, FeeRate, OutPoint, Psbt, ScriptBuf, Sequence, SignedAmount, - Transaction, TxIn, TxOut, Weight, -}; -use miniscript::{bitcoin, plan::Plan}; - -use crate::{DataProvider, Finalizer, PsbtUpdater, UpdatePsbtError}; - -/// A UTXO with spend plan -#[derive(Debug, Clone)] -pub struct PlanUtxo { - /// plan - pub plan: Plan, - /// outpoint - pub outpoint: OutPoint, - /// txout - pub txout: TxOut, -} - -/// An output in the transaction, includes a txout and whether the output should be -/// treated as change. -#[derive(Debug, Clone)] -struct Output { - txout: TxOut, - is_change: bool, -} - -impl Output { - /// Create a new output - fn new(script: ScriptBuf, amount: Amount) -> Self { - Self::from((script, amount)) - } - - /// Create a new change output - fn new_change(script: ScriptBuf, amount: Amount) -> Self { - let mut output = Self::new(script, amount); - output.is_change = true; - output - } -} - -impl Default for Output { - fn default() -> Self { - Self { - txout: TxOut { - script_pubkey: ScriptBuf::default(), - value: Amount::default(), - }, - is_change: false, - } - } -} - -impl From<(ScriptBuf, Amount)> for Output { - fn from(tup: (ScriptBuf, Amount)) -> Self { - Self { - txout: TxOut { - script_pubkey: tup.0, - value: tup.1, - }, - ..Default::default() - } - } -} - -/// Transaction builder -#[derive(Debug, Clone, Default)] -pub struct Builder { - utxos: Vec, - outputs: Vec, - version: Option, - locktime: Option, - - sequence: Option, - check_fee: CheckFee, -} - -impl Builder { - /// New - pub fn new() -> Self { - Self::default() - } - - /// Add outputs to the transaction. - /// - /// This should be used for setting outgoing scripts and amounts. If adding a change output, - /// use [`Builder::add_change_output`] instead. - pub fn add_outputs( - &mut self, - outputs: impl IntoIterator, - ) -> &mut Self { - self.outputs.extend(outputs.into_iter().map(Output::from)); - self - } - - /// Add an output with the given `script` and `amount` to the transaction. - /// - /// See also [`add_outputs`](Self::add_outputs). - pub fn add_output(&mut self, script: ScriptBuf, amount: Amount) -> &mut Self { - self.add_outputs([(script, amount)]); - self - } - - /// Get the target amounts based on the weight and value of all outputs not including change. - /// - /// This is a convenience method used for passing target values to a coin selection - /// implementation. - pub fn target_outputs(&self) -> impl Iterator + '_ { - self.outputs - .iter() - .filter(|out| !out.is_change) - .cloned() - .map(|out| (out.txout.weight(), out.txout.value)) - } - - /// Add a change output. - /// - /// This should only be used for adding a change output. See [`Builder::add_output`] for - /// adding an outgoing output. Note that only one output may be designated as change, which - /// means only the last call to this method will apply to the transaction. - /// - /// Note: if combined with [`Builder::check_fee`], the given amount may be adjusted to - /// meet the desired transaction fee. - pub fn add_change_output(&mut self, script: ScriptBuf, amount: Amount) -> &mut Self { - if self.is_change_added() { - let out = self - .outputs - .iter_mut() - .find(|out| out.is_change) - .expect("must have change output"); - out.txout = TxOut { - script_pubkey: script, - value: amount, - }; - } else { - self.outputs.push(Output::new_change(script, amount)); - } - self - } - - /// Add an input to fund the tx - pub fn add_input(&mut self, utxo: impl Into) -> &mut Self { - self.utxos.push(utxo.into()); - self - } - - /// Add inputs to be used to fund the tx - pub fn add_inputs(&mut self, utxos: I) -> &mut Self - where - I: IntoIterator, - I::Item: Into, - { - self.utxos.extend(utxos.into_iter().map(Into::into)); - self - } - - /// Whether a change output has been added to this [`Builder`] - fn is_change_added(&self) -> bool { - self.outputs.iter().any(|out| out.is_change) - } - - /// Target a given fee / feerate of the transaction. - /// - /// If change is added, this allows making an adjustment to the value of the change - /// output to meet the given fee and/or feerate. By default we target a minimum - /// feerate of 1 sat/vbyte. - /// - /// Note: this option may be ignored if meeting the specified fee or feerate would - /// consume the entire amount of the change. - pub fn check_fee(&mut self, fee: Option, feerate: Option) { - let mut check = CheckFee::default(); - if let Some(fee) = fee { - check.fee = fee; - } - if let Some(feerate) = feerate { - check.feerate = feerate; - } - self.check_fee = check; - } - - /// Use a specific [`transaction::Version`] - pub fn version(&mut self, version: transaction::Version) -> &mut Self { - self.version = Some(version); - self - } - - /// Use a specific transaction [`LockTime`](absolute::LockTime). - /// - /// Note that building a transaction may raise an error if the given locktime has a - /// different lock type than that of a planned input. The greatest locktime value - /// among all of the spend plans is what goes into the final tx, so this value - /// may be ignored if it doesn't increase the overall maximum. - pub fn locktime(&mut self, locktime: absolute::LockTime) -> &mut Self { - self.locktime = Some(locktime); - self - } - - /// Set a default [`Sequence`] for all inputs. Note that building the tx may raise an - /// error if the given `sequence` is incompatible with the relative locktime of a - /// planned input. - pub fn sequence(&mut self, sequence: Sequence) -> &mut Self { - self.sequence = Some(sequence); - self - } - - /// Add a data-carrying output using `OP_RETURN`. - /// - /// # Errors - /// - /// - If `data` exceeds 80 bytes in size. - /// - If this is not the first `OP_RETURN` output being added to this builder. - /// - /// Refer to for more - /// details about transaction standardness. - pub fn add_data(&mut self, data: T) -> Result<&mut Self, Error> - where - T: AsRef<[u8]>, - { - if self - .outputs - .iter() - .any(|out| out.txout.script_pubkey.is_op_return()) - { - return Err(Error::TooManyOpReturn); - } - if data.as_ref().len() > 80 { - return Err(Error::MaxOpReturnRelay); - } - - let mut bytes = bitcoin::script::PushBytesBuf::new(); - bytes.extend_from_slice(data.as_ref()).expect("should push"); - - self.outputs - .push(Output::new(ScriptBuf::new_op_return(bytes), Amount::ZERO)); - - Ok(self) - } - - /// Build a PSBT with the given data provider and return a [`PsbtUpdater`]. - /// - /// # Errors - /// - /// - If attempting to mix locktime units - /// - If the tx is illegally constructed or fails one of a number of sanity checks - /// defined by the library. - /// - If a requested locktime or sequence interferes with the locktime constraints - /// of a planned input. - pub fn build_psbt(self, provider: &mut D) -> Result - where - D: DataProvider, - { - use absolute::LockTime; - - let version = self.version.unwrap_or(transaction::Version::TWO); - - // accumulate the max required locktime - let mut lock_time: Option = self.utxos.iter().try_fold(None, |acc, u| match u - .plan - .absolute_timelock - { - None => Ok(acc), - Some(lock) => match acc { - None => Ok(Some(lock)), - Some(acc) => { - if !lock.is_same_unit(acc) { - Err(Error::LockTypeMismatch) - } else if acc.is_implied_by(lock) { - Ok(Some(lock)) - } else { - Ok(Some(acc)) - } - } - }, - })?; - - if let Some(param) = self.locktime { - match lock_time { - Some(lt) => { - if !lt.is_same_unit(param) { - return Err(Error::LockTypeMismatch); - } - if param.to_consensus_u32() < lt.to_consensus_u32() { - return Err(Error::LockTimeCltv { - requested: param, - required: lt, - }); - } - if lt.is_implied_by(param) { - lock_time = Some(param); - } - } - None => lock_time = Some(param), - } - } - - let lock_time = lock_time.unwrap_or(LockTime::ZERO); - - let input = self - .utxos - .iter() - .map(|PlanUtxo { plan, outpoint, .. }| { - Ok(TxIn { - previous_output: *outpoint, - sequence: match (self.sequence, plan.relative_timelock) { - (Some(requested), Some(lt)) => { - let required = lt.to_sequence(); - if !check_nsequence(requested, required) { - return Err(Error::SequenceCsv { - requested, - required, - }); - } - requested - } - (None, Some(lt)) => lt.to_sequence(), - (Some(seq), None) => seq, - (None, None) => Sequence::ENABLE_RBF_NO_LOCKTIME, - }, - ..Default::default() - }) - }) - .collect::, Error>>()?; - - let output = self - .outputs - .iter() - .cloned() - .map(|out| out.txout) - .collect::>(); - - let mut unsigned_tx = Transaction { - version, - lock_time, - input, - output, - }; - - // check, validate - self.sanity_check()?; - - if self.is_change_added() { - self.do_check_fee(&mut unsigned_tx); - } - - provider.sort_transaction(&mut unsigned_tx); - - Ok(PsbtUpdater::new(unsigned_tx, self.utxos)?) - } - - /// Convenience method to build an updated [`Psbt`] and return a [`Finalizer`]. - /// Refer to [`build_psbt`](Self::build_psbt) for more. - /// - /// # Errors - /// - /// This method returns an error if a problem occurs when either building or updating - /// the PSBT. - pub fn build_tx(self, provider: &mut D) -> Result<(Psbt, Finalizer), Error> - where - D: DataProvider, - { - let mut updater = self.build_psbt(provider)?; - updater - .update_psbt(provider, crate::UpdateOptions::default()) - .map_err(Error::Update)?; - Ok(updater.into_finalizer()) - } - - /// Sanity checks the tx for - /// - /// - Negative fee - /// - Absurd fee: The absurd fee threshold is currently 2x the sum of the outputs - // - // TODO: check total amounts, max tx weight, is standard spk - // - vin/vout not empty - fn sanity_check(&self) -> Result<(), Error> { - let total_in: Amount = self.utxos.iter().map(|p| p.txout.value).sum(); - let total_out: Amount = self.outputs.iter().map(|out| out.txout.value).sum(); - if total_out > total_in { - return Err(Error::NegativeFee(SignedAmount::from_sat( - total_in.to_sat() as i64 - total_out.to_sat() as i64, - ))); - } - let weight = self.estimate_weight(); - if total_in > total_out * 2 { - let fee = total_in - total_out; - let feerate = fee / weight; - return Err(Error::InsaneFee(feerate)); - } - - Ok(()) - } - - /// This will shift the allocation of funds from the change output to the - /// transaction fee in two cases: - /// - /// - if the computed feerate of tx is below a target feerate - /// - if the computed fee of tx is below a target fee amount - /// - /// We have to set an amount by which the change output is allowed to shrink - /// and still be positive. This will be the value of the change output minus - /// some amount of dust (546). - /// - /// If the target fee or feerate cannot be met without shrinking the change output - /// to below the dust limit, then no shrinking will occur. - /// - /// Panics if `tx` is not a sane tx - fn do_check_fee(&self, tx: &mut Transaction) { - const DUST: u64 = 546; - if !self.is_change_added() { - return; - } - let CheckFee { - fee: exp_fee, - feerate: exp_feerate, - } = self.check_fee; - - // We use these units in the below calculation: - // fee: u64 satoshi - // weight: u64 wu - // feerate: f32 satoshi per 1000 wu - let fee = self.fee_amount(tx).expect("must be sane tx").to_sat(); - let weight = self.estimate_weight().to_wu(); - let feerate = 1000.0 * fee as f32 / weight as f32; - - let txout = self - .outputs - .iter() - .find(|out| out.is_change) - .map(|out| out.txout.clone()) - .expect("must have change output"); - let (output_index, _) = tx - .output - .iter() - .enumerate() - .find(|(_, txo)| **txo == txout) - .expect("must have txout"); - - // check feerate - if feerate < exp_feerate.to_sat_per_kwu() as f32 { - let exp_feerate = exp_feerate.to_sat_per_kwu() as f32; - let exp_fee = (exp_feerate * (weight as f32 / 1000.0)) as u64; - let delta = exp_fee.saturating_sub(fee); - - let txout = &mut tx.output[output_index]; - if txout.value.to_sat() >= delta + DUST { - txout.value -= Amount::from_sat(delta); - } - } - - // check fee - let fee = self.fee_amount(tx).expect("must be sane tx"); - if fee < exp_fee { - let delta = exp_fee - fee; - let txout = &mut tx.output[output_index]; - if txout.value >= delta + Amount::from_sat(DUST) { - txout.value -= delta; - } - } - } - - /// Get an estimate of the current tx weight - pub fn estimate_weight(&self) -> Weight { - Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: (0..self.utxos.len()).map(|_| TxIn::default()).collect(), - output: self.outputs.iter().cloned().map(|out| out.txout).collect(), - } - .weight() - + self - .utxos - .iter() - .map(|p| Weight::from_wu_usize(p.plan.satisfaction_weight())) - .sum() - } - - /// Returns the tx fee as the sum of the inputs minus the sum of the outputs - /// returning `None` on overflowing subtraction. - fn fee_amount(&self, tx: &Transaction) -> Option { - self.utxos - .iter() - .map(|p| p.txout.value) - .sum::() - .checked_sub(tx.output.iter().map(|txo| txo.value).sum::()) - } -} - -/// Checks that the given `sequence` is compatible with `csv`. To be compatible, both -/// must enable relative locktime, have the same lock type unit, and the requested -/// sequence must be at least the value of `csv`. -fn check_nsequence(sequence: Sequence, csv: Sequence) -> bool { - debug_assert!( - csv.is_relative_lock_time(), - "csv must enable relative locktime" - ); - if !sequence.is_relative_lock_time() { - return false; - } - if sequence.is_height_locked() != csv.is_height_locked() { - return false; - } - if sequence < csv { - return false; - } - - true -} - -/// Check fee -#[derive(Debug, Copy, Clone)] -struct CheckFee { - fee: Amount, - feerate: FeeRate, -} - -impl Default for CheckFee { - fn default() -> Self { - Self { - feerate: FeeRate::from_sat_per_vb_unchecked(1), - fee: Amount::default(), - } - } -} - -/// [`Builder`] error -#[derive(Debug)] -pub enum Error { - /// insane feerate - InsaneFee(FeeRate), - /// requested locktime is incompatible with required CLTV - LockTimeCltv { - /// requested locktime - requested: absolute::LockTime, - /// required locktime - required: absolute::LockTime, - }, - /// attempted to mix locktime types - LockTypeMismatch, - /// output exceeds data carrier limit - MaxOpReturnRelay, - /// negative fee - NegativeFee(SignedAmount), - /// bitcoin psbt error - Psbt(bitcoin::psbt::Error), - /// requested sequence is incompatible with requirement - SequenceCsv { - /// requested sequence - requested: Sequence, - /// required sequence - required: Sequence, - }, - /// too many `OP_RETURN` in a single tx - TooManyOpReturn, - /// error when updating a PSBT - Update(UpdatePsbtError), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InsaneFee(r) => write!(f, "absurd feerate: {r:#}"), - Self::LockTimeCltv { - requested, - required, - } => write!( - f, - "requested locktime {requested} must be at least {required}" - ), - Self::LockTypeMismatch => write!(f, "cannot mix locktime units"), - Self::MaxOpReturnRelay => write!(f, "non-standard: output exceeds data carrier limit"), - Self::NegativeFee(e) => write!(f, "illegal tx: negative fee: {}", e.display_dynamic()), - Self::Psbt(e) => e.fmt(f), - Self::SequenceCsv { - requested, - required, - } => write!(f, "{requested} is incompatible with required {required}"), - Self::TooManyOpReturn => write!(f, "non-standard: only 1 OP_RETURN output permitted"), - Self::Update(e) => e.fmt(f), - } - } -} - -impl From for Error { - fn from(e: bitcoin::psbt::Error) -> Self { - Self::Psbt(e) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for Error {} - -#[cfg(test)] -mod test { - use super::*; - use crate::Signer; - use alloc::string::String; - - use bitcoin::{ - secp256k1::{self, Secp256k1}, - Txid, - }; - use miniscript::{ - descriptor::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, KeyMap}, - plan::Assets, - ForEachKey, - }; - - use bdk_chain::{ - bdk_core, keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph, - TxGraph, - }; - use bdk_core::{CheckPoint, ConfirmationBlockTime}; - - const XPRV: &str = "tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L"; - const WIF: &str = "cU6BxEezV8FnkEPBCaFtc4WNuUKmgFaAu6sJErB154GXgMUjhgWe"; - const SPK: &str = "00143f027073e6f341c481f55b7baae81dda5e6a9fba"; - - fn get_single_sig_tr_xprv() -> Vec { - (0..2) - .map(|i| format!("tr({XPRV}/86h/1h/0h/{i}/*)")) - .collect() - } - - fn get_single_sig_cltv_timestamp() -> String { - format!("wsh(and_v(v:pk({WIF}),after(1735877503)))") - } - - type KeychainTxGraph = IndexedTxGraph>; - - #[derive(Debug)] - struct TestProvider { - assets: Assets, - signer: Signer, - secp: Secp256k1, - chain: LocalChain, - graph: KeychainTxGraph, - } - - impl DataProvider for TestProvider { - fn get_tx(&self, txid: Txid) -> Option { - self.graph - .graph() - .get_tx(txid) - .map(|tx| tx.as_ref().clone()) - } - - fn get_descriptor_for_txout( - &self, - txout: &TxOut, - ) -> Option> { - let indexer = &self.graph.index; - - let (keychain, index) = indexer.index_of_spk(txout.script_pubkey.clone())?; - let desc = indexer.get_descriptor(*keychain)?; - - desc.at_derivation_index(*index).ok() - } - } - - impl TestProvider { - /// Set max absolute timelock - fn after(mut self, lt: absolute::LockTime) -> Self { - self.assets = self.assets.after(lt); - self - } - - /// Get a reference to the tx graph - fn graph(&self) -> &TxGraph { - self.graph.graph() - } - - /// Get a reference to the indexer - fn index(&self) -> &KeychainTxOutIndex { - &self.graph.index - } - - /// Get the script pubkey at the specified `index` from the first keychain - /// (by Ord). - fn spk_at_index(&self, index: u32) -> Option { - let keychain = self.graph.index.keychains().next().unwrap().0; - self.graph.index.spk_at_index(keychain, index) - } - - /// Get next unused internal script pubkey - fn next_internal_spk(&mut self) -> ScriptBuf { - let keychain = self.graph.index.keychains().last().unwrap().0; - let ((_, spk), _) = self.graph.index.next_unused_spk(keychain).unwrap(); - spk - } - - /// Get balance - fn balance(&self) -> bdk_chain::Balance { - let chain = &self.chain; - let chain_tip = chain.tip().block_id(); - - let outpoints = self.graph.index.outpoints().clone(); - let graph = self.graph.graph(); - graph.balance(chain, chain_tip, outpoints, |_, _| true) - } - - /// Get a list of planned utxos sorted largest first - fn planned_utxos(&self) -> Vec { - let chain = &self.chain; - let chain_tip = chain.tip().block_id(); - let op = self.index().outpoints().clone(); - - let mut utxos = vec![]; - - for (indexed, txo) in self.graph().filter_chain_unspents(chain, chain_tip, op) { - let (keychain, index) = indexed; - let desc = self.index().get_descriptor(keychain).unwrap(); - let def = desc.at_derivation_index(index).unwrap(); - if let Ok(plan) = def.plan(&self.assets) { - utxos.push(PlanUtxo { - plan, - outpoint: txo.outpoint, - txout: txo.txout, - }); - } - } - - utxos.sort_by_key(|p| p.txout.value); - utxos.reverse(); - - utxos - } - - /// Attempt to create all the required signatures for this psbt - fn sign(&self, psbt: &mut Psbt) { - let _ = psbt.sign(&self.signer, &self.secp); - } - } - - macro_rules! block_id { - ( $height:expr, $hash:expr ) => { - bdk_chain::BlockId { - height: $height, - hash: $hash, - } - }; - } - - fn new_tx(lt: u32) -> Transaction { - Transaction { - version: transaction::Version(2), - lock_time: absolute::LockTime::from_consensus(lt), - input: vec![TxIn::default()], - output: vec![], - } - } - - fn parse_descriptor(s: &str) -> (Descriptor, KeyMap) { - >::parse_descriptor(&Secp256k1::new(), s).unwrap() - } - - /// Initialize a [`TestProvider`] with the given `descriptors`. - /// - /// The returned object contains a local chain at height 1000 and an indexed tx graph - /// with 10 x 1Msat utxos. - fn init_graph(descriptors: &[String]) -> TestProvider { - use bitcoin::{constants, hashes::Hash, Network}; - - let mut keys = vec![]; - let mut keymap = KeyMap::new(); - - let mut index = KeychainTxOutIndex::new(10); - for (keychain, desc_str) in descriptors.iter().enumerate() { - let (desc, km) = parse_descriptor(desc_str); - desc.for_each_key(|k| { - keys.push(k.clone()); - true - }); - keymap.extend(km); - index.insert_descriptor(keychain, desc).unwrap(); - } - - let mut graph = KeychainTxGraph::new(index); - - let genesis_hash = constants::genesis_block(Network::Regtest).block_hash(); - let mut cp = CheckPoint::new(block_id!(0, genesis_hash)); - - for height in 1..11 { - let ((_, script_pubkey), _) = graph.index.reveal_next_spk(0).unwrap(); - - let tx = Transaction { - output: vec![TxOut { - value: Amount::from_btc(0.01).unwrap(), - script_pubkey, - }], - ..new_tx(height) - }; - let txid = tx.compute_txid(); - let _ = graph.insert_tx(tx); - - let block_id = block_id!(height, Hash::hash(height.to_be_bytes().as_slice())); - let anchor = ConfirmationBlockTime { - block_id, - confirmation_time: height as u64, - }; - let _ = graph.insert_anchor(txid, anchor); - - cp = cp.insert(block_id); - } - - let tip = block_id!(1000, Hash::hash(b"Z")); - cp = cp.insert(tip); - let chain = LocalChain::from_tip(cp).unwrap(); - - let assets = Assets::new().add(keys); - - TestProvider { - assets, - signer: Signer(keymap), - secp: Secp256k1::new(), - chain, - graph, - } - } - - #[test] - fn test_build_tx_finalize() { - let mut graph = init_graph(&get_single_sig_tr_xprv()); - assert_eq!(graph.balance().total().to_btc(), 0.1); - - let recip = ScriptBuf::from_hex(SPK).unwrap(); - let mut builder = Builder::new(); - builder.add_output(recip, Amount::from_sat(2_500_000)); - - let selection = graph.planned_utxos().into_iter().take(3); - builder.add_inputs(selection); - builder.add_change_output(graph.next_internal_spk(), Amount::from_sat(499_500)); - - let (mut psbt, finalizer) = builder.build_tx(&mut graph).unwrap(); - assert_eq!(psbt.unsigned_tx.input.len(), 3); - assert_eq!(psbt.unsigned_tx.output.len(), 2); - - graph.sign(&mut psbt); - assert!(finalizer.finalize(&mut psbt).is_finalized()); - } - - #[test] - fn test_build_tx_insane_fee() { - let mut graph = init_graph(&get_single_sig_tr_xprv()); - - let recip = ScriptBuf::from_hex(SPK).unwrap(); - let mut builder = Builder::new(); - builder.add_output(recip, Amount::from_btc(0.01).unwrap()); - - let selection = graph - .planned_utxos() - .into_iter() - .take(3) - .collect::>(); - assert_eq!( - selection - .iter() - .map(|p| p.txout.value) - .sum::() - .to_btc(), - 0.03 - ); - builder.add_inputs(selection); - - let err = builder.build_tx(&mut graph).unwrap_err(); - assert!(matches!(err, Error::InsaneFee(..))); - } - - #[test] - fn test_build_tx_negative_fee() { - let mut graph = init_graph(&get_single_sig_tr_xprv()); - - let recip = ScriptBuf::from_hex(SPK).unwrap(); - - let mut builder = Builder::new(); - builder.add_output(recip, Amount::from_btc(0.02).unwrap()); - builder.add_inputs(graph.planned_utxos().into_iter().take(1)); - - let err = builder.build_tx(&mut graph).unwrap_err(); - assert!(matches!(err, Error::NegativeFee(..))); - } - - #[test] - fn test_build_tx_add_data() { - let mut graph = init_graph(&get_single_sig_tr_xprv()); - - let mut builder = Builder::new(); - builder.add_inputs(graph.planned_utxos().into_iter().take(1)); - builder.add_output(graph.next_internal_spk(), Amount::from_sat(999_000)); - builder.add_data(b"satoshi nakamoto").unwrap(); - - let psbt = builder.build_tx(&mut graph).unwrap().0; - assert!(psbt - .unsigned_tx - .output - .iter() - .any(|txo| txo.script_pubkey.is_op_return())); - - // try to add more than 80 bytes of data - let data = [0x90; 81]; - builder = Builder::new(); - assert!(matches!( - builder.add_data(data), - Err(Error::MaxOpReturnRelay) - )); - - // try to add more than 1 op return - let data = [0x90; 80]; - builder = Builder::new(); - builder.add_data(data).unwrap(); - assert!(matches!( - builder.add_data(data), - Err(Error::TooManyOpReturn) - )); - } - - #[test] - fn test_build_tx_version() { - use transaction::Version; - let mut graph = init_graph(&get_single_sig_tr_xprv()); - - // test default tx version (2) - let mut builder = Builder::new(); - let recip = graph.spk_at_index(0).unwrap(); - let utxo = graph.planned_utxos().first().unwrap().clone(); - let amt = utxo.txout.value - Amount::from_sat(256); - builder.add_input(utxo.clone()); - builder.add_output(recip.clone(), amt); - - let psbt = builder.build_tx(&mut graph).unwrap().0; - assert_eq!(psbt.unsigned_tx.version, Version::TWO); - - // allow any potentially non-standard version - builder = Builder::new(); - builder.version(Version(3)); - builder.add_input(utxo); - builder.add_output(recip, amt); - - let psbt = builder.build_tx(&mut graph).unwrap().0; - assert_eq!(psbt.unsigned_tx.version, Version(3)); - } - - #[test] - fn test_timestamp_timelock() { - #[derive(Clone)] - struct InOut { - input: PlanUtxo, - output: (ScriptBuf, Amount), - } - fn check_locktime(graph: &mut TestProvider, in_out: InOut, lt: u32, exp_lt: Option) { - let InOut { - input, - output: (recip, amount), - } = in_out; - - let mut builder = Builder::new(); - builder.add_output(recip, amount); - builder.add_input(input); - builder.locktime(absolute::LockTime::from_consensus(lt)); - - let res = builder.build_tx(graph); - - match res { - Ok((mut psbt, finalizer)) => { - assert_eq!( - psbt.unsigned_tx.lock_time.to_consensus_u32(), - exp_lt.unwrap() - ); - graph.sign(&mut psbt); - assert!(finalizer.finalize(&mut psbt).is_finalized()); - } - Err(e) => { - assert!(exp_lt.is_none()); - if absolute::LockTime::from_consensus(lt).is_block_height() { - assert!(matches!(e, Error::LockTypeMismatch)); - } else if lt < 1735877503 { - assert!(matches!(e, Error::LockTimeCltv { .. })); - } - } - } - } - - // initial state - let mut graph = init_graph(&[get_single_sig_cltv_timestamp()]); - let mut t = 1735877503; - let locktime = absolute::LockTime::from_consensus(t); - - // supply the assets needed to create plans - graph = graph.after(locktime); - - let in_out = InOut { - input: graph.planned_utxos().first().unwrap().clone(), - output: (ScriptBuf::from_hex(SPK).unwrap(), Amount::from_sat(999_000)), - }; - - // Test: tx should use the planned locktime - check_locktime(&mut graph, in_out.clone(), t, Some(t)); - - // Test: requesting a lower timelock should error - check_locktime( - &mut graph, - in_out.clone(), - absolute::LOCK_TIME_THRESHOLD, - None, - ); - - // Test: tx may use a custom locktime - t += 1; - check_locktime(&mut graph, in_out.clone(), t, Some(t)); - - // Test: error if lock type mismatch - check_locktime(&mut graph, in_out, 100, None); - } - - #[test] - fn test_build_zero_fee_tx() { - let mut graph = init_graph(&get_single_sig_tr_xprv()); - - let recip = ScriptBuf::from_hex(SPK).unwrap(); - let utxos = graph.planned_utxos(); - - // case: 1-in/1-out - let mut builder = Builder::new(); - builder.add_inputs(utxos.iter().take(1).cloned()); - builder.add_output(recip.clone(), Amount::from_sat(1_000_000)); - let psbt = builder.build_tx(&mut graph).unwrap().0; - assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!(psbt.unsigned_tx.output[0].value.to_btc(), 0.01); - - // case: 1-in/2-out - let mut builder = Builder::new(); - builder.add_inputs(utxos.iter().take(1).cloned()); - builder.add_output(recip, Amount::from_sat(500_000)); - builder.add_change_output(graph.next_internal_spk(), Amount::from_sat(500_000)); - builder.check_fee(Some(Amount::ZERO), Some(FeeRate::from_sat_per_kwu(0))); - - let psbt = builder.build_tx(&mut graph).unwrap().0; - assert_eq!(psbt.unsigned_tx.output.len(), 2); - assert!(psbt - .unsigned_tx - .output - .iter() - .all(|txo| txo.value.to_sat() == 500_000)); - } -} diff --git a/src/canonical_unspents.rs b/src/canonical_unspents.rs new file mode 100644 index 0000000..7fe59e8 --- /dev/null +++ b/src/canonical_unspents.rs @@ -0,0 +1,280 @@ +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::fmt; + +use bitcoin::{psbt, OutPoint, Sequence, Transaction, TxOut, Txid}; +use miniscript::{bitcoin, plan::Plan}; + +use crate::{ + collections::HashMap, input::CoinbaseMismatch, FromPsbtInputError, Input, RbfSet, TxStatus, +}; + +/// Tx with confirmation status. +pub type TxWithStatus = (T, Option); + +/// Our canonical view of unspent outputs. +#[derive(Debug, Clone)] +pub struct CanonicalUnspents { + txs: HashMap>, + statuses: HashMap, + spends: HashMap, +} + +impl CanonicalUnspents { + /// Construct [`CanonicalUnspents`] from an iterator of txs with confirmation status. + pub fn new(canonical_txs: impl IntoIterator>) -> Self + where + T: Into>, + { + let mut txs = HashMap::new(); + let mut statuses = HashMap::new(); + let mut spends = HashMap::new(); + for (tx, status) in canonical_txs { + let tx: Arc = tx.into(); + let txid = tx.compute_txid(); + spends.extend(tx.input.iter().map(|txin| (txin.previous_output, txid))); + txs.insert(txid, tx); + if let Some(status) = status { + statuses.insert(txid, status); + } + } + Self { + txs, + statuses, + spends, + } + } + + /// Extract txs in the set of `replace` from the canonical view of unspents. + /// + /// Returns the [`RbfSet`] if the replacements are valid and succesfully extracted. + /// Errors if the replacements cannot be extracted (e.g. due to missing data). + pub fn extract_replacements( + &mut self, + replace: impl IntoIterator, + ) -> Result { + let mut rbf_txs = replace + .into_iter() + .map(|txid| -> Result<(Txid, Arc), _> { + let tx = self + .txs + .get(&txid) + .cloned() + .ok_or(ExtractReplacementsError::TransactionNotFound(txid))?; + if tx.is_coinbase() { + return Err(ExtractReplacementsError::CannotReplaceCoinbase); + } + Ok((tx.compute_txid(), tx)) + }) + .collect::, _>>()?; + + // Remove txs in this set which have ancestors of other members of this set. + let mut to_remove_from_rbf_txs = Vec::::new(); + let mut to_remove_stack = rbf_txs + .iter() + .map(|(txid, tx)| (*txid, tx.clone())) + .collect::>(); + while let Some((txid, tx)) = to_remove_stack.pop() { + if to_remove_from_rbf_txs.contains(&txid) { + continue; + } + for vout in 0..tx.output.len() as u32 { + let op = OutPoint::new(txid, vout); + if let Some(next_txid) = self.spends.get(&op) { + if let Some(next_tx) = self.txs.get(next_txid) { + to_remove_from_rbf_txs.push(*next_txid); + to_remove_stack.push((*next_txid, next_tx.clone())); + } + } + } + } + for txid in &to_remove_from_rbf_txs { + rbf_txs.remove(txid); + } + + // Find prev outputs of all txs in the set. + // Fail when a prev output is not found. We need to use the prevouts to determine fee for RBF! + let prev_txouts = rbf_txs + .values() + .flat_map(|tx| &tx.input) + .map(|txin| txin.previous_output) + .map(|op| -> Result<(OutPoint, TxOut), _> { + let txout = self + .txs + .get(&op.txid) + .and_then(|tx| tx.output.get(op.vout as usize)) + .cloned() + .ok_or(ExtractReplacementsError::PreviousOutputNotFound(op))?; + Ok((op, txout)) + }) + .collect::, _>>()?; + + // Remove rbf txs (and their descendants) from canonical unspents. + let to_remove_from_canonical_unspents = rbf_txs.keys().chain(&to_remove_from_rbf_txs); + for txid in to_remove_from_canonical_unspents { + if let Some(tx) = self.txs.remove(txid) { + self.statuses.remove(txid); + for txin in &tx.input { + self.spends.remove(&txin.previous_output); + } + } + } + + Ok( + RbfSet::new(rbf_txs.into_values(), prev_txouts) + .expect("must not have missing prevouts"), + ) + } + + /// Whether outpoint is a leaf (unspent). + pub fn is_unspent(&self, outpoint: OutPoint) -> bool { + if self.spends.contains_key(&outpoint) { + return false; + } + match self.txs.get(&outpoint.txid) { + Some(tx) => { + let vout: usize = outpoint.vout.try_into().expect("vout must fit into usize"); + vout < tx.output.len() + } + None => false, + } + } + + /// Try get leaf (unspent) of given `outpoint`. + pub fn try_get_unspent(&self, outpoint: OutPoint, plan: Plan) -> Option { + if self.spends.contains_key(&outpoint) { + return None; + } + let prev_tx = Arc::clone(self.txs.get(&outpoint.txid)?); + Input::from_prev_tx( + plan, + prev_tx, + outpoint.vout.try_into().expect("vout must fit into usize"), + self.statuses.get(&outpoint.txid).cloned(), + ) + .ok() + } + + /// Try get leaves of given `outpoints`. + pub fn try_get_unspents<'a, O>(&'a self, outpoints: O) -> impl Iterator + 'a + where + O: IntoIterator, + O::IntoIter: 'a, + { + outpoints + .into_iter() + .filter_map(|(op, plan)| self.try_get_unspent(op, plan)) + } + + /// Try get foreign leaf (unspent). + pub fn try_get_foreign_unspent( + &self, + outpoint: OutPoint, + sequence: Sequence, + psbt_input: psbt::Input, + satisfaction_weight: usize, + is_coinbase: bool, + ) -> Result { + if !self.is_unspent(outpoint) { + return Err(GetForeignUnspentError::OutputIsAlreadySpent(outpoint)); + } + if let Some(prev_tx) = self.txs.get(&outpoint.txid) { + let non_witness_utxo = psbt_input.non_witness_utxo.as_ref(); + if non_witness_utxo.is_some() && non_witness_utxo != Some(prev_tx) { + return Err(GetForeignUnspentError::UtxoMismatch(outpoint)); + } + let witness_utxo = psbt_input.witness_utxo.as_ref(); + if witness_utxo.is_some() + && psbt_input.witness_utxo.as_ref() != prev_tx.output.get(outpoint.vout as usize) + { + return Err(GetForeignUnspentError::UtxoMismatch(outpoint)); + } + if is_coinbase != prev_tx.is_coinbase() { + return Err(GetForeignUnspentError::Coinbase(CoinbaseMismatch { + txid: outpoint.txid, + expected: is_coinbase, + got: prev_tx.is_coinbase(), + })); + } + } + let status = self.statuses.get(&outpoint.txid).cloned(); + Input::from_psbt_input( + outpoint, + sequence, + psbt_input, + satisfaction_weight, + status, + is_coinbase, + ) + .map_err(GetForeignUnspentError::FromPsbtInput) + } + + /// Try get foreign leaves (unspent). + pub fn try_get_foreign_unspents<'a, O>( + &'a self, + outpoints: O, + ) -> impl Iterator> + 'a + where + O: IntoIterator, + O::IntoIter: 'a, + { + outpoints + .into_iter() + .map(|(op, seq, input, sat_wu, is_coinbase)| { + self.try_get_foreign_unspent(op, seq, input, sat_wu, is_coinbase) + }) + } +} + +/// Canonical unspents error +#[derive(Debug)] +pub enum GetForeignUnspentError { + /// Invalid parameter for `is_coinbase` + Coinbase(CoinbaseMismatch), + /// Error creating an input from a PSBT input + FromPsbtInput(FromPsbtInputError), + /// Cannot get unspent input from output that is already spent + OutputIsAlreadySpent(OutPoint), + /// The witness or non-witness UTXO in the PSBT input does not match the expected outpoint + UtxoMismatch(OutPoint), +} + +impl fmt::Display for GetForeignUnspentError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Coinbase(err) => write!(f, "{}", err), + Self::FromPsbtInput(err) => write!(f, "{}", err), + Self::OutputIsAlreadySpent(op) => { + write!(f, "outpoint is already spent: {}", op) + } + Self::UtxoMismatch(op) => write!(f, "UTXO mismatch: {}", op), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for GetForeignUnspentError {} + +/// Error when attempting to do [`extract_replacements`](CanonicalUnspents::extract_replacements). +#[derive(Debug)] +pub enum ExtractReplacementsError { + /// Transaction not found in canonical unspents + TransactionNotFound(Txid), + /// Cannot replace a coinbase transaction + CannotReplaceCoinbase, + /// Previous output not found for input + PreviousOutputNotFound(OutPoint), +} + +impl fmt::Display for ExtractReplacementsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TransactionNotFound(txid) => write!(f, "transaction not found: {}", txid), + Self::CannotReplaceCoinbase => write!(f, "cannot replace a coinbase transaction"), + Self::PreviousOutputNotFound(op) => write!(f, "previous output not found: {}", op), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ExtractReplacementsError {} diff --git a/src/finalizer.rs b/src/finalizer.rs new file mode 100644 index 0000000..5ffbd61 --- /dev/null +++ b/src/finalizer.rs @@ -0,0 +1,123 @@ +use crate::collections::{BTreeMap, HashMap}; +use bitcoin::{OutPoint, Psbt, Witness}; +use miniscript::{bitcoin, plan::Plan, psbt::PsbtInputSatisfier}; + +/// Finalizer +#[derive(Debug)] +pub struct Finalizer { + pub(crate) plans: HashMap, +} + +impl Finalizer { + /// Create. + pub fn new(plans: impl IntoIterator) -> Self { + Self { + plans: plans.into_iter().collect(), + } + } + + /// Finalize a PSBT input and return whether finalization was successful or input was already + /// finalized. + /// + /// # Errors + /// + /// If the spending plan associated with the PSBT input cannot be satisfied, + /// then a [`miniscript::Error`] is returned. + /// + /// # Panics + /// + /// - If `input_index` is outside the bounds of the PSBT input vector. + pub fn finalize_input( + &self, + psbt: &mut Psbt, + input_index: usize, + ) -> Result { + // return true if already finalized. + { + let psbt_input = &psbt.inputs[input_index]; + if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { + return Ok(true); + } + } + + let mut finalized = false; + let outpoint = psbt + .unsigned_tx + .input + .get(input_index) + .expect("index out of range") + .previous_output; + if let Some(plan) = self.plans.get(&outpoint) { + let stfr = PsbtInputSatisfier::new(psbt, input_index); + let (stack, script) = plan.satisfy(&stfr)?; + // clearing all fields and setting back the utxo, final scriptsig and witness + let original = core::mem::take(&mut psbt.inputs[input_index]); + let psbt_input = &mut psbt.inputs[input_index]; + psbt_input.non_witness_utxo = original.non_witness_utxo; + psbt_input.witness_utxo = original.witness_utxo; + if !script.is_empty() { + psbt_input.final_script_sig = Some(script); + } + if !stack.is_empty() { + psbt_input.final_script_witness = Some(Witness::from_slice(&stack)); + } + finalized = true; + } + + Ok(finalized) + } + + /// Attempt to finalize all of the inputs. + /// + /// This method returns a [`FinalizeMap`] that contains the result of finalization + /// for each input. + pub fn finalize(&self, psbt: &mut Psbt) -> FinalizeMap { + let mut finalized = true; + let mut result = FinalizeMap(BTreeMap::new()); + + for input_index in 0..psbt.inputs.len() { + let psbt_input = &psbt.inputs[input_index]; + if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { + continue; + } + match self.finalize_input(psbt, input_index) { + Ok(is_final) => { + if finalized && !is_final { + finalized = false; + } + result.0.insert(input_index, Ok(is_final)); + } + Err(e) => { + result.0.insert(input_index, Err(e)); + } + } + } + + // clear psbt outputs + if finalized { + for psbt_output in &mut psbt.outputs { + psbt_output.bip32_derivation.clear(); + psbt_output.tap_key_origins.clear(); + psbt_output.tap_internal_key.take(); + } + } + + result + } +} + +/// Holds the results of finalization +#[derive(Debug)] +pub struct FinalizeMap(BTreeMap>); + +impl FinalizeMap { + /// Whether all inputs were finalized + pub fn is_finalized(&self) -> bool { + self.0.values().all(|res| matches!(res, Ok(true))) + } + + /// Get the results as a map of `input_index` to `finalize_input` result. + pub fn results(self) -> BTreeMap> { + self.0 + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..0e19e72 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,556 @@ +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::fmt; + +use bitcoin::constants::COINBASE_MATURITY; +use bitcoin::transaction::OutputsIndexError; +use bitcoin::{absolute, psbt, relative, Amount, Sequence, Txid}; +use miniscript::bitcoin; +use miniscript::bitcoin::{OutPoint, Transaction, TxOut}; +use miniscript::plan::Plan; + +/// Confirmation status of a tx data. +#[derive(Debug, Clone, Copy)] +pub struct TxStatus { + /// Confirmation block height. + pub height: absolute::Height, + /// Confirmation block median time past. + /// + /// TODO: Currently BDK cannot fetch MTP time. We can pretend that the latest block time is the + /// MTP time for now. + pub time: absolute::Time, +} + +impl TxStatus { + /// From consensus `height` and `time`. + pub fn new(height: u32, time: u64) -> Result { + Ok(Self { + height: absolute::Height::from_consensus(height)?, + // TODO: handle `.try_into::()` + time: absolute::Time::from_consensus(time as _)?, + }) + } +} + +#[derive(Debug, Clone)] +enum PlanOrPsbtInput { + Plan(Plan), + PsbtInput { + psbt_input: psbt::Input, + sequence: Sequence, + absolute_timelock: absolute::LockTime, + satisfaction_weight: usize, + }, +} + +impl PlanOrPsbtInput { + /// From [`psbt::Input`]. + /// + /// Errors if neither the witness- or non-witness UTXO are present in `psbt_input`. + fn from_psbt_input( + sequence: Sequence, + psbt_input: psbt::Input, + satisfaction_weight: usize, + ) -> Result { + // We require at least one of the witness or non-witness utxo + if psbt_input.witness_utxo.is_none() && psbt_input.non_witness_utxo.is_none() { + return Err(FromPsbtInputError::UtxoCheck); + } + Ok(Self::PsbtInput { + psbt_input, + sequence, + absolute_timelock: absolute::LockTime::ZERO, + satisfaction_weight, + }) + } + + pub fn plan(&self) -> Option<&Plan> { + match self { + PlanOrPsbtInput::Plan(plan) => Some(plan), + _ => None, + } + } + + pub fn psbt_input(&self) -> Option<&bitcoin::psbt::Input> { + match self { + PlanOrPsbtInput::PsbtInput { psbt_input, .. } => Some(psbt_input), + _ => None, + } + } + + pub fn absolute_timelock(&self) -> Option { + match self { + PlanOrPsbtInput::Plan(plan) => plan.absolute_timelock, + PlanOrPsbtInput::PsbtInput { + absolute_timelock, .. + } => Some(*absolute_timelock), + } + } + + pub fn relative_timelock(&self) -> Option { + match self { + PlanOrPsbtInput::Plan(plan) => plan.relative_timelock, + PlanOrPsbtInput::PsbtInput { sequence, .. } => sequence.to_relative_lock_time(), + } + } + + pub fn sequence(&self) -> Option { + match self { + PlanOrPsbtInput::Plan(plan) => plan.relative_timelock.map(|rtl| rtl.to_sequence()), + PlanOrPsbtInput::PsbtInput { sequence, .. } => Some(*sequence), + } + } + + pub fn satisfaction_weight(&self) -> usize { + match self { + PlanOrPsbtInput::Plan(plan) => plan.satisfaction_weight(), + PlanOrPsbtInput::PsbtInput { + satisfaction_weight, + .. + } => *satisfaction_weight, + } + } + + pub fn is_segwit(&self) -> bool { + match self { + PlanOrPsbtInput::Plan(plan) => plan.witness_version().is_some(), + PlanOrPsbtInput::PsbtInput { psbt_input, .. } => { + psbt_input.final_script_witness.is_some() + } + } + } + + pub fn tx(&self) -> Option<&Transaction> { + match self { + PlanOrPsbtInput::Plan(_) => None, + PlanOrPsbtInput::PsbtInput { psbt_input, .. } => psbt_input.non_witness_utxo.as_ref(), + } + } +} + +/// Mismatch between the expected and actual value of [`Transaction::is_coinbase`]. +#[derive(Debug, Clone)] +pub struct CoinbaseMismatch { + /// txid + pub txid: Txid, + /// expected value of whether a tx is coinbase + pub expected: bool, + /// whether the actual tx is coinbase + pub got: bool, +} + +impl fmt::Display for CoinbaseMismatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "invalid coinbase parameter for txid {}; expected `is_coinbase`: {}, found: {}", + self.txid, self.expected, self.got + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CoinbaseMismatch {} + +/// Error creating [`Input`] from a PSBT input +#[derive(Debug, Clone)] +pub enum FromPsbtInputError { + /// Invalid `is_coinbase` parameter + Coinbase(CoinbaseMismatch), + /// Invalid outpoint + InvalidOutPoint(OutPoint), + /// The input's UTXO is missing or invalid + UtxoCheck, +} + +impl fmt::Display for FromPsbtInputError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Coinbase(err) => write!(f, "{}", err), + Self::InvalidOutPoint(op) => { + write!(f, "invalid outpoint: {}", op) + } + Self::UtxoCheck => { + write!( + f, + "one of the witness or non-witness utxo is missing or invalid" + ) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for FromPsbtInputError {} + +/// Single-input plan. +#[derive(Debug, Clone)] +pub struct Input { + prev_outpoint: OutPoint, + prev_txout: TxOut, + prev_tx: Option>, + plan: PlanOrPsbtInput, + status: Option, + is_coinbase: bool, +} + +impl Input { + /// Create [`Input`] from a previous transaction. + /// + /// # Errors + /// + /// Returns `OutputsIndexError` if the previous txout is not found in `prev_tx` + /// at `output_index`. + pub fn from_prev_tx( + plan: Plan, + prev_tx: T, + output_index: usize, + status: Option, + ) -> Result + where + T: Into>, + { + let tx: Arc = prev_tx.into(); + let is_coinbase = tx.is_coinbase(); + Ok(Self { + prev_outpoint: OutPoint::new(tx.compute_txid(), output_index as _), + prev_txout: tx.tx_out(output_index).cloned()?, + prev_tx: Some(tx), + plan: PlanOrPsbtInput::Plan(plan), + status, + is_coinbase, + }) + } + + /// Create [`Input`] from a previous txout and plan. + pub fn from_prev_txout( + plan: Plan, + prev_outpoint: OutPoint, + prev_txout: TxOut, + status: Option, + is_coinbase: bool, + ) -> Self { + Self { + prev_outpoint, + prev_txout, + prev_tx: None, + plan: PlanOrPsbtInput::Plan(plan), + status, + is_coinbase, + } + } + + /// Create [`Input`] from a [`psbt::Input`]. + /// + /// # Errors + /// + /// - If neither the witness or non-witness utxo are present in `psbt_input`. + /// - If `prev_outpoint` doesn't agree with the previous transaction. + /// - If the previous transaction is known but doesn't match the provided `is_coinbase` + /// parameter. + pub fn from_psbt_input( + prev_outpoint: OutPoint, + sequence: Sequence, + psbt_input: psbt::Input, + satisfaction_weight: usize, + status: Option, + is_coinbase: bool, + ) -> Result { + let outpoint = prev_outpoint; + let prev_txout = match ( + psbt_input.non_witness_utxo.as_ref(), + psbt_input.witness_utxo.as_ref(), + ) { + (Some(prev_tx), witness_utxo) => { + // The outpoint must be valid + if prev_tx.compute_txid() != outpoint.txid { + return Err(FromPsbtInputError::InvalidOutPoint(outpoint)); + } + let prev_txout = prev_tx + .output + .get(outpoint.vout as usize) + .cloned() + .ok_or(FromPsbtInputError::InvalidOutPoint(outpoint))?; + // In case the witness-utxo is present, the txout must match + if let Some(txout) = witness_utxo { + if txout != &prev_txout { + return Err(FromPsbtInputError::UtxoCheck); + } + } + // The value of `is_coinbase` must match that of the previous tx + if is_coinbase != prev_tx.is_coinbase() { + return Err(FromPsbtInputError::Coinbase(CoinbaseMismatch { + txid: outpoint.txid, + expected: is_coinbase, + got: prev_tx.is_coinbase(), + })); + } + prev_txout + } + (_, Some(txout)) => txout.clone(), + _ => return Err(FromPsbtInputError::UtxoCheck), + }; + let prev_tx = psbt_input.non_witness_utxo.clone().map(Arc::new); + let plan = PlanOrPsbtInput::from_psbt_input(sequence, psbt_input, satisfaction_weight)?; + Ok(Self { + prev_outpoint, + prev_txout, + prev_tx, + plan, + status, + is_coinbase, + }) + } + + /// Plan + pub fn plan(&self) -> Option<&Plan> { + self.plan.plan() + } + + /// Psbt input + pub fn psbt_input(&self) -> Option<&bitcoin::psbt::Input> { + self.plan.psbt_input() + } + + /// Previous outpoint. + pub fn prev_outpoint(&self) -> OutPoint { + self.prev_outpoint + } + + /// Previous txout. + pub fn prev_txout(&self) -> &TxOut { + &self.prev_txout + } + + /// Previous tx (if any). + pub fn prev_tx(&self) -> Option<&Transaction> { + self.prev_tx + .as_ref() + .map(|tx| tx.as_ref()) + .or(self.plan.tx()) + } + + /// Confirmation status. + pub fn status(&self) -> Option { + self.status + } + + /// Whether prev output resides in coinbase. + pub fn is_coinbase(&self) -> bool { + self.is_coinbase + } + + /// Whether prev output is an immature coinbase output and cannot be spent in the next block. + pub fn is_immature(&self, tip_height: absolute::Height) -> bool { + if !self.is_coinbase { + return false; + } + match self.status { + Some(status) => { + let age = tip_height + .to_consensus_u32() + .saturating_sub(status.height.to_consensus_u32()); + age + 1 < COINBASE_MATURITY + } + None => { + debug_assert!(false, "coinbase should never be unconfirmed"); + true + } + } + } + + /// Whether the output is still locked by timelock constraints and cannot be spent in the + /// next block. + pub fn is_timelocked(&self, tip_height: absolute::Height, tip_time: absolute::Time) -> bool { + if let Some(locktime) = self.plan.absolute_timelock() { + if !locktime.is_satisfied_by(tip_height, tip_time) { + return true; + } + } + if let Some(locktime) = self.plan.relative_timelock() { + // TODO: Make sure this logic is right. + let (relative_height, relative_time) = match self.status { + Some(status) => { + let relative_height = tip_height + .to_consensus_u32() + .saturating_sub(status.height.to_consensus_u32()); + let relative_time = tip_time + .to_consensus_u32() + .saturating_sub(status.time.to_consensus_u32()); + ( + relative::Height::from_height( + relative_height.try_into().unwrap_or(u16::MAX), + ), + relative::Time::from_seconds_floor(relative_time) + .unwrap_or(relative::Time::MAX), + ) + } + None => (relative::Height::ZERO, relative::Time::ZERO), + }; + if !locktime.is_satisfied_by(relative_height, relative_time) { + return true; + } + } + false + } + + /// Confirmations of this tx. + pub fn confirmations(&self, tip_height: absolute::Height) -> u32 { + self.status.map_or(0, |status| { + tip_height + .to_consensus_u32() + .saturating_sub(status.height.to_consensus_u32().saturating_sub(1)) + }) + } + + /// Whether this output can be spent now. + pub fn is_spendable_now(&self, tip_height: absolute::Height, tip_time: absolute::Time) -> bool { + !self.is_immature(tip_height) && !self.is_timelocked(tip_height, tip_time) + } + + /// Absolute timelock. + pub fn absolute_timelock(&self) -> Option { + self.plan.absolute_timelock() + } + + /// Relative timelock. + pub fn relative_timelock(&self) -> Option { + self.plan.relative_timelock() + } + + /// Sequence value. + pub fn sequence(&self) -> Option { + self.plan.sequence() + } + + /// The weight in witness units needed for satisfying the [`Input`]. + /// + /// The satisfaction weight is the combined size of the fully satisfied input's witness + /// and scriptSig expressed in weight units. See . + pub fn satisfaction_weight(&self) -> u64 { + self.plan + .satisfaction_weight() + .try_into() + .expect("usize must fit into u64") + } + + /// Is segwit. + pub fn is_segwit(&self) -> bool { + self.plan.is_segwit() + } +} + +/// Input group. Cannot be empty. +#[derive(Debug, Clone)] +pub struct InputGroup(Vec); + +impl From for InputGroup { + fn from(input: Input) -> Self { + Self::from_input(input) + } +} + +impl InputGroup { + /// From a single input. + pub fn from_input(input: impl Into) -> Self { + Self(vec![input.into()]) + } + + /// This return `None` to avoid creating empty input groups. + pub fn from_inputs(inputs: impl IntoIterator>) -> Option { + let group = inputs.into_iter().map(Into::into).collect::>(); + if group.is_empty() { + None + } else { + Some(Self(group)) + } + } + + /// Reference to the inputs of this group. + pub fn inputs(&self) -> &Vec { + &self.0 + } + + /// Consume the input group and return all inputs. + pub fn into_inputs(self) -> Vec { + self.0 + } + + /// Push input in group. + pub fn push(&mut self, input: Input) { + self.0.push(input); + } + + /// Whether any contained inputs are immature. + pub fn is_immature(&self, tip_height: absolute::Height) -> bool { + self.0.iter().any(|input| input.is_immature(tip_height)) + } + + /// Whether any contained inputs are time locked. + pub fn is_timelocked(&self, tip_height: absolute::Height, tip_time: absolute::Time) -> bool { + self.0 + .iter() + .any(|input| input.is_timelocked(tip_height, tip_time)) + } + + /// Whether all contained inputs are spendable now. + pub fn is_spendable_now(&self, tip_height: absolute::Height, tip_time: absolute::Time) -> bool { + self.0 + .iter() + .all(|input| input.is_spendable_now(tip_height, tip_time)) + } + + /// Returns the tx confirmation count this is the smallest in this group. + pub fn min_confirmations(&self, tip_height: absolute::Height) -> u32 { + self.inputs() + .iter() + .map(|input| input.confirmations(tip_height)) + .min() + .expect("group must not be empty") + } + + /// Whether any contained input satisfies the predicate. + pub fn any(&self, f: F) -> bool + where + F: FnMut(&Input) -> bool, + { + self.inputs().iter().any(f) + } + + /// Whether all of the contained inputs satisfies the predicate. + pub fn all(&self, f: F) -> bool + where + F: FnMut(&Input) -> bool, + { + self.inputs().iter().all(f) + } + + /// Total value of all contained inputs. + pub fn value(&self) -> Amount { + self.inputs() + .iter() + .map(|input| input.prev_txout.value) + .sum() + } + + /// Total weight of all contained inputs (excluding input count varint). + pub fn weight(&self) -> u64 { + /// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4) and 1 byte for the scriptSig + /// length. + pub const TXIN_BASE_WEIGHT: u64 = (32 + 4 + 4 + 1) * 4; + self.inputs() + .iter() + .map(|input| TXIN_BASE_WEIGHT + input.satisfaction_weight()) + .sum() + } + + /// Input count. + pub fn input_count(&self) -> usize { + self.inputs().len() + } + + /// Whether any contained input is a segwit spend. + pub fn is_segwit(&self) -> bool { + self.inputs().iter().any(|input| input.is_segwit()) + } +} diff --git a/src/input_candidates.rs b/src/input_candidates.rs new file mode 100644 index 0000000..6603f1e --- /dev/null +++ b/src/input_candidates.rs @@ -0,0 +1,328 @@ +use alloc::vec::Vec; +use core::fmt; +use core::ops::Deref; + +use bdk_coin_select::{metrics::LowestFee, Candidate, NoBnbSolution}; +use bitcoin::{absolute, FeeRate, OutPoint}; +use miniscript::bitcoin; + +use crate::collections::{BTreeMap, HashSet}; +use crate::{ + cs_feerate, CannotMeetTarget, Input, InputGroup, Selection, Selector, SelectorError, + SelectorParams, +}; + +/// Input candidates. +#[must_use] +#[derive(Debug, Clone)] +pub struct InputCandidates { + contains: HashSet, + must_select: Option, + can_select: Vec, + cs_candidates: Vec, +} + +fn cs_candidate_from_group(group: &InputGroup) -> Candidate { + Candidate { + value: group.value().to_sat(), + weight: group.weight(), + input_count: group.input_count(), + is_segwit: group.is_segwit(), + } +} + +impl InputCandidates { + /// Construct [`InputCandidates`] with a list of inputs that must be selected as well as + /// those that may additionally be selected. If the same outpoint occurs in both `must_select` and + /// `can_select`, the one in `must_select` is retained. + pub fn new(must_select: A, can_select: B) -> Self + where + A: IntoIterator, + B: IntoIterator, + { + let mut contains = HashSet::::new(); + let must_select = InputGroup::from_inputs( + must_select + .into_iter() + .filter(|input| contains.insert(input.prev_outpoint())), + ); + let can_select = can_select + .into_iter() + .filter(|input| contains.insert(input.prev_outpoint())) + .map(InputGroup::from_input) + .collect::>(); + let cs_candidates = Self::build_cs_candidates(&must_select, &can_select); + InputCandidates { + contains, + must_select, + can_select, + cs_candidates, + } + } + + fn build_cs_candidates( + must_select: &Option, + can_select: &[InputGroup], + ) -> Vec { + must_select + .iter() + .chain(can_select) + .map(cs_candidate_from_group) + .collect() + } + + /// Iterate over all contained inputs of all groups. + pub fn inputs(&self) -> impl Iterator + '_ { + self.groups().flat_map(InputGroup::inputs) + } + + /// Consume and iterate over all conatined inputs of all groups. + pub fn into_inputs(self) -> impl Iterator { + self.into_groups().flat_map(InputGroup::into_inputs) + } + + /// Iterate over all contained groups. + pub fn groups(&self) -> impl Iterator + '_ { + self.must_select.iter().chain(&self.can_select) + } + + /// Consume and iterate over all contained groups. + pub fn into_groups(self) -> impl Iterator { + self.must_select.into_iter().chain(self.can_select) + } + + /// Can select + pub fn can_select(&self) -> &[InputGroup] { + &self.can_select + } + + /// Must select + pub fn must_select(&self) -> Option<&InputGroup> { + self.must_select.as_ref() + } + + /// cs candidates + pub fn coin_select_candidates(&self) -> &Vec { + &self.cs_candidates + } + + /// Whether the outpoint is an input candidate. + pub fn contains(&self, outpoint: OutPoint) -> bool { + self.contains.contains(&outpoint) + } + + /// Regroup inputs with given `policy`. + /// + /// Anything grouped with `must_select` inputs also becomes `must_select`. + pub fn regroup(self, mut policy: P) -> Self + where + P: FnMut(&Input) -> G, + G: Ord + Clone, + { + let mut order = Vec::::with_capacity(self.contains.len()); + let mut groups = BTreeMap::>::new(); + for input in self + .can_select + .into_iter() + .flat_map(InputGroup::into_inputs) + { + let group_id = policy(&input); + use crate::collections::btree_map::Entry; + let entry = match groups.entry(group_id.clone()) { + Entry::Vacant(entry) => { + order.push(group_id.clone()); + entry.insert(vec![]) + } + Entry::Occupied(entry) => entry.into_mut(), + }; + entry.push(input); + } + + let mut must_select = self.must_select.map_or(vec![], |g| g.into_inputs()); + let must_select_order = must_select.iter().map(&mut policy).collect::>(); + for g_id in must_select_order { + if let Some(inputs) = groups.remove(&g_id) { + must_select.extend(inputs); + } + } + let must_select = InputGroup::from_inputs(must_select); + + let mut can_select = Vec::::new(); + for g_id in order { + if let Some(inputs) = groups.remove(&g_id) { + if let Some(group) = InputGroup::from_inputs(inputs) { + can_select.push(group); + } + } + } + + let cs_candidates = Self::build_cs_candidates(&must_select, &can_select); + let no_dup = self.contains; + + Self { + contains: no_dup, + must_select, + can_select, + cs_candidates, + } + } + + /// Filters out inputs. + /// + /// If a filtered-out input is part of a group, the group will also be filtered out. + /// Does not filter `must_select` inputs. + pub fn filter

(mut self, mut policy: P) -> Self + where + P: FnMut(&Input) -> bool, + { + let mut to_rm = Vec::::new(); + self.can_select.retain(|group| { + let retain = group.all(&mut policy); + if !retain { + for input in group.inputs() { + to_rm.push(input.prev_outpoint()); + } + } + retain + }); + for op in to_rm { + self.contains.remove(&op); + } + self.cs_candidates = Self::build_cs_candidates(&self.must_select, &self.can_select); + self + } + + /// Attempt to convert the input candidates into a valid [`Selection`] with a given + /// `algorithm` and selector `params`. + pub fn into_selection( + self, + algorithm: A, + params: SelectorParams, + ) -> Result> + where + A: FnMut(&mut Selector) -> Result<(), E>, + { + let mut selector = Selector::new(&self, params).map_err(IntoSelectionError::Selector)?; + selector + .select_with_algorithm(algorithm) + .map_err(IntoSelectionError::SelectionAlgorithm)?; + let selection = selector + .try_finalize() + .ok_or(IntoSelectionError::CannotMeetTarget(CannotMeetTarget))?; + Ok(selection) + } +} + +/// Occurs when we cannot find a solution for selection. +#[derive(Debug)] +pub enum IntoSelectionError { + /// Coin selector returned an error + Selector(SelectorError), + /// Selection algorithm failed. + SelectionAlgorithm(E), + /// The target cannot be met + CannotMeetTarget(CannotMeetTarget), +} + +impl fmt::Display for IntoSelectionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IntoSelectionError::Selector(error) => { + write!(f, "{}", error) + } + IntoSelectionError::SelectionAlgorithm(error) => { + write!(f, "selection algorithm failed: {}", error) + } + IntoSelectionError::CannotMeetTarget(error) => write!(f, "{}", error), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for IntoSelectionError {} + +/// Occurs when we are missing outputs. +#[derive(Debug)] +pub struct MissingOutputs(HashSet); + +impl Deref for MissingOutputs { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for MissingOutputs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: should not use fmt::Debug on Display + write!(f, "missing outputs: {:?}", self.0) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for MissingOutputs {} + +/// Occurs when a must-select policy cannot be fulfilled. +#[derive(Debug)] +pub enum PolicyFailure { + /// Missing outputs. + MissingOutputs(MissingOutputs), + /// Policy failure. + PolicyFailure(PF), +} + +impl fmt::Display for PolicyFailure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PolicyFailure::MissingOutputs(err) => write!(f, "{}", err), + PolicyFailure::PolicyFailure(err) => { + write!(f, "policy failure: {}", err) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PolicyFailure {} + +/// Select for lowest fee with bnb +pub fn selection_algorithm_lowest_fee_bnb( + longterm_feerate: FeeRate, + max_rounds: usize, +) -> impl FnMut(&mut Selector) -> Result<(), NoBnbSolution> { + let long_term_feerate = cs_feerate(longterm_feerate); + move |selector| { + let target = selector.target(); + let change_policy = selector.change_policy(); + selector + .inner_mut() + .run_bnb( + LowestFee { + target, + long_term_feerate, + change_policy, + }, + max_rounds, + ) + .map(|_| ()) + } +} + +/// Default group policy. +pub fn group_by_spk() -> impl Fn(&Input) -> bitcoin::ScriptBuf { + |input| input.prev_txout().script_pubkey.clone() +} + +/// Filter out inputs that cannot be spent now. +pub fn filter_unspendable_now( + tip_height: absolute::Height, + tip_time: absolute::Time, +) -> impl Fn(&Input) -> bool { + move |input| input.is_spendable_now(tip_height, tip_time) +} + +/// No filtering. +pub fn no_filtering() -> impl Fn(&InputGroup) -> bool { + |_| true +} diff --git a/src/lib.rs b/src/lib.rs index 9fc5404..2e52602 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,13 +9,28 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -mod builder; +mod canonical_unspents; +mod finalizer; +mod input; +mod input_candidates; +mod output; +mod rbf; +mod selection; +mod selector; mod signer; -mod updater; -pub use builder::*; +pub use canonical_unspents::*; +pub use finalizer::*; +pub use input::*; +pub use input_candidates::*; +pub use miniscript; +pub use miniscript::bitcoin; +use miniscript::{DefiniteDescriptorKey, Descriptor}; +pub use output::*; +pub use rbf::*; +pub use selection::*; +pub use selector::*; pub use signer::*; -pub use updater::*; pub(crate) mod collections { #![allow(unused)] @@ -27,3 +42,6 @@ pub(crate) mod collections { pub type HashMap = alloc::collections::BTreeMap; pub use alloc::collections::*; } + +/// Definite descriptor. +pub type DefiniteDescriptor = Descriptor; diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..92e972a --- /dev/null +++ b/src/output.rs @@ -0,0 +1,110 @@ +use bitcoin::{Amount, ScriptBuf, TxOut}; +use miniscript::bitcoin; + +use crate::DefiniteDescriptor; + +/// Source of the output script pubkey +#[derive(Debug, Clone)] +pub enum ScriptSource { + /// bitcoin script + Script(ScriptBuf), + /// definite descriptor + Descriptor(DefiniteDescriptor), +} + +impl From for ScriptSource { + fn from(script: ScriptBuf) -> Self { + Self::from_script(script) + } +} + +impl From for ScriptSource { + fn from(descriptor: DefiniteDescriptor) -> Self { + Self::from_descriptor(descriptor) + } +} + +impl ScriptSource { + /// From script + pub fn from_script(script: ScriptBuf) -> Self { + Self::Script(script) + } + + /// From descriptor + pub fn from_descriptor(descriptor: DefiniteDescriptor) -> Self { + Self::Descriptor(descriptor) + } + + /// To ScriptBuf + pub fn script(&self) -> ScriptBuf { + match self { + ScriptSource::Script(spk) => spk.clone(), + ScriptSource::Descriptor(descriptor) => descriptor.script_pubkey(), + } + } + + /// Get descriptor (if any). + pub fn descriptor(&self) -> Option<&DefiniteDescriptor> { + match self { + ScriptSource::Script(_) => None, + ScriptSource::Descriptor(descriptor) => Some(descriptor), + } + } +} + +/// Builder output +#[derive(Debug, Clone)] +pub struct Output { + /// Value + pub value: Amount, + /// Spk source + pub script_pubkey_source: ScriptSource, +} + +impl From<(ScriptBuf, Amount)> for Output { + fn from((script, value): (ScriptBuf, Amount)) -> Self { + Self::with_script(script, value) + } +} + +impl From<(DefiniteDescriptor, Amount)> for Output { + fn from((descriptor, value): (DefiniteDescriptor, Amount)) -> Self { + Self::with_descriptor(descriptor, value) + } +} + +impl Output { + /// From script + pub fn with_script(script: ScriptBuf, value: Amount) -> Self { + Self { + value, + script_pubkey_source: script.into(), + } + } + + /// From descriptor + pub fn with_descriptor(descriptor: DefiniteDescriptor, value: Amount) -> Self { + Self { + value, + script_pubkey_source: descriptor.into(), + } + } + + /// Script pubkey + pub fn script_pubkey(&self) -> ScriptBuf { + self.script_pubkey_source.script() + } + + /// Descriptor + pub fn descriptor(&self) -> Option<&DefiniteDescriptor> { + self.script_pubkey_source.descriptor() + } + + /// Create txout. + pub fn txout(&self) -> TxOut { + TxOut { + value: self.value, + script_pubkey: self.script_pubkey_source.script(), + } + } +} diff --git a/src/rbf.rs b/src/rbf.rs new file mode 100644 index 0000000..edb2683 --- /dev/null +++ b/src/rbf.rs @@ -0,0 +1,157 @@ +use alloc::sync::Arc; +use core::fmt::Display; + +use bitcoin::{absolute, Amount, OutPoint, Transaction, TxOut, Txid}; +use miniscript::bitcoin; + +use crate::collections::{HashMap, HashSet}; +use crate::{CanonicalUnspents, Input, RbfParams}; + +/// Set of txs to replace. +pub struct RbfSet { + txs: HashMap>, + prev_txouts: HashMap, +} + +/// Occurs when the given original tx has no input spend that is still available for spending. +#[derive(Debug)] +pub struct OriginalTxHasNoInputsAvailable { + /// Original txid. + pub txid: Txid, +} + +impl Display for OriginalTxHasNoInputsAvailable { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "original tx {} has no input spend that is still available", + self.txid + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for OriginalTxHasNoInputsAvailable {} + +impl RbfSet { + /// Create. + /// + /// Returns `None` if we have missing `prev_txouts` for the `txs`. + /// + /// Do not include transactions in `txs` that are descendants of transactions that are already + /// in `txs`. + pub fn new(txs: T, prev_txouts: O) -> Option + where + T: IntoIterator, + T::Item: Into>, + O: IntoIterator, + { + let set = Self { + txs: txs + .into_iter() + .map(|tx| { + let tx: Arc = tx.into(); + (tx.compute_txid(), tx) + }) + .collect(), + prev_txouts: prev_txouts.into_iter().collect(), + }; + let no_missing_previous_txouts = set + .txs + .values() + .flat_map(|tx| tx.input.iter().map(|txin| txin.previous_output)) + .all(|op: OutPoint| set.prev_txouts.contains_key(&op)); + if no_missing_previous_txouts { + Some(set) + } else { + None + } + } + + /// Txids of the original txs that are to be replaced. + pub fn txids(&self) -> impl ExactSizeIterator + '_ { + self.txs.keys().copied() + } + + /// Contains tx. + pub fn contains_tx(&self, txid: Txid) -> bool { + self.txs.contains_key(&txid) + } + + /// Filters input candidates according to rule 2. + /// + /// According to rule 2, we cannot spend unconfirmed txs in the replacement unless it + /// was a spend that was already part of the original tx. + pub fn candidate_filter(&self, tip_height: absolute::Height) -> impl Fn(&Input) -> bool + '_ { + let prev_spends = self + .txs + .values() + .flat_map(|tx| tx.input.iter().map(|txin| txin.previous_output)) + .collect::>(); + move |input| { + prev_spends.contains(&input.prev_outpoint()) || input.confirmations(tip_height) > 0 + } + } + + /// Tries to find the largest input per original tx. + /// + /// The returned outpoints can be used to create the `must_select` inputs to pass into + /// `InputCandidates`. This guarantees that the all transactions within this set gets replaced. + pub fn must_select_largest_input_of_each_original_tx( + &self, + canon_utxos: &CanonicalUnspents, + ) -> Result, OriginalTxHasNoInputsAvailable> { + let mut must_select = HashSet::new(); + + for original_tx in self.txs.values() { + let mut largest_value = Amount::ZERO; + let mut largest_spend = Option::::None; + let original_tx_spends = original_tx.input.iter().map(|txin| txin.previous_output); + for spend in original_tx_spends { + // If this spends from another original tx , we do not consider it as replacing + // the parent will replace this one. + if self.txs.contains_key(&spend.txid) { + continue; + } + let txout = self.prev_txouts.get(&spend).expect("must have prev txout"); + + // not available + if !canon_utxos.is_unspent(spend) { + continue; + } + + if txout.value > largest_value { + largest_value = txout.value; + largest_spend = Some(spend); + } + } + let largest_spend = largest_spend.ok_or(OriginalTxHasNoInputsAvailable { + txid: original_tx.compute_txid(), + })?; + must_select.insert(largest_spend); + } + + Ok(must_select) + } + + fn _fee(&self, tx: &Transaction) -> Amount { + let output_sum: Amount = tx.output.iter().map(|txout| txout.value).sum(); + let input_sum: Amount = tx + .input + .iter() + .map(|txin| { + self.prev_txouts + .get(&txin.previous_output) + .expect("prev output must exist") + .value + }) + .sum(); + // TODO: is it safe to do unchecked subtraction? + input_sum - output_sum + } + + /// Coin selector RBF parameters. + pub fn selector_rbf_params(&self) -> RbfParams { + RbfParams::new(self.txs.values().map(|tx| (tx.as_ref(), self._fee(tx)))) + } +} diff --git a/src/selection.rs b/src/selection.rs new file mode 100644 index 0000000..d0c8d1a --- /dev/null +++ b/src/selection.rs @@ -0,0 +1,204 @@ +use core::fmt::{Debug, Display}; +use std::vec::Vec; + +use bdk_coin_select::FeeRate; +use bitcoin::{absolute, transaction, Sequence}; +use miniscript::bitcoin; +use miniscript::psbt::PsbtExt; + +use crate::{Finalizer, Input, Output}; + +const FALLBACK_SEQUENCE: bitcoin::Sequence = bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF; + +pub(crate) fn cs_feerate(feerate: bitcoin::FeeRate) -> bdk_coin_select::FeeRate { + FeeRate::from_sat_per_wu(feerate.to_sat_per_kwu() as f32 / 1000.0) +} + +/// Final selection of inputs and outputs. +#[derive(Debug, Clone)] +pub struct Selection { + /// Inputs in this selection. + pub inputs: Vec, + /// Outputs in this selection. + pub outputs: Vec, +} + +/// Parameters for creating a psbt. +#[derive(Debug, Clone)] +pub struct PsbtParams { + /// Use a specific [`transaction::Version`]. + pub version: transaction::Version, + + /// Fallback tx locktime. + /// + /// The locktime to use if no inputs specifies a required absolute locktime. + /// + /// It is best practive to set this to the latest block height to avoid fee sniping. + pub fallback_locktime: absolute::LockTime, + + /// [`Sequence`] value to use by default if not provided by the input. + pub fallback_sequence: Sequence, + + /// Whether to require the full tx, aka [`non_witness_utxo`] for segwit v0 inputs, + /// default is `true`. + /// + /// [`non_witness_utxo`]: bitcoin::psbt::Input::non_witness_utxo + pub mandate_full_tx_for_segwit_v0: bool, +} + +impl Default for PsbtParams { + fn default() -> Self { + Self { + version: transaction::Version::TWO, + fallback_locktime: absolute::LockTime::ZERO, + fallback_sequence: FALLBACK_SEQUENCE, + mandate_full_tx_for_segwit_v0: true, + } + } +} + +/// Occurs when creating a psbt fails. +#[derive(Debug)] +pub enum CreatePsbtError { + /// Attempted to mix locktime types. + LockTypeMismatch, + /// Missing tx for legacy input. + MissingFullTxForLegacyInput(Input), + /// Missing tx for segwit v0 input. + MissingFullTxForSegwitV0Input(Input), + /// Psbt error. + Psbt(bitcoin::psbt::Error), + /// Update psbt output with descriptor error. + OutputUpdate(miniscript::psbt::OutputUpdateError), +} + +impl core::fmt::Display for CreatePsbtError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CreatePsbtError::LockTypeMismatch => write!(f, "cannot mix locktime units"), + CreatePsbtError::MissingFullTxForLegacyInput(input) => write!( + f, + "legacy input that spends {} requires PSBT_IN_NON_WITNESS_UTXO", + input.prev_outpoint() + ), + CreatePsbtError::MissingFullTxForSegwitV0Input(input) => write!( + f, + "segwit v0 input that spends {} requires PSBT_IN_NON_WITNESS_UTXO", + input.prev_outpoint() + ), + CreatePsbtError::Psbt(error) => Display::fmt(&error, f), + CreatePsbtError::OutputUpdate(output_update_error) => { + Display::fmt(&output_update_error, f) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CreatePsbtError {} + +impl Selection { + /// Returns none if there is a mismatch of units in `locktimes`. + /// + // TODO: As according to BIP-64... ? + fn _accumulate_max_locktime( + locktimes: impl IntoIterator, + fallback: absolute::LockTime, + ) -> Option { + let mut acc = Option::::None; + for locktime in locktimes { + match &mut acc { + Some(acc) => { + if !acc.is_same_unit(locktime) { + return None; + } + if acc.is_implied_by(locktime) { + *acc = locktime; + } + } + acc => *acc = Some(locktime), + }; + } + if acc.is_none() { + acc = Some(fallback); + } + acc + } + + /// Create psbt. + pub fn create_psbt(&self, params: PsbtParams) -> Result { + let mut psbt = bitcoin::Psbt::from_unsigned_tx(bitcoin::Transaction { + version: params.version, + lock_time: Self::_accumulate_max_locktime( + self.inputs + .iter() + .filter_map(|input| input.absolute_timelock()), + params.fallback_locktime, + ) + .ok_or(CreatePsbtError::LockTypeMismatch)?, + input: self + .inputs + .iter() + .map(|input| bitcoin::TxIn { + previous_output: input.prev_outpoint(), + sequence: input.sequence().unwrap_or(params.fallback_sequence), + ..Default::default() + }) + .collect(), + output: self.outputs.iter().map(|output| output.txout()).collect(), + }) + .map_err(CreatePsbtError::Psbt)?; + + for (plan_input, psbt_input) in self.inputs.iter().zip(psbt.inputs.iter_mut()) { + if let Some(finalized_psbt_input) = plan_input.psbt_input() { + *psbt_input = finalized_psbt_input.clone(); + continue; + } + if let Some(plan) = plan_input.plan() { + plan.update_psbt_input(psbt_input); + + let witness_version = plan.witness_version(); + if witness_version.is_some() { + psbt_input.witness_utxo = Some(plan_input.prev_txout().clone()); + } + // We are allowed to have full tx for segwit inputs. Might as well include it. + // If the caller does not wish to include the full tx in Segwit V0 inputs, they should not + // include it in `crate::Input`. + psbt_input.non_witness_utxo = plan_input.prev_tx().cloned(); + if psbt_input.non_witness_utxo.is_none() { + if witness_version.is_none() { + return Err(CreatePsbtError::MissingFullTxForLegacyInput( + plan_input.clone(), + )); + } + if params.mandate_full_tx_for_segwit_v0 + && witness_version == Some(bitcoin::WitnessVersion::V0) + { + return Err(CreatePsbtError::MissingFullTxForSegwitV0Input( + plan_input.clone(), + )); + } + } + continue; + } + unreachable!("input candidate must either have finalized psbt input or plan"); + } + for (output_index, output) in self.outputs.iter().enumerate() { + if let Some(desc) = output.descriptor() { + psbt.update_output_with_descriptor(output_index, desc) + .map_err(CreatePsbtError::OutputUpdate)?; + } + } + + Ok(psbt) + } + + /// Into psbt finalizer. + pub fn into_finalizer(self) -> Finalizer { + Finalizer::new( + self.inputs + .iter() + .filter_map(|input| Some((input.prev_outpoint(), input.plan().cloned()?))), + ) + } +} diff --git a/src/selector.rs b/src/selector.rs new file mode 100644 index 0000000..3063e0f --- /dev/null +++ b/src/selector.rs @@ -0,0 +1,370 @@ +use bdk_coin_select::{ + ChangePolicy, DrainWeights, InsufficientFunds, Replace, Target, TargetFee, TargetOutputs, +}; +use bitcoin::{Amount, FeeRate, Transaction, TxOut, Weight}; +use miniscript::bitcoin; + +use crate::{cs_feerate, DefiniteDescriptor, InputCandidates, InputGroup, Output, Selection}; +use alloc::vec::Vec; +use core::fmt; + +/// A coin selector +#[derive(Debug, Clone)] +pub struct Selector<'c> { + candidates: &'c InputCandidates, + target_outputs: Vec, + target: Target, + change_policy: bdk_coin_select::ChangePolicy, + change_descriptor: DefiniteDescriptor, + inner: bdk_coin_select::CoinSelector<'c>, +} + +/// Parameters for creating tx. +/// +/// TODO: Create a builder interface on this that does checks. I.e. +/// * Error if recipient is dust. +/// * Error on multi OP_RETURN outputs. +/// * Error on anything that does not satisfy mempool policy. +/// If the caller wants to create non-mempool-policy conforming txs, they can just fill in the +/// fields directly. +#[derive(Debug, Clone)] +pub struct SelectorParams { + /// Feerate target! + /// + /// This can end up higher. + pub target_feerate: bitcoin::FeeRate, + + ///// Uses `target_feerate` as a fallback. + //pub long_term_feerate: bitcoin::FeeRate, + /// Outputs that must be included. + pub target_outputs: Vec, + + /// To derive change output. + /// + /// Will error if this is unsatisfiable descriptor. + pub change_descriptor: DefiniteDescriptor, + + /// The policy to determine whether we create a change output. + pub change_policy: ChangePolicyType, + + /// Params for replacing tx(s). + pub replace: Option, +} + +/// Rbf original tx stats. +#[derive(Debug, Clone, Copy)] +pub struct OriginalTxStats { + /// Total weight of the original tx. + pub weight: Weight, + /// Total fee amount of the original tx. + pub fee: Amount, +} + +impl From<(Weight, Amount)> for OriginalTxStats { + fn from((weight, fee): (Weight, Amount)) -> Self { + Self { weight, fee } + } +} + +impl From<(&Transaction, Amount)> for OriginalTxStats { + fn from((tx, fee): (&Transaction, Amount)) -> Self { + let weight = tx.weight(); + Self { weight, fee } + } +} + +/// Rbf params. +#[derive(Debug, Clone)] +pub struct RbfParams { + /// Original txs. + pub original_txs: Vec, + /// Incremental relay feerate. + pub incremental_relay_feerate: FeeRate, +} + +/// Change policy type +// TODO: Make this more flexible. +#[derive(Debug, Clone, Copy)] +pub enum ChangePolicyType { + /// Avoid creating dust change output. + NoDust, + /// Avoid creating dust change output and minimize waste. + NoDustAndLeastWaste { + /// Long term feerate. + longterm_feerate: bitcoin::FeeRate, + }, +} + +impl OriginalTxStats { + /// Return the [`FeeRate`] of the original tx. + pub fn feerate(&self) -> FeeRate { + self.fee / self.weight + } +} + +impl RbfParams { + /// Construct RBF parameters. + pub fn new(tx_to_replace: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + Self { + original_txs: tx_to_replace.into_iter().map(Into::into).collect(), + incremental_relay_feerate: FeeRate::from_sat_per_vb_unchecked(1), + } + } + + /// To coin select `Replace` params. + pub fn to_cs_replace(&self) -> Replace { + Replace { + fee: self.original_txs.iter().map(|otx| otx.fee.to_sat()).sum(), + incremental_relay_feerate: cs_feerate(self.incremental_relay_feerate), + } + } + + /// Max feerate of all the original txs. + /// + /// The replacement tx must have a feerate larger than this value. + pub fn max_feerate(&self) -> FeeRate { + self.original_txs + .iter() + .map(|otx| otx.feerate()) + .max() + .unwrap_or(FeeRate::ZERO) + } +} + +impl SelectorParams { + /// With default params. + pub fn new( + target_feerate: bitcoin::FeeRate, + target_outputs: Vec, + change_descriptor: DefiniteDescriptor, + change_policy: ChangePolicyType, + ) -> Self { + Self { + change_descriptor, + change_policy, + target_feerate, + target_outputs, + replace: None, + } + } + + /// To coin select target. + pub fn to_cs_target(&self) -> Target { + let feerate_lb = self + .replace + .as_ref() + .map_or(FeeRate::ZERO, |r| r.max_feerate()); + Target { + fee: TargetFee { + rate: cs_feerate(self.target_feerate.max(feerate_lb)), + replace: self.replace.as_ref().map(|r| r.to_cs_replace()), + }, + outputs: TargetOutputs::fund_outputs( + self.target_outputs + .iter() + .map(|output| (output.txout().weight().to_wu(), output.value.to_sat())), + ), + } + } + + /// To change output weights. + /// + /// # Error + /// + /// Fails if `change_descriptor` cannot be satisfied. + pub fn to_cs_change_weights(&self) -> Result { + Ok(DrainWeights { + output_weight: (TxOut { + script_pubkey: self.change_descriptor.script_pubkey(), + value: Amount::ZERO, + }) + .weight() + .to_wu(), + spend_weight: self.change_descriptor.max_weight_to_satisfy()?.to_wu(), + n_outputs: 1, + }) + } + + /// To change policy. + /// + /// # Error + /// + /// Fails if `change_descriptor` cannot be satisfied. + pub fn to_cs_change_policy(&self) -> Result { + let change_weights = self.to_cs_change_weights()?; + let dust_value = self + .change_descriptor + .script_pubkey() + .minimal_non_dust() + .to_sat(); + Ok(match self.change_policy { + ChangePolicyType::NoDust => ChangePolicy::min_value(change_weights, dust_value), + ChangePolicyType::NoDustAndLeastWaste { longterm_feerate } => { + ChangePolicy::min_value_and_waste( + change_weights, + dust_value, + cs_feerate(self.target_feerate), + cs_feerate(longterm_feerate), + ) + } + }) + } +} + +/// Error when the selection is impossible with the input candidates +#[derive(Debug)] +pub struct CannotMeetTarget; + +impl fmt::Display for CannotMeetTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "meeting the target is not possible with the input candidates" + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CannotMeetTarget {} + +/// Selector error +#[derive(Debug)] +pub enum SelectorError { + /// miniscript error + Miniscript(miniscript::Error), + /// meeting the target is not possible + CannotMeetTarget(CannotMeetTarget), +} + +impl fmt::Display for SelectorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Miniscript(err) => write!(f, "{}", err), + Self::CannotMeetTarget(err) => write!(f, "{}", err), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SelectorError {} + +impl<'c> Selector<'c> { + /// Create new input selector. + /// + /// # Errors + /// + /// - If we are unable to create a change policy from the `params`. + /// - If the target is unreachable given the total input value. + pub fn new( + candidates: &'c InputCandidates, + params: SelectorParams, + ) -> Result { + let target = params.to_cs_target(); + let change_policy = params + .to_cs_change_policy() + .map_err(SelectorError::Miniscript)?; + let target_outputs = params.target_outputs; + let change_descriptor = params.change_descriptor; + if target.value() > candidates.groups().map(|grp| grp.value().to_sat()).sum() { + return Err(SelectorError::CannotMeetTarget(CannotMeetTarget)); + } + let mut inner = bdk_coin_select::CoinSelector::new(candidates.coin_select_candidates()); + if candidates.must_select().is_some() { + inner.select_next(); + } + Ok(Self { + candidates, + target, + target_outputs, + change_policy, + change_descriptor, + inner, + }) + } + + /// Get the inner coin selector. + pub fn inner(&self) -> &bdk_coin_select::CoinSelector<'c> { + &self.inner + } + + /// Get a mutable reference to the inner coin selector. + pub fn inner_mut(&mut self) -> &mut bdk_coin_select::CoinSelector<'c> { + &mut self.inner + } + + /// Coin selection target. + pub fn target(&self) -> Target { + self.target + } + + /// Coin selection change policy. + pub fn change_policy(&self) -> bdk_coin_select::ChangePolicy { + self.change_policy + } + + /// Select with the provided `algorithm`. + pub fn select_with_algorithm(&mut self, mut algorithm: F) -> Result<(), E> + where + F: FnMut(&mut Selector) -> Result<(), E>, + { + algorithm(self) + } + + /// Select all. + pub fn select_all(&mut self) { + self.inner.select_all(); + } + + /// Select in order until target is met. + pub fn select_until_target_met(&mut self) -> Result<(), InsufficientFunds> { + self.inner.select_until_target_met(self.target) + } + + /// Whether we added the change output to the selection. + /// + /// Return `None` if target is not met yet. + pub fn has_change(&self) -> Option { + if !self.inner.is_target_met(self.target) { + return None; + } + let has_drain = self + .inner + .drain_value(self.target, self.change_policy) + .is_some(); + Some(has_drain) + } + + /// Try get final selection. + /// + /// Return `None` if target is not met yet. + pub fn try_finalize(&self) -> Option { + if !self.inner.is_target_met(self.target) { + return None; + } + let maybe_change = self.inner.drain(self.target, self.change_policy); + let to_apply = self.candidates.groups().collect::>(); + Some(Selection { + inputs: self + .inner + .apply_selection(&to_apply) + .copied() + .flat_map(InputGroup::inputs) + .cloned() + .collect(), + outputs: { + let mut outputs = self.target_outputs.clone(); + if maybe_change.is_some() { + outputs.push(Output::with_descriptor( + self.change_descriptor.clone(), + Amount::from_sat(maybe_change.value), + )); + } + outputs + }, + }) + } +} diff --git a/src/updater.rs b/src/updater.rs deleted file mode 100644 index 85ded19..0000000 --- a/src/updater.rs +++ /dev/null @@ -1,315 +0,0 @@ -use bitcoin::{ - bip32::{self, DerivationPath, Fingerprint}, - psbt::{self, PsbtSighashType}, - OutPoint, Psbt, Transaction, TxOut, Txid, Witness, -}; -use miniscript::{ - bitcoin, - descriptor::{DefiniteDescriptorKey, DescriptorType}, - plan::Plan, - psbt::{PsbtExt, PsbtInputSatisfier}, - Descriptor, -}; - -use crate::collections::{BTreeMap, HashMap}; -use crate::PlanUtxo; - -/// Trait describing the actions required to update a PSBT. -pub trait DataProvider { - /// Get transaction by txid - fn get_tx(&self, txid: Txid) -> Option; - - /// Get the definite descriptor that can derive the script in `txout`. - fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option>; - - /// Sort transaction inputs and outputs. - /// - /// This has a default implementation that does no sorting. The implementation must not alter - /// the semantics of the transaction in any way, like changing the number of inputs and outputs, - /// changing scripts or amounts, or otherwise interfere with transaction building. - fn sort_transaction(&mut self, _tx: &mut Transaction) {} -} - -/// Updater -#[derive(Debug)] -pub struct PsbtUpdater { - psbt: Psbt, - map: HashMap, -} - -impl PsbtUpdater { - /// New from `unsigned_tx` and `utxos` - pub fn new( - unsigned_tx: Transaction, - utxos: impl IntoIterator, - ) -> Result { - let map: HashMap<_, _> = utxos.into_iter().map(|p| (p.outpoint, p)).collect(); - debug_assert!( - unsigned_tx - .input - .iter() - .all(|txin| map.contains_key(&txin.previous_output)), - "all spends must be accounted for", - ); - let psbt = Psbt::from_unsigned_tx(unsigned_tx)?; - - Ok(Self { psbt, map }) - } - - /// Get plan - fn get_plan(&self, outpoint: &OutPoint) -> Option<&Plan> { - Some(&self.map.get(outpoint)?.plan) - } - - // Get txout - fn get_txout(&self, outpoint: &OutPoint) -> Option { - self.map.get(outpoint).map(|p| p.txout.clone()) - } - - /// Update the PSBT with the given `provider` and update options. - /// - /// # Errors - /// - /// This function may error if a discrepancy is found between the outpoint, previous - /// txout and witness/non-witness utxo for a planned input. - pub fn update_psbt( - &mut self, - provider: &D, - opt: UpdateOptions, - ) -> Result<(), UpdatePsbtError> - where - D: DataProvider, - { - let tx = self.psbt.unsigned_tx.clone(); - - // update inputs - for (input_index, txin) in tx.input.iter().enumerate() { - let outpoint = txin.previous_output; - let plan = self.get_plan(&outpoint).expect("must have plan").clone(); - let prevout = self.get_txout(&outpoint).expect("must have txout"); - - // update input with plan - let psbt_input = &mut self.psbt.inputs[input_index]; - plan.update_psbt_input(psbt_input); - - // add non-/witness utxo - if let Some(desc) = provider.get_descriptor_for_txout(&prevout) { - if is_witness(desc.desc_type()) { - psbt_input.witness_utxo = Some(prevout.clone()); - } - if !is_taproot(desc.desc_type()) && !opt.only_witness_utxo { - psbt_input.non_witness_utxo = provider.get_tx(outpoint.txid); - } - } - - if opt.sighash_type.is_some() { - psbt_input.sighash_type = opt.sighash_type; - } - - // update fields not covered by `update_psbt_input` e.g. `.tap_scripts` - if opt.update_with_descriptor { - if let Some(desc) = provider.get_descriptor_for_txout(&prevout) { - self.psbt - .update_input_with_descriptor(input_index, &desc) - .map_err(UpdatePsbtError::Utxo)?; - } - } - } - - // update outputs - for (output_index, txout) in tx.output.iter().enumerate() { - if let Some(desc) = provider.get_descriptor_for_txout(txout) { - self.psbt - .update_output_with_descriptor(output_index, &desc) - .map_err(UpdatePsbtError::Output)?; - } - } - - Ok(()) - } - - /// Return a reference to the PSBT - pub fn psbt(&self) -> &Psbt { - &self.psbt - } - - /// Add a [`bip32::Xpub`] and key origin to the psbt global xpubs - pub fn add_global_xpub(&mut self, xpub: bip32::Xpub, origin: (Fingerprint, DerivationPath)) { - self.psbt.xpub.insert(xpub, origin); - } - - /// Set a `sighash_type` for the psbt input at `index` - pub fn sighash_type(&mut self, index: usize, sighash_type: Option) { - if let Some(psbt_input) = self.psbt.inputs.get_mut(index) { - psbt_input.sighash_type = sighash_type; - } - } - - /// Convert this updater into a [`Finalizer`] and return the updated [`Psbt`]. - pub fn into_finalizer(self) -> (Psbt, Finalizer) { - (self.psbt, Finalizer { map: self.map }) - } -} - -/// Options for updating a PSBT -#[derive(Debug, Default, Clone)] -pub struct UpdateOptions { - /// Only set the input `witness_utxo` if applicable, i.e. do not set `non_witness_utxo`. - /// - /// Defaults to `false` which will set the `non_witness_utxo` for non-taproot inputs - pub only_witness_utxo: bool, - - /// Use a particular sighash type for all PSBT inputs - pub sighash_type: Option, - - /// Whether to use the descriptor to update as many fields as we can. - /// - /// Defaults to `false` which will update only the fields of the PSBT - /// that are relevant to the current spend plan. - pub update_with_descriptor: bool, -} - -/// Error when updating a PSBT -#[derive(Debug)] -pub enum UpdatePsbtError { - /// output update - Output(miniscript::psbt::OutputUpdateError), - /// utxo update - Utxo(miniscript::psbt::UtxoUpdateError), -} - -impl core::fmt::Display for UpdatePsbtError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Output(e) => e.fmt(f), - Self::Utxo(e) => e.fmt(f), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for UpdatePsbtError {} - -/// Whether the given descriptor type matches any of the post-segwit descriptor types -/// including segwit v1 (taproot) -fn is_witness(desc_ty: DescriptorType) -> bool { - use DescriptorType::*; - matches!( - desc_ty, - Wpkh | ShWpkh | Wsh | ShWsh | ShWshSortedMulti | WshSortedMulti | Tr, - ) -} - -/// Whether this descriptor type is `Tr` -fn is_taproot(desc_ty: DescriptorType) -> bool { - matches!(desc_ty, DescriptorType::Tr) -} - -/// Finalizer -#[derive(Debug)] -pub struct Finalizer { - map: HashMap, -} - -impl Finalizer { - /// Get plan - fn get_plan(&self, outpoint: &OutPoint) -> Option<&Plan> { - Some(&self.map.get(outpoint)?.plan) - } - - /// Finalize a PSBT input and return whether finalization was successful. - /// - /// # Errors - /// - /// If the spending plan associated with the PSBT input cannot be satisfied, - /// then a [`miniscript::Error`] is returned. - /// - /// # Panics - /// - /// - If `input_index` is outside the bounds of the PSBT input vector. - pub fn finalize_input( - &self, - psbt: &mut Psbt, - input_index: usize, - ) -> Result { - let mut finalized = false; - let outpoint = psbt - .unsigned_tx - .input - .get(input_index) - .expect("index out of range") - .previous_output; - if let Some(plan) = self.get_plan(&outpoint) { - let stfr = PsbtInputSatisfier::new(psbt, input_index); - let (stack, script) = plan.satisfy(&stfr)?; - // clearing all fields and setting back the utxo, final scriptsig and witness - let original = core::mem::take(&mut psbt.inputs[input_index]); - let psbt_input = &mut psbt.inputs[input_index]; - psbt_input.non_witness_utxo = original.non_witness_utxo; - psbt_input.witness_utxo = original.witness_utxo; - if !script.is_empty() { - psbt_input.final_script_sig = Some(script); - } - if !stack.is_empty() { - psbt_input.final_script_witness = Some(Witness::from_slice(&stack)); - } - finalized = true; - } - - Ok(finalized) - } - - /// Attempt to finalize all of the inputs. - /// - /// This method returns a [`FinalizeMap`] that contains the result of finalization - /// for each input. - pub fn finalize(&self, psbt: &mut Psbt) -> FinalizeMap { - let mut finalized = true; - let mut result = FinalizeMap(BTreeMap::new()); - - for input_index in 0..psbt.inputs.len() { - let psbt_input = &psbt.inputs[input_index]; - if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { - continue; - } - match self.finalize_input(psbt, input_index) { - Ok(is_final) => { - if finalized && !is_final { - finalized = false; - } - result.0.insert(input_index, Ok(is_final)); - } - Err(e) => { - result.0.insert(input_index, Err(e)); - } - } - } - - // clear psbt outputs - if finalized { - for psbt_output in &mut psbt.outputs { - psbt_output.bip32_derivation.clear(); - psbt_output.tap_key_origins.clear(); - psbt_output.tap_internal_key.take(); - } - } - - result - } -} - -/// Holds the results of finalization -#[derive(Debug)] -pub struct FinalizeMap(BTreeMap>); - -impl FinalizeMap { - /// Whether all inputs were finalized - pub fn is_finalized(&self) -> bool { - self.0.values().all(|res| matches!(res, Ok(true))) - } - - /// Get the results as a map of `input_index` to `finalize_input` result. - pub fn results(self) -> BTreeMap> { - self.0 - } -} diff --git a/tests/psbt.rs b/tests/psbt.rs new file mode 100644 index 0000000..78f4298 --- /dev/null +++ b/tests/psbt.rs @@ -0,0 +1,454 @@ +// #[cfg(test)] +// mod test { +// use super::*; +// use crate::Signer; +// use alloc::string::String; +// +// use bitcoin::{ +// secp256k1::{self, Secp256k1}, +// Txid, +// }; +// use miniscript::{ +// descriptor::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, KeyMap}, +// plan::Assets, +// ForEachKey, +// }; +// +// use bdk_chain::{ +// bdk_core, keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph, +// TxGraph, +// }; +// use bdk_core::{CheckPoint, ConfirmationBlockTime}; +// +// const XPRV: &str = "tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L"; +// const WIF: &str = "cU6BxEezV8FnkEPBCaFtc4WNuUKmgFaAu6sJErB154GXgMUjhgWe"; +// const SPK: &str = "00143f027073e6f341c481f55b7baae81dda5e6a9fba"; +// +// fn get_single_sig_tr_xprv() -> Vec { +// (0..2) +// .map(|i| format!("tr({XPRV}/86h/1h/0h/{i}/*)")) +// .collect() +// } +// +// fn get_single_sig_cltv_timestamp() -> String { +// format!("wsh(and_v(v:pk({WIF}),after(1735877503)))") +// } +// +// type KeychainTxGraph = IndexedTxGraph>; +// +// #[derive(Debug)] +// struct TestProvider { +// assets: Assets, +// signer: Signer, +// secp: Secp256k1, +// chain: LocalChain, +// graph: KeychainTxGraph, +// } +// +// // impl DataProvider for TestProvider { +// // fn get_tx(&self, txid: Txid) -> Option { +// // self.graph +// // .graph() +// // .get_tx(txid) +// // .map(|tx| tx.as_ref().clone()) +// // } +// // +// // // fn get_descriptor_for_txout( +// // // &self, +// // // txout: &TxOut, +// // // ) -> Option> { +// // // let indexer = &self.graph.index; +// // // +// // // let (keychain, index) = indexer.index_of_spk(txout.script_pubkey.clone())?; +// // // let desc = indexer.get_descriptor(*keychain)?; +// // // +// // // desc.at_derivation_index(*index).ok() +// // // } +// // } +// +// impl TestProvider { +// /// Set max absolute timelock +// fn after(mut self, lt: absolute::LockTime) -> Self { +// self.assets = self.assets.after(lt); +// self +// } +// +// /// Get a reference to the tx graph +// fn graph(&self) -> &TxGraph { +// self.graph.graph() +// } +// +// /// Get a reference to the indexer +// fn index(&self) -> &KeychainTxOutIndex { +// &self.graph.index +// } +// +// /// Get the script pubkey at the specified `index` from the first keychain +// /// (by Ord). +// fn spk_at_index(&self, index: u32) -> Option { +// let keychain = self.graph.index.keychains().next().unwrap().0; +// self.graph.index.spk_at_index(keychain, index) +// } +// +// /// Get next unused internal script pubkey +// fn next_internal_spk(&mut self) -> ScriptBuf { +// let keychain = self.graph.index.keychains().last().unwrap().0; +// let ((_, spk), _) = self.graph.index.next_unused_spk(keychain).unwrap(); +// spk +// } +// +// /// Get balance +// fn balance(&self) -> bdk_chain::Balance { +// let chain = &self.chain; +// let chain_tip = chain.tip().block_id(); +// +// let outpoints = self.graph.index.outpoints().clone(); +// let graph = self.graph.graph(); +// graph.balance(chain, chain_tip, outpoints, |_, _| true) +// } +// +// /// Get a list of planned utxos sorted largest first +// fn planned_utxos(&self) -> Vec { +// let chain = &self.chain; +// let chain_tip = chain.tip().block_id(); +// let op = self.index().outpoints().clone(); +// +// let mut utxos = vec![]; +// +// for (indexed, txo) in self.graph().filter_chain_unspents(chain, chain_tip, op) { +// let (keychain, index) = indexed; +// let desc = self.index().get_descriptor(keychain).unwrap(); +// let def = desc.at_derivation_index(index).unwrap(); +// if let Ok(plan) = def.plan(&self.assets) { +// utxos.push(PlanInput { +// plan, +// outpoint: txo.outpoint, +// txout: txo.txout, +// residing_tx: None, +// }); +// } +// } +// +// utxos.sort_by_key(|p| p.txout.value); +// utxos.reverse(); +// +// utxos +// } +// +// /// Attempt to create all the required signatures for this psbt +// fn sign(&self, psbt: &mut Psbt) { +// let _ = psbt.sign(&self.signer, &self.secp); +// } +// } +// +// macro_rules! block_id { +// ( $height:expr, $hash:expr ) => { +// bdk_chain::BlockId { +// height: $height, +// hash: $hash, +// } +// }; +// } +// +// fn new_tx(lt: u32) -> Transaction { +// Transaction { +// version: transaction::Version(2), +// lock_time: absolute::LockTime::from_consensus(lt), +// input: vec![TxIn::default()], +// output: vec![], +// } +// } +// +// fn parse_descriptor(s: &str) -> (Descriptor, KeyMap) { +// >::parse_descriptor(&Secp256k1::new(), s).unwrap() +// } +// +// /// Initialize a [`TestProvider`] with the given `descriptors`. +// /// +// /// The returned object contains a local chain at height 1000 and an indexed tx graph +// /// with 10 x 1Msat utxos. +// fn init_graph(descriptors: &[String]) -> TestProvider { +// use bitcoin::{constants, hashes::Hash, Network}; +// +// let mut keys = vec![]; +// let mut keymap = KeyMap::new(); +// +// let mut index = KeychainTxOutIndex::new(10); +// for (keychain, desc_str) in descriptors.iter().enumerate() { +// let (desc, km) = parse_descriptor(desc_str); +// desc.for_each_key(|k| { +// keys.push(k.clone()); +// true +// }); +// keymap.extend(km); +// index.insert_descriptor(keychain, desc).unwrap(); +// } +// +// let mut graph = KeychainTxGraph::new(index); +// +// let genesis_hash = constants::genesis_block(Network::Regtest).block_hash(); +// let mut cp = CheckPoint::new(block_id!(0, genesis_hash)); +// +// for height in 1..11 { +// let ((_, script_pubkey), _) = graph.index.reveal_next_spk(0).unwrap(); +// +// let tx = Transaction { +// output: vec![TxOut { +// value: Amount::from_btc(0.01).unwrap(), +// script_pubkey, +// }], +// ..new_tx(height) +// }; +// let txid = tx.compute_txid(); +// let _ = graph.insert_tx(tx); +// +// let block_id = block_id!(height, Hash::hash(height.to_be_bytes().as_slice())); +// let anchor = ConfirmationBlockTime { +// block_id, +// confirmation_time: height as u64, +// }; +// let _ = graph.insert_anchor(txid, anchor); +// +// cp = cp.insert(block_id); +// } +// +// let tip = block_id!(1000, Hash::hash(b"Z")); +// cp = cp.insert(tip); +// let chain = LocalChain::from_tip(cp).unwrap(); +// +// let assets = Assets::new().add(keys); +// +// TestProvider { +// assets, +// signer: Signer(keymap), +// secp: Secp256k1::new(), +// chain, +// graph, +// } +// } +// +// #[test] +// fn test_build_tx_finalize() { +// let mut graph = init_graph(&get_single_sig_tr_xprv()); +// assert_eq!(graph.balance().total().to_btc(), 0.1); +// +// let recip = ScriptBuf::from_hex(SPK).unwrap(); +// let mut builder = Builder::new(); +// builder.add_output(recip, Amount::from_sat(2_500_000)); +// +// let selection = graph.planned_utxos().into_iter().take(3); +// builder.add_inputs(selection); +// builder.add_change_output(graph.next_internal_spk(), Amount::from_sat(499_500)); +// +// let (mut psbt, finalizer) = builder.build_tx().unwrap(); +// assert_eq!(psbt.unsigned_tx.input.len(), 3); +// assert_eq!(psbt.unsigned_tx.output.len(), 2); +// +// graph.sign(&mut psbt); +// assert!(finalizer.finalize(&mut psbt).is_finalized()); +// } +// +// #[test] +// fn test_build_tx_insane_fee() { +// let mut graph = init_graph(&get_single_sig_tr_xprv()); +// +// let recip = ScriptBuf::from_hex(SPK).unwrap(); +// let mut builder = Builder::new(); +// builder.add_output(recip, Amount::from_btc(0.01).unwrap()); +// +// let selection = graph +// .planned_utxos() +// .into_iter() +// .take(3) +// .collect::>(); +// assert_eq!( +// selection +// .iter() +// .map(|p| p.txout.value) +// .sum::() +// .to_btc(), +// 0.03 +// ); +// builder.add_inputs(selection); +// +// let err = builder.build_tx().unwrap_err(); +// assert!(matches!(err, Error::InsaneFee(..))); +// } +// +// #[test] +// fn test_build_tx_negative_fee() { +// let mut graph = init_graph(&get_single_sig_tr_xprv()); +// +// let recip = ScriptBuf::from_hex(SPK).unwrap(); +// +// let mut builder = Builder::new(); +// builder.add_output(recip, Amount::from_btc(0.02).unwrap()); +// builder.add_inputs(graph.planned_utxos().into_iter().take(1)); +// +// let err = builder.build_tx().unwrap_err(); +// assert!(matches!(err, Error::NegativeFee(..))); +// } +// +// #[test] +// fn test_build_tx_add_data() { +// let mut graph = init_graph(&get_single_sig_tr_xprv()); +// +// let mut builder = Builder::new(); +// builder.add_inputs(graph.planned_utxos().into_iter().take(1)); +// builder.add_output(graph.next_internal_spk(), Amount::from_sat(999_000)); +// builder.add_data(b"satoshi nakamoto").unwrap(); +// +// let psbt = builder.build_tx().unwrap().0; +// assert!(psbt +// .unsigned_tx +// .output +// .iter() +// .any(|txo| txo.script_pubkey.is_op_return())); +// +// // try to add more than 80 bytes of data +// let data = [0x90; 81]; +// builder = Builder::new(); +// assert!(matches!( +// builder.add_data(data), +// Err(Error::MaxOpReturnRelay) +// )); +// +// // try to add more than 1 op return +// let data = [0x90; 80]; +// builder = Builder::new(); +// builder.add_data(data).unwrap(); +// assert!(matches!( +// builder.add_data(data), +// Err(Error::TooManyOpReturn) +// )); +// } +// +// #[test] +// fn test_build_tx_version() { +// use transaction::Version; +// let mut graph = init_graph(&get_single_sig_tr_xprv()); +// +// // test default tx version (2) +// let mut builder = Builder::new(); +// let recip = graph.spk_at_index(0).unwrap(); +// let utxo = graph.planned_utxos().first().unwrap().clone(); +// let amt = utxo.txout.value - Amount::from_sat(256); +// builder.add_input(utxo.clone()); +// builder.add_output(recip.clone(), amt); +// +// let psbt = builder.build_tx().unwrap().0; +// assert_eq!(psbt.unsigned_tx.version, Version::TWO); +// +// // allow any potentially non-standard version +// builder = Builder::new(); +// builder.version(Version(3)); +// builder.add_input(utxo); +// builder.add_output(recip, amt); +// +// let psbt = builder.build_tx().unwrap().0; +// assert_eq!(psbt.unsigned_tx.version, Version(3)); +// } +// +// #[test] +// fn test_timestamp_timelock() { +// #[derive(Clone)] +// struct InOut { +// input: PlanInput, +// output: (ScriptBuf, Amount), +// } +// fn check_locktime(graph: &mut TestProvider, in_out: InOut, lt: u32, exp_lt: Option) { +// let InOut { +// input, +// output: (recip, amount), +// } = in_out; +// +// let mut builder = Builder::new(); +// builder.add_output(recip, amount); +// builder.add_input(input); +// builder.locktime(absolute::LockTime::from_consensus(lt)); +// +// let res = builder.build_tx(); +// +// match res { +// Ok((mut psbt, finalizer)) => { +// assert_eq!( +// psbt.unsigned_tx.lock_time.to_consensus_u32(), +// exp_lt.unwrap() +// ); +// graph.sign(&mut psbt); +// assert!(finalizer.finalize(&mut psbt).is_finalized()); +// } +// Err(e) => { +// assert!(exp_lt.is_none()); +// if absolute::LockTime::from_consensus(lt).is_block_height() { +// assert!(matches!(e, Error::LockTypeMismatch)); +// } else if lt < 1735877503 { +// assert!(matches!(e, Error::LockTimeCltv { .. })); +// } +// } +// } +// } +// +// // initial state +// let mut graph = init_graph(&[get_single_sig_cltv_timestamp()]); +// let mut t = 1735877503; +// let locktime = absolute::LockTime::from_consensus(t); +// +// // supply the assets needed to create plans +// graph = graph.after(locktime); +// +// let in_out = InOut { +// input: graph.planned_utxos().first().unwrap().clone(), +// output: (ScriptBuf::from_hex(SPK).unwrap(), Amount::from_sat(999_000)), +// }; +// +// // Test: tx should use the planned locktime +// check_locktime(&mut graph, in_out.clone(), t, Some(t)); +// +// // Test: requesting a lower timelock should error +// check_locktime( +// &mut graph, +// in_out.clone(), +// absolute::LOCK_TIME_THRESHOLD, +// None, +// ); +// +// // Test: tx may use a custom locktime +// t += 1; +// check_locktime(&mut graph, in_out.clone(), t, Some(t)); +// +// // Test: error if lock type mismatch +// check_locktime(&mut graph, in_out, 100, None); +// } +// +// #[test] +// fn test_build_zero_fee_tx() { +// let mut graph = init_graph(&get_single_sig_tr_xprv()); +// +// let recip = ScriptBuf::from_hex(SPK).unwrap(); +// let utxos = graph.planned_utxos(); +// +// // case: 1-in/1-out +// let mut builder = Builder::new(); +// builder.add_inputs(utxos.iter().take(1).cloned()); +// builder.add_output(recip.clone(), Amount::from_sat(1_000_000)); +// let psbt = builder.build_tx().unwrap().0; +// assert_eq!(psbt.unsigned_tx.output.len(), 1); +// assert_eq!(psbt.unsigned_tx.output[0].value.to_btc(), 0.01); +// +// // case: 1-in/2-out +// let mut builder = Builder::new(); +// builder.add_inputs(utxos.iter().take(1).cloned()); +// builder.add_output(recip, Amount::from_sat(500_000)); +// builder.add_change_output(graph.next_internal_spk(), Amount::from_sat(500_000)); +// builder.check_fee(Some(Amount::ZERO), Some(FeeRate::from_sat_per_kwu(0))); +// +// let psbt = builder.build_tx().unwrap().0; +// assert_eq!(psbt.unsigned_tx.output.len(), 2); +// assert!(psbt +// .unsigned_tx +// .output +// .iter() +// .all(|txo| txo.value.to_sat() == 500_000)); +// } +// }