Move everything else over 🎉
This completes the move of things from https://github.com/LLFourn/bdk_core_staging
This commit is contained in:
parent
949608ab1f
commit
c069b0fb41
11
Cargo.toml
11
Cargo.toml
@ -1,11 +1,16 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/bdk",
|
||||
"crates/bdk_chain",
|
||||
"crates/bdk_file_store",
|
||||
"crates/bdk_electrum",
|
||||
"crates/chain",
|
||||
"crates/file_store",
|
||||
"crates/electrum",
|
||||
"example-crates/esplora-wallet",
|
||||
"example-crates/electrum-wallet",
|
||||
"example-crates/keychain_tracker_electrum_example",
|
||||
"example-crates/keychain_tracker_esplora_example",
|
||||
"example-crates/keychain_tracker_example_cli",
|
||||
"nursery/tmp_plan",
|
||||
"nursery/coin_select"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
@ -32,7 +32,7 @@
|
||||
//!
|
||||
//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
|
||||
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
|
||||
//! println!("policy: {}", serde_json::to_string(&policy)?);
|
||||
//! println!("policy: {}", serde_json::to_string(&policy).unwrap());
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! ```
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
||||
//! import.change_descriptor().as_ref(),
|
||||
//! Network::Testnet,
|
||||
//! )?;
|
||||
//! # Ok::<_, bdk::Error>(())
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
//!
|
||||
//! ### Export a `Wallet`
|
||||
@ -47,12 +47,10 @@
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
|
||||
//! Network::Testnet,
|
||||
//! )?;
|
||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
//! .map_err(ToString::to_string)
|
||||
//! .map_err(bdk::Error::Generic)?;
|
||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
|
||||
//!
|
||||
//! println!("Exported: {}", export.to_string());
|
||||
//! # Ok::<_, bdk::Error>(())
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
|
||||
use core::str::FromStr;
|
||||
|
@ -5,12 +5,12 @@ edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/LLFourn/bdk_core_staging"
|
||||
documentation = "https://docs.rs/bdk_electrum"
|
||||
description = "BDK Electrum client library for updating the keychain tracker."
|
||||
description = "Fetch data from electrum in the form BDK accepts"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../bdk_chain", version = "0.3", features = ["serde", "miniscript"] }
|
||||
bdk_chain = { path = "../chain", version = "0.3", features = ["serde", "miniscript"] }
|
||||
electrum-client = { version = "0.12" }
|
||||
|
16
crates/esplora/Cargo.toml
Normal file
16
crates/esplora/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/LLFourn/bdk_core_staging"
|
||||
documentation = "https://docs.rs/bdk_esplora"
|
||||
description = "Fetch data from esplora in the form that accepts"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.3", features = ["serde", "miniscript"] }
|
||||
esplora-client = { version = "0.3" }
|
3
crates/esplora/README.md
Normal file
3
crates/esplora/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# BDK Esplora
|
||||
|
||||
BDK Esplora client library for updating the `bdk_chain` structures.
|
303
crates/esplora/src/lib.rs
Normal file
303
crates/esplora/src/lib.rs
Normal file
@ -0,0 +1,303 @@
|
||||
//! This crate is used for updating structures of [`bdk_chain`] with data from an esplora server.
|
||||
//!
|
||||
//! The star of the show is the [`EsploraExt::scan`] method which scans for relevant
|
||||
//! blockchain data (via esplora) and outputs a [`KeychainScan`].
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, Script, Txid},
|
||||
chain_graph::ChainGraph,
|
||||
keychain::KeychainScan,
|
||||
sparse_chain, BlockId, ConfirmationTime,
|
||||
};
|
||||
use esplora_client::{OutputStatus, TxStatus};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub use esplora_client;
|
||||
use esplora_client::Error;
|
||||
|
||||
/// Trait to extend [`esplora_client::BlockingClient`] functionality.
|
||||
///
|
||||
/// Refer to [crate-level documentation] for more.
|
||||
///
|
||||
/// [crate-level documentation]: crate
|
||||
pub trait EsploraExt {
|
||||
/// Scan the blockchain (via esplora) for the data specified and returns a [`KeychainScan`].
|
||||
///
|
||||
/// - `local_chain`: the most recent block hashes present locally
|
||||
/// - `keychain_spks`: keychains that we want to scan transactions for
|
||||
/// - `txids`: transactions that we want updated [`ChainPosition`]s for
|
||||
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to included in the update
|
||||
///
|
||||
/// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
///
|
||||
/// [`ChainPosition`]: bdk_chain::sparse_chain::ChainPosition
|
||||
fn scan<K: Ord + Clone>(
|
||||
&self,
|
||||
local_chain: &BTreeMap<u32, BlockHash>,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<KeychainScan<K, ConfirmationTime>, Error>;
|
||||
|
||||
/// Convenience method to call [`scan`] without requiring a keychain.
|
||||
///
|
||||
/// [`scan`]: EsploraExt::scan
|
||||
fn scan_without_keychain(
|
||||
&self,
|
||||
local_chain: &BTreeMap<u32, BlockHash>,
|
||||
misc_spks: impl IntoIterator<Item = Script>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
parallel_requests: usize,
|
||||
) -> Result<ChainGraph<ConfirmationTime>, Error> {
|
||||
let wallet_scan = self.scan(
|
||||
local_chain,
|
||||
[(
|
||||
(),
|
||||
misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk)),
|
||||
)]
|
||||
.into(),
|
||||
txids,
|
||||
outpoints,
|
||||
usize::MAX,
|
||||
parallel_requests,
|
||||
)?;
|
||||
|
||||
Ok(wallet_scan.update)
|
||||
}
|
||||
}
|
||||
|
||||
impl EsploraExt for esplora_client::BlockingClient {
|
||||
fn scan<K: Ord + Clone>(
|
||||
&self,
|
||||
local_chain: &BTreeMap<u32, BlockHash>,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, Script)>>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<KeychainScan<K, ConfirmationTime>, Error> {
|
||||
let parallel_requests = parallel_requests.max(1);
|
||||
let mut scan = KeychainScan::default();
|
||||
let update = &mut scan.update;
|
||||
let last_active_indices = &mut scan.last_active_indices;
|
||||
|
||||
for (&height, &original_hash) in local_chain.iter().rev() {
|
||||
let update_block_id = BlockId {
|
||||
height,
|
||||
hash: self.get_block_hash(height)?,
|
||||
};
|
||||
let _ = update
|
||||
.insert_checkpoint(update_block_id)
|
||||
.expect("cannot repeat height here");
|
||||
if update_block_id.hash == original_hash {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let tip_at_start = BlockId {
|
||||
height: self.get_height()?,
|
||||
hash: self.get_tip_hash()?,
|
||||
};
|
||||
if let Err(failure) = update.insert_checkpoint(tip_at_start) {
|
||||
match failure {
|
||||
sparse_chain::InsertCheckpointError::HashNotMatching { .. } => {
|
||||
// there has been a re-org before we started scanning. We haven't consumed any iterators so it's safe to recursively call.
|
||||
return EsploraExt::scan(
|
||||
self,
|
||||
local_chain,
|
||||
keychain_spks,
|
||||
txids,
|
||||
outpoints,
|
||||
stop_gap,
|
||||
parallel_requests,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
let mut last_active_index = None;
|
||||
let mut empty_scripts = 0;
|
||||
|
||||
loop {
|
||||
let handles = (0..parallel_requests)
|
||||
.filter_map(
|
||||
|_| -> Option<
|
||||
std::thread::JoinHandle<Result<(u32, Vec<esplora_client::Tx>), _>>,
|
||||
> {
|
||||
let (index, script) = spks.next()?;
|
||||
let client = self.clone();
|
||||
Some(std::thread::spawn(move || {
|
||||
let mut related_txs = client.scripthash_txs(&script, None)?;
|
||||
|
||||
let n_confirmed =
|
||||
related_txs.iter().filter(|tx| tx.status.confirmed).count();
|
||||
// esplora pages on 25 confirmed transactions. If there's 25 or more we
|
||||
// keep requesting to see if there's more.
|
||||
if n_confirmed >= 25 {
|
||||
loop {
|
||||
let new_related_txs = client.scripthash_txs(
|
||||
&script,
|
||||
Some(related_txs.last().unwrap().txid),
|
||||
)?;
|
||||
let n = new_related_txs.len();
|
||||
related_txs.extend(new_related_txs);
|
||||
// we've reached the end
|
||||
if n < 25 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result::<_, esplora_client::Error>::Ok((index, related_txs))
|
||||
}))
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let n_handles = handles.len();
|
||||
|
||||
for handle in handles {
|
||||
let (index, related_txs) = handle.join().unwrap()?; // TODO: don't unwrap
|
||||
if related_txs.is_empty() {
|
||||
empty_scripts += 1;
|
||||
} else {
|
||||
last_active_index = Some(index);
|
||||
empty_scripts = 0;
|
||||
}
|
||||
for tx in related_txs {
|
||||
let confirmation_time =
|
||||
map_confirmation_time(&tx.status, tip_at_start.height);
|
||||
|
||||
if let Err(failure) = update.insert_tx(tx.to_tx(), confirmation_time) {
|
||||
use bdk_chain::{
|
||||
chain_graph::InsertTxError, sparse_chain::InsertTxError::*,
|
||||
};
|
||||
match failure {
|
||||
InsertTxError::Chain(TxTooHigh { .. }) => {
|
||||
unreachable!("chain position already checked earlier")
|
||||
}
|
||||
InsertTxError::Chain(TxMovedUnexpectedly { .. })
|
||||
| InsertTxError::UnresolvableConflict(_) => {
|
||||
/* implies reorg during scan. We deal with that below */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n_handles == 0 || empty_scripts >= stop_gap {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_active_index) = last_active_index {
|
||||
last_active_indices.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
for txid in txids.into_iter() {
|
||||
let (tx, tx_status) = match (self.get_tx(&txid)?, self.get_tx_status(&txid)?) {
|
||||
(Some(tx), Some(tx_status)) => (tx, tx_status),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let confirmation_time = map_confirmation_time(&tx_status, tip_at_start.height);
|
||||
|
||||
if let Err(failure) = update.insert_tx(tx, confirmation_time) {
|
||||
use bdk_chain::{chain_graph::InsertTxError, sparse_chain::InsertTxError::*};
|
||||
match failure {
|
||||
InsertTxError::Chain(TxTooHigh { .. }) => {
|
||||
unreachable!("chain position already checked earlier")
|
||||
}
|
||||
InsertTxError::Chain(TxMovedUnexpectedly { .. })
|
||||
| InsertTxError::UnresolvableConflict(_) => {
|
||||
/* implies reorg during scan. We deal with that below */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for op in outpoints.into_iter() {
|
||||
let mut op_txs = Vec::with_capacity(2);
|
||||
if let (Some(tx), Some(tx_status)) =
|
||||
(self.get_tx(&op.txid)?, self.get_tx_status(&op.txid)?)
|
||||
{
|
||||
op_txs.push((tx, tx_status));
|
||||
if let Some(OutputStatus {
|
||||
txid: Some(txid),
|
||||
status: Some(spend_status),
|
||||
..
|
||||
}) = self.get_output_status(&op.txid, op.vout as _)?
|
||||
{
|
||||
if let Some(spend_tx) = self.get_tx(&txid)? {
|
||||
op_txs.push((spend_tx, spend_status));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (tx, status) in op_txs {
|
||||
let confirmation_time = map_confirmation_time(&status, tip_at_start.height);
|
||||
|
||||
if let Err(failure) = update.insert_tx(tx, confirmation_time) {
|
||||
use bdk_chain::{chain_graph::InsertTxError, sparse_chain::InsertTxError::*};
|
||||
match failure {
|
||||
InsertTxError::Chain(TxTooHigh { .. }) => {
|
||||
unreachable!("chain position already checked earlier")
|
||||
}
|
||||
InsertTxError::Chain(TxMovedUnexpectedly { .. })
|
||||
| InsertTxError::UnresolvableConflict(_) => {
|
||||
/* implies reorg during scan. We deal with that below */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reorg_occurred = {
|
||||
if let Some(checkpoint) = update.chain().latest_checkpoint() {
|
||||
self.get_block_hash(checkpoint.height)? != checkpoint.hash
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if reorg_occurred {
|
||||
// A reorg occurred so lets find out where all the txids we found are in the chain now.
|
||||
// XXX: collect required because of weird type naming issues
|
||||
let txids_found = update
|
||||
.chain()
|
||||
.txids()
|
||||
.map(|(_, txid)| *txid)
|
||||
.collect::<Vec<_>>();
|
||||
scan.update = EsploraExt::scan_without_keychain(
|
||||
self,
|
||||
local_chain,
|
||||
[],
|
||||
txids_found,
|
||||
[],
|
||||
parallel_requests,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(scan)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_confirmation_time(tx_status: &TxStatus, height_at_start: u32) -> ConfirmationTime {
|
||||
match (tx_status.block_time, tx_status.block_height) {
|
||||
(Some(time), Some(height)) if height <= height_at_start => {
|
||||
ConfirmationTime::Confirmed { height, time }
|
||||
}
|
||||
_ => ConfirmationTime::Unconfirmed,
|
||||
}
|
||||
}
|
@ -5,6 +5,9 @@ edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../bdk_chain", version = "0.3", features = [ "serde", "miniscript" ] }
|
||||
bdk_chain = { path = "../chain", version = "0.3", features = [ "serde", "miniscript" ] }
|
||||
bincode = { version = "2.0.0-rc.2", features = [ "serde" ] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
@ -89,16 +89,16 @@ fn new_fails_if_file_is_too_short() {
|
||||
|
||||
#[test]
|
||||
fn new_fails_if_magic_bytes_are_invalid() {
|
||||
let invalid_magic_mnemonic = "ldkfs0000000";
|
||||
let invalid_magic_bytes = "ldkfs0000000";
|
||||
|
||||
let path = TempPath::new();
|
||||
path.open()
|
||||
.write_all(invalid_magic_mnemonic.as_bytes())
|
||||
.write_all(invalid_magic_bytes.as_bytes())
|
||||
.expect("should write");
|
||||
|
||||
match KeychainStore::<TestKeychain, TxHeight, Transaction>::new(path.open()) {
|
||||
Err(FileError::InvalidMagicBytes(b)) => {
|
||||
assert_eq!(b, invalid_magic_mnemonic.as_bytes())
|
||||
assert_eq!(b, invalid_magic_bytes.as_bytes())
|
||||
}
|
||||
unexpected => panic!("unexpected result: {:?}", unexpected),
|
||||
};
|
||||
|
1
example-crates/keychain_tracker_electrum_example/.gitignore
vendored
Normal file
1
example-crates/keychain_tracker_electrum_example/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "keychain_tracker_electrum_example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", version = "0.3", features = ["serde"] }
|
||||
bdk_electrum = { path = "../../crates/electrum" }
|
||||
keychain_tracker_example_cli = { path = "../keychain_tracker_example_cli"}
|
@ -0,0 +1,6 @@
|
||||
# Keychain Tracker with electrum
|
||||
|
||||
This example shows how you use the `KeychainTracker` from `bdk_chain` to create a simple command
|
||||
line wallet.
|
||||
|
||||
|
248
example-crates/keychain_tracker_electrum_example/src/main.rs
Normal file
248
example-crates/keychain_tracker_electrum_example/src/main.rs
Normal file
@ -0,0 +1,248 @@
|
||||
use bdk_chain::bitcoin::{Address, OutPoint, Txid};
|
||||
use bdk_electrum::bdk_chain::{self, bitcoin::Network, TxHeight};
|
||||
use bdk_electrum::{
|
||||
electrum_client::{self, ElectrumApi},
|
||||
ElectrumExt, ElectrumUpdate,
|
||||
};
|
||||
use keychain_tracker_example_cli::{
|
||||
self as cli,
|
||||
anyhow::{self, Context},
|
||||
clap::{self, Parser, Subcommand},
|
||||
};
|
||||
use std::{collections::BTreeMap, fmt::Debug, io, io::Write};
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum ElectrumCommands {
|
||||
/// Scans the addresses in the wallet using esplora API.
|
||||
Scan {
|
||||
/// When a gap this large has been found for a keychain it will stop.
|
||||
#[clap(long, default_value = "5")]
|
||||
stop_gap: usize,
|
||||
#[clap(flatten)]
|
||||
scan_options: ScanOptions,
|
||||
},
|
||||
/// Scans particular addresses using esplora API
|
||||
Sync {
|
||||
/// Scan all the unused addresses
|
||||
#[clap(long)]
|
||||
unused_spks: bool,
|
||||
/// Scan every address that you have derived
|
||||
#[clap(long)]
|
||||
all_spks: bool,
|
||||
/// Scan unspent outpoints for spends or changes to confirmation status of residing tx
|
||||
#[clap(long)]
|
||||
utxos: bool,
|
||||
/// Scan unconfirmed transactions for updates
|
||||
#[clap(long)]
|
||||
unconfirmed: bool,
|
||||
#[clap(flatten)]
|
||||
scan_options: ScanOptions,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone, PartialEq)]
|
||||
pub struct ScanOptions {
|
||||
/// Set batch size for each script_history call to electrum client
|
||||
#[clap(long, default_value = "25")]
|
||||
pub batch_size: usize,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, keymap, mut tracker, mut db) = cli::init::<ElectrumCommands, _>()?;
|
||||
|
||||
let electrum_url = match args.network {
|
||||
Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
|
||||
Network::Testnet => "ssl://electrum.blockstream.info:60002",
|
||||
Network::Regtest => "tcp://localhost:60401",
|
||||
Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
|
||||
};
|
||||
let config = electrum_client::Config::builder()
|
||||
.validate_domain(match args.network {
|
||||
Network::Bitcoin => true,
|
||||
_ => false,
|
||||
})
|
||||
.build();
|
||||
|
||||
let client = electrum_client::Client::from_config(electrum_url, config)?;
|
||||
|
||||
let electrum_cmd = match args.command {
|
||||
cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
||||
general_command => {
|
||||
return cli::handle_commands(
|
||||
general_command,
|
||||
|transaction| {
|
||||
let _txid = client.transaction_broadcast(transaction)?;
|
||||
Ok(())
|
||||
},
|
||||
&mut tracker,
|
||||
&mut db,
|
||||
args.network,
|
||||
&keymap,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let response = match electrum_cmd {
|
||||
ElectrumCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options: scan_option,
|
||||
} => {
|
||||
let (spk_iterators, local_chain) = {
|
||||
// Get a short lock on the tracker to get the spks iterators
|
||||
// and local chain state
|
||||
let tracker = &*tracker.lock().unwrap();
|
||||
let spk_iterators = tracker
|
||||
.txout_index
|
||||
.spks_of_all_keychains()
|
||||
.into_iter()
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
let spk_iter = iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
}
|
||||
|
||||
eprint!("{} ", i);
|
||||
let _ = io::stdout().flush();
|
||||
});
|
||||
(keychain, spk_iter)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let local_chain = tracker.chain().checkpoints().clone();
|
||||
(spk_iterators, local_chain)
|
||||
};
|
||||
|
||||
// we scan the spks **without** a lock on the tracker
|
||||
client.scan(
|
||||
&local_chain,
|
||||
spk_iterators,
|
||||
core::iter::empty(),
|
||||
core::iter::empty(),
|
||||
stop_gap,
|
||||
scan_option.batch_size,
|
||||
)?
|
||||
}
|
||||
ElectrumCommands::Sync {
|
||||
mut unused_spks,
|
||||
mut utxos,
|
||||
mut unconfirmed,
|
||||
all_spks,
|
||||
scan_options,
|
||||
} => {
|
||||
// Get a short lock on the tracker to get the spks we're interested in
|
||||
let tracker = tracker.lock().unwrap();
|
||||
|
||||
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
||||
unused_spks = true;
|
||||
unconfirmed = true;
|
||||
utxos = true;
|
||||
} else if all_spks {
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
|
||||
Box::new(core::iter::empty());
|
||||
if all_spks {
|
||||
let all_spks = tracker
|
||||
.txout_index
|
||||
.all_spks()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!("scanning {:?}", index);
|
||||
script
|
||||
})));
|
||||
}
|
||||
if unused_spks {
|
||||
let unused_spks = tracker
|
||||
.txout_index
|
||||
.unused_spks(..)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!(
|
||||
"Checking if address {} {:?} has been used",
|
||||
Address::from_script(&script, args.network).unwrap(),
|
||||
index
|
||||
);
|
||||
|
||||
script
|
||||
})));
|
||||
}
|
||||
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
|
||||
if utxos {
|
||||
let utxos = tracker
|
||||
.full_utxos()
|
||||
.map(|(_, utxo)| utxo)
|
||||
.collect::<Vec<_>>();
|
||||
outpoints = Box::new(
|
||||
utxos
|
||||
.into_iter()
|
||||
.inspect(|utxo| {
|
||||
eprintln!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
})
|
||||
.map(|utxo| utxo.outpoint),
|
||||
);
|
||||
};
|
||||
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
|
||||
if unconfirmed {
|
||||
let unconfirmed_txids = tracker
|
||||
.chain()
|
||||
.range_txids_by_height(TxHeight::Unconfirmed..)
|
||||
.map(|(_, txid)| *txid)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprintln!("Checking if {} is confirmed yet", txid);
|
||||
}));
|
||||
}
|
||||
|
||||
let local_chain = tracker.chain().checkpoints().clone();
|
||||
// drop lock on tracker
|
||||
drop(tracker);
|
||||
|
||||
// we scan the spks **without** a lock on the tracker
|
||||
ElectrumUpdate {
|
||||
chain_update: client
|
||||
.scan_without_keychain(
|
||||
&local_chain,
|
||||
spks,
|
||||
txids,
|
||||
outpoints,
|
||||
scan_options.batch_size,
|
||||
)
|
||||
.context("scanning the blockchain")?,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let missing_txids = response.missing_full_txs(&*tracker.lock().unwrap());
|
||||
|
||||
// fetch the missing full transactions **without** a lock on the tracker
|
||||
let new_txs = client
|
||||
.batch_transaction_get(missing_txids)
|
||||
.context("fetching full transactions")?;
|
||||
|
||||
{
|
||||
// Get a final short lock to apply the changes
|
||||
let mut tracker = tracker.lock().unwrap();
|
||||
let changeset = {
|
||||
let scan = response.into_keychain_scan(new_txs, &*tracker)?;
|
||||
tracker.determine_changeset(&scan)?
|
||||
};
|
||||
db.lock().unwrap().append_changeset(&changeset)?;
|
||||
tracker.apply_changeset(changeset);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
3
example-crates/keychain_tracker_esplora_example/.gitignore
vendored
Normal file
3
example-crates/keychain_tracker_esplora_example/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.bdk_example_db
|
11
example-crates/keychain_tracker_esplora_example/Cargo.toml
Normal file
11
example-crates/keychain_tracker_esplora_example/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "keychain_tracker_esplora_example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", version = "0.3", features = ["serde", "miniscript"] }
|
||||
bdk_esplora = { path = "../../crates/esplora" }
|
||||
keychain_tracker_example_cli = { path = "../keychain_tracker_example_cli" }
|
241
example-crates/keychain_tracker_esplora_example/src/main.rs
Normal file
241
example-crates/keychain_tracker_esplora_example/src/main.rs
Normal file
@ -0,0 +1,241 @@
|
||||
use bdk_chain::bitcoin::{Address, OutPoint, Txid};
|
||||
use bdk_chain::{bitcoin::Network, TxHeight};
|
||||
use bdk_esplora::esplora_client;
|
||||
use bdk_esplora::EsploraExt;
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use keychain_tracker_example_cli::{
|
||||
self as cli,
|
||||
anyhow::{self, Context},
|
||||
clap::{self, Parser, Subcommand},
|
||||
};
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum EsploraCommands {
|
||||
/// Scans the addresses in the wallet using esplora API.
|
||||
Scan {
|
||||
/// When a gap this large has been found for a keychain it will stop.
|
||||
#[clap(long, default_value = "5")]
|
||||
stop_gap: usize,
|
||||
|
||||
#[clap(flatten)]
|
||||
scan_options: ScanOptions,
|
||||
},
|
||||
/// Scans particular addresses using esplora API
|
||||
Sync {
|
||||
/// Scan all the unused addresses
|
||||
#[clap(long)]
|
||||
unused_spks: bool,
|
||||
/// Scan every address that you have derived
|
||||
#[clap(long)]
|
||||
all_spks: bool,
|
||||
/// Scan unspent outpoints for spends or changes to confirmation status of residing tx
|
||||
#[clap(long)]
|
||||
utxos: bool,
|
||||
/// Scan unconfirmed transactions for updates
|
||||
#[clap(long)]
|
||||
unconfirmed: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
scan_options: ScanOptions,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone, PartialEq)]
|
||||
pub struct ScanOptions {
|
||||
#[clap(long, default_value = "5")]
|
||||
pub parallel_requests: usize,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, keymap, keychain_tracker, db) = cli::init::<EsploraCommands, _>()?;
|
||||
let esplora_url = match args.network {
|
||||
Network::Bitcoin => "https://mempool.space/api",
|
||||
Network::Testnet => "https://mempool.space/testnet/api",
|
||||
Network::Regtest => "http://localhost:3002",
|
||||
Network::Signet => "https://mempool.space/signet/api",
|
||||
};
|
||||
|
||||
let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
|
||||
|
||||
let esplora_cmd = match args.command {
|
||||
cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
|
||||
general_command => {
|
||||
return cli::handle_commands(
|
||||
general_command,
|
||||
|transaction| Ok(client.broadcast(transaction)?),
|
||||
&keychain_tracker,
|
||||
&db,
|
||||
args.network,
|
||||
&keymap,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match esplora_cmd {
|
||||
EsploraCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options,
|
||||
} => {
|
||||
let (spk_iterators, local_chain) = {
|
||||
// Get a short lock on the tracker to get the spks iterators
|
||||
// and local chain state
|
||||
let tracker = &*keychain_tracker.lock().unwrap();
|
||||
let spk_iterators = tracker
|
||||
.txout_index
|
||||
.spks_of_all_keychains()
|
||||
.into_iter()
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
(
|
||||
keychain,
|
||||
iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
}
|
||||
|
||||
eprint!("{} ", i);
|
||||
let _ = io::stdout().flush();
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let local_chain = tracker.chain().checkpoints().clone();
|
||||
(spk_iterators, local_chain)
|
||||
};
|
||||
|
||||
// we scan the iterators **without** a lock on the tracker
|
||||
let wallet_scan = client
|
||||
.scan(
|
||||
&local_chain,
|
||||
spk_iterators,
|
||||
core::iter::empty(),
|
||||
core::iter::empty(),
|
||||
stop_gap,
|
||||
scan_options.parallel_requests,
|
||||
)
|
||||
.context("scanning the blockchain")?;
|
||||
eprintln!();
|
||||
|
||||
{
|
||||
// we take a short lock to apply results to tracker and db
|
||||
let tracker = &mut *keychain_tracker.lock().unwrap();
|
||||
let db = &mut *db.lock().unwrap();
|
||||
let changeset = tracker.apply_update(wallet_scan)?;
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
}
|
||||
EsploraCommands::Sync {
|
||||
mut unused_spks,
|
||||
mut utxos,
|
||||
mut unconfirmed,
|
||||
all_spks,
|
||||
scan_options,
|
||||
} => {
|
||||
// Get a short lock on the tracker to get the spks we're interested in
|
||||
let tracker = keychain_tracker.lock().unwrap();
|
||||
|
||||
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
||||
unused_spks = true;
|
||||
unconfirmed = true;
|
||||
utxos = true;
|
||||
} else if all_spks {
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
|
||||
Box::new(core::iter::empty());
|
||||
if all_spks {
|
||||
let all_spks = tracker
|
||||
.txout_index
|
||||
.all_spks()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!("scanning {:?}", index);
|
||||
script
|
||||
})));
|
||||
}
|
||||
if unused_spks {
|
||||
let unused_spks = tracker
|
||||
.txout_index
|
||||
.unused_spks(..)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
|
||||
eprintln!(
|
||||
"Checking if address {} {:?} has been used",
|
||||
Address::from_script(&script, args.network).unwrap(),
|
||||
index
|
||||
);
|
||||
|
||||
script
|
||||
})));
|
||||
}
|
||||
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
|
||||
if utxos {
|
||||
let utxos = tracker
|
||||
.full_utxos()
|
||||
.map(|(_, utxo)| utxo)
|
||||
.collect::<Vec<_>>();
|
||||
outpoints = Box::new(
|
||||
utxos
|
||||
.into_iter()
|
||||
.inspect(|utxo| {
|
||||
eprintln!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
})
|
||||
.map(|utxo| utxo.outpoint),
|
||||
);
|
||||
};
|
||||
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
|
||||
if unconfirmed {
|
||||
let unconfirmed_txids = tracker
|
||||
.chain()
|
||||
.range_txids_by_height(TxHeight::Unconfirmed..)
|
||||
.map(|(_, txid)| *txid)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprintln!("Checking if {} is confirmed yet", txid);
|
||||
}));
|
||||
}
|
||||
|
||||
let local_chain = tracker.chain().checkpoints().clone();
|
||||
|
||||
// drop lock on tracker
|
||||
drop(tracker);
|
||||
|
||||
// we scan the desired spks **without** a lock on the tracker
|
||||
let scan = client
|
||||
.scan_without_keychain(
|
||||
&local_chain,
|
||||
spks,
|
||||
txids,
|
||||
outpoints,
|
||||
scan_options.parallel_requests,
|
||||
)
|
||||
.context("scanning the blockchain")?;
|
||||
|
||||
{
|
||||
// we take a short lock to apply the results to the tracker and db
|
||||
let tracker = &mut *keychain_tracker.lock().unwrap();
|
||||
let changeset = tracker.apply_update(scan.into())?;
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
1
example-crates/keychain_tracker_example_cli/.gitignore
vendored
Normal file
1
example-crates/keychain_tracker_example_cli/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
16
example-crates/keychain_tracker_example_cli/Cargo.toml
Normal file
16
example-crates/keychain_tracker_example_cli/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "keychain_tracker_example_cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", version = "0.3", features = ["serde", "miniscript"]}
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_tmp_plan = { path = "../../nursery/tmp_plan" }
|
||||
bdk_coin_select = { path = "../../nursery/coin_select" }
|
||||
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
1
example-crates/keychain_tracker_example_cli/README.md
Normal file
1
example-crates/keychain_tracker_example_cli/README.md
Normal file
@ -0,0 +1 @@
|
||||
Provides common command line processing logic between examples using the `KeychainTracker`
|
688
example-crates/keychain_tracker_example_cli/src/lib.rs
Normal file
688
example-crates/keychain_tracker_example_cli/src/lib.rs
Normal file
@ -0,0 +1,688 @@
|
||||
pub extern crate anyhow;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bdk_chain::{
|
||||
bitcoin::{
|
||||
secp256k1::Secp256k1,
|
||||
util::sighash::{Prevouts, SighashCache},
|
||||
Address, LockTime, Network, Sequence, Transaction, TxIn, TxOut,
|
||||
},
|
||||
chain_graph::InsertTxError,
|
||||
keychain::{DerivationAdditions, KeychainChangeSet, KeychainTracker},
|
||||
miniscript::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
Descriptor, DescriptorPublicKey,
|
||||
},
|
||||
sparse_chain::{self, ChainPosition},
|
||||
DescriptorExt, FullTxOut,
|
||||
};
|
||||
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
|
||||
use bdk_file_store::KeychainStore;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::{
|
||||
cmp::Reverse, collections::HashMap, fmt::Debug, path::PathBuf, sync::Mutex, time::Duration,
|
||||
};
|
||||
|
||||
pub use bdk_file_store;
|
||||
pub use clap;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
#[clap(propagate_version = true)]
|
||||
pub struct Args<C: clap::Subcommand> {
|
||||
#[clap(env = "DESCRIPTOR")]
|
||||
pub descriptor: String,
|
||||
#[clap(env = "CHANGE_DESCRIPTOR")]
|
||||
pub change_descriptor: Option<String>,
|
||||
|
||||
#[clap(env = "BITCOIN_NETWORK", long, default_value = "signet")]
|
||||
pub network: Network,
|
||||
|
||||
#[clap(env = "BDK_DB_PATH", long, default_value = ".bdk_example_db")]
|
||||
pub db_path: PathBuf,
|
||||
|
||||
#[clap(env = "BDK_CP_LIMIT", long, default_value = "20")]
|
||||
pub cp_limit: usize,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub command: Commands<C>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum Commands<C: clap::Subcommand> {
|
||||
#[clap(flatten)]
|
||||
ChainSpecific(C),
|
||||
/// Address generation and inspection
|
||||
Address {
|
||||
#[clap(subcommand)]
|
||||
addr_cmd: AddressCmd,
|
||||
},
|
||||
/// Get the wallet balance
|
||||
Balance,
|
||||
/// TxOut related commands
|
||||
#[clap(name = "txout")]
|
||||
TxOut {
|
||||
#[clap(subcommand)]
|
||||
txout_cmd: TxOutCmd,
|
||||
},
|
||||
/// Send coins to an address
|
||||
Send {
|
||||
value: u64,
|
||||
address: Address,
|
||||
#[clap(short, default_value = "largest-first")]
|
||||
coin_select: CoinSelectionAlgo,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CoinSelectionAlgo {
|
||||
LargestFirst,
|
||||
SmallestFirst,
|
||||
OldestFirst,
|
||||
NewestFirst,
|
||||
BranchAndBound,
|
||||
}
|
||||
|
||||
impl Default for CoinSelectionAlgo {
|
||||
fn default() -> Self {
|
||||
Self::LargestFirst
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for CoinSelectionAlgo {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use CoinSelectionAlgo::*;
|
||||
Ok(match s {
|
||||
"largest-first" => LargestFirst,
|
||||
"smallest-first" => SmallestFirst,
|
||||
"oldest-first" => OldestFirst,
|
||||
"newest-first" => NewestFirst,
|
||||
"bnb" => BranchAndBound,
|
||||
unknown => return Err(anyhow!("unknown coin selection algorithm '{}'", unknown)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CoinSelectionAlgo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use CoinSelectionAlgo::*;
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
LargestFirst => "largest-first",
|
||||
SmallestFirst => "smallest-first",
|
||||
OldestFirst => "oldest-first",
|
||||
NewestFirst => "newest-first",
|
||||
BranchAndBound => "bnb",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum AddressCmd {
|
||||
/// Get the next unused address
|
||||
Next,
|
||||
/// Get a new address regardless if the existing ones haven't been used
|
||||
New,
|
||||
/// List all addresses
|
||||
List {
|
||||
#[clap(long)]
|
||||
change: bool,
|
||||
},
|
||||
Index,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum TxOutCmd {
|
||||
List {
|
||||
/// Return only spent outputs
|
||||
#[clap(short, long)]
|
||||
spent: bool,
|
||||
/// Return only unspent outputs
|
||||
#[clap(short, long)]
|
||||
unspent: bool,
|
||||
/// Return only confirmed outputs
|
||||
#[clap(long)]
|
||||
confirmed: bool,
|
||||
/// Return only unconfirmed outputs
|
||||
#[clap(long)]
|
||||
unconfirmed: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
|
||||
)]
|
||||
pub enum Keychain {
|
||||
External,
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Keychain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Keychain::External => write!(f, "external"),
|
||||
Keychain::Internal => write!(f, "internal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure defining output of a AddressCmd execution.
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct AddrsOutput {
|
||||
keychain: String,
|
||||
index: u32,
|
||||
addrs: Address,
|
||||
used: bool,
|
||||
}
|
||||
|
||||
pub fn run_address_cmd<P>(
|
||||
tracker: &Mutex<KeychainTracker<Keychain, P>>,
|
||||
db: &Mutex<KeychainStore<Keychain, P>>,
|
||||
addr_cmd: AddressCmd,
|
||||
network: Network,
|
||||
) -> Result<()>
|
||||
where
|
||||
P: bdk_chain::sparse_chain::ChainPosition,
|
||||
KeychainChangeSet<Keychain, P>: serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
let mut tracker = tracker.lock().unwrap();
|
||||
let txout_index = &mut tracker.txout_index;
|
||||
|
||||
let addr_cmmd_output = match addr_cmd {
|
||||
AddressCmd::Next => Some(txout_index.next_unused_spk(&Keychain::External)),
|
||||
AddressCmd::New => Some(txout_index.reveal_next_spk(&Keychain::External)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(((index, spk), additions)) = addr_cmmd_output {
|
||||
let mut db = db.lock().unwrap();
|
||||
// update database since we're about to give out a new address
|
||||
db.append_changeset(&additions.into())?;
|
||||
|
||||
let spk = spk.clone();
|
||||
let address =
|
||||
Address::from_script(&spk, network).expect("should always be able to derive address");
|
||||
eprintln!("This is the address at index {}", index);
|
||||
println!("{}", address);
|
||||
}
|
||||
|
||||
match addr_cmd {
|
||||
AddressCmd::Next | AddressCmd::New => {
|
||||
/* covered */
|
||||
Ok(())
|
||||
}
|
||||
AddressCmd::Index => {
|
||||
for (keychain, derivation_index) in txout_index.last_revealed_indices() {
|
||||
println!("{:?}: {}", keychain, derivation_index);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
AddressCmd::List { change } => {
|
||||
let target_keychain = match change {
|
||||
true => Keychain::Internal,
|
||||
false => Keychain::External,
|
||||
};
|
||||
for (index, spk) in txout_index.revealed_spks_of_keychain(&target_keychain) {
|
||||
let address = Address::from_script(&spk, network)
|
||||
.expect("should always be able to derive address");
|
||||
println!(
|
||||
"{:?} {} used:{}",
|
||||
index,
|
||||
address,
|
||||
txout_index.is_used(&(target_keychain, index))
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_balance_cmd<P: ChainPosition>(tracker: &Mutex<KeychainTracker<Keychain, P>>) {
|
||||
let tracker = tracker.lock().unwrap();
|
||||
let (confirmed, unconfirmed) =
|
||||
tracker
|
||||
.full_utxos()
|
||||
.fold((0, 0), |(confirmed, unconfirmed), (_, utxo)| {
|
||||
if utxo.chain_position.height().is_confirmed() {
|
||||
(confirmed + utxo.txout.value, unconfirmed)
|
||||
} else {
|
||||
(confirmed, unconfirmed + utxo.txout.value)
|
||||
}
|
||||
});
|
||||
|
||||
println!("confirmed: {}", confirmed);
|
||||
println!("unconfirmed: {}", unconfirmed);
|
||||
}
|
||||
|
||||
pub fn run_txo_cmd<K: Debug + Clone + Ord, P: ChainPosition>(
|
||||
txout_cmd: TxOutCmd,
|
||||
tracker: &Mutex<KeychainTracker<K, P>>,
|
||||
network: Network,
|
||||
) {
|
||||
match txout_cmd {
|
||||
TxOutCmd::List {
|
||||
unspent,
|
||||
spent,
|
||||
confirmed,
|
||||
unconfirmed,
|
||||
} => {
|
||||
let tracker = tracker.lock().unwrap();
|
||||
let txouts: Box<dyn Iterator<Item = (&(K, u32), FullTxOut<P>)>> = match (unspent, spent)
|
||||
{
|
||||
(true, false) => Box::new(tracker.full_utxos()),
|
||||
(false, true) => Box::new(
|
||||
tracker
|
||||
.full_txouts()
|
||||
.filter(|(_, txout)| txout.spent_by.is_some()),
|
||||
),
|
||||
_ => Box::new(tracker.full_txouts()),
|
||||
};
|
||||
|
||||
let txouts: Box<dyn Iterator<Item = (&(K, u32), FullTxOut<P>)>> =
|
||||
match (confirmed, unconfirmed) {
|
||||
(true, false) => Box::new(
|
||||
txouts.filter(|(_, txout)| txout.chain_position.height().is_confirmed()),
|
||||
),
|
||||
(false, true) => Box::new(
|
||||
txouts.filter(|(_, txout)| !txout.chain_position.height().is_confirmed()),
|
||||
),
|
||||
_ => txouts,
|
||||
};
|
||||
|
||||
for (spk_index, full_txout) in txouts {
|
||||
let address =
|
||||
Address::from_script(&full_txout.txout.script_pubkey, network).unwrap();
|
||||
|
||||
println!(
|
||||
"{:?} {} {} {} spent:{:?}",
|
||||
spk_index,
|
||||
full_txout.txout.value,
|
||||
full_txout.outpoint,
|
||||
address,
|
||||
full_txout.spent_by
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_tx<P: ChainPosition>(
|
||||
value: u64,
|
||||
address: Address,
|
||||
coin_select: CoinSelectionAlgo,
|
||||
keychain_tracker: &mut KeychainTracker<Keychain, P>,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
) -> Result<(
|
||||
Transaction,
|
||||
Option<(DerivationAdditions<Keychain>, (Keychain, u32))>,
|
||||
)> {
|
||||
let mut additions = DerivationAdditions::default();
|
||||
|
||||
let assets = bdk_tmp_plan::Assets {
|
||||
keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// TODO use planning module
|
||||
let mut candidates = planned_utxos(keychain_tracker, &assets).collect::<Vec<_>>();
|
||||
|
||||
// apply coin selection algorithm
|
||||
match coin_select {
|
||||
CoinSelectionAlgo::LargestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value))
|
||||
}
|
||||
CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value),
|
||||
CoinSelectionAlgo::OldestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone())
|
||||
}
|
||||
CoinSelectionAlgo::NewestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone()))
|
||||
}
|
||||
CoinSelectionAlgo::BranchAndBound => {}
|
||||
}
|
||||
|
||||
// turn the txos we chose into a weight and value
|
||||
let wv_candidates = candidates
|
||||
.iter()
|
||||
.map(|(plan, utxo)| {
|
||||
WeightedValue::new(
|
||||
utxo.txout.value,
|
||||
plan.expected_weight() as _,
|
||||
plan.witness_version().is_some(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut outputs = vec![TxOut {
|
||||
value,
|
||||
script_pubkey: address.script_pubkey(),
|
||||
}];
|
||||
|
||||
let internal_keychain = if keychain_tracker
|
||||
.txout_index
|
||||
.keychains()
|
||||
.get(&Keychain::Internal)
|
||||
.is_some()
|
||||
{
|
||||
Keychain::Internal
|
||||
} else {
|
||||
Keychain::External
|
||||
};
|
||||
|
||||
let ((change_index, change_script), change_additions) = keychain_tracker
|
||||
.txout_index
|
||||
.next_unused_spk(&internal_keychain);
|
||||
additions.append(change_additions);
|
||||
|
||||
// Clone to drop the immutable reference.
|
||||
let change_script = change_script.clone();
|
||||
|
||||
let change_plan = bdk_tmp_plan::plan_satisfaction(
|
||||
&keychain_tracker
|
||||
.txout_index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.at_derivation_index(change_index),
|
||||
&assets,
|
||||
)
|
||||
.expect("failed to obtain change plan");
|
||||
|
||||
let mut change_output = TxOut {
|
||||
value: 0,
|
||||
script_pubkey: change_script,
|
||||
};
|
||||
|
||||
let cs_opts = CoinSelectorOpt {
|
||||
target_feerate: 0.5,
|
||||
min_drain_value: keychain_tracker
|
||||
.txout_index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.dust_value(),
|
||||
..CoinSelectorOpt::fund_outputs(
|
||||
&outputs,
|
||||
&change_output,
|
||||
change_plan.expected_weight() as u32,
|
||||
)
|
||||
};
|
||||
|
||||
// TODO: How can we make it easy to shuffle in order of inputs and outputs here?
|
||||
// apply coin selection by saying we need to fund these outputs
|
||||
let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts);
|
||||
|
||||
// just select coins in the order provided until we have enough
|
||||
// only use first result (least waste)
|
||||
let selection = match coin_select {
|
||||
CoinSelectionAlgo::BranchAndBound => {
|
||||
coin_select_bnb(Duration::from_secs(10), coin_selector.clone())
|
||||
.map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())?
|
||||
}
|
||||
_ => coin_selector.select_until_finished()?,
|
||||
};
|
||||
let (_, selection_meta) = selection.best_strategy();
|
||||
|
||||
// get the selected utxos
|
||||
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
|
||||
|
||||
if let Some(drain_value) = selection_meta.drain_value {
|
||||
change_output.value = drain_value;
|
||||
// if the selection tells us to use change and the change value is sufficient we add it as an output
|
||||
outputs.push(change_output)
|
||||
}
|
||||
|
||||
let mut transaction = Transaction {
|
||||
version: 0x02,
|
||||
lock_time: keychain_tracker
|
||||
.chain()
|
||||
.latest_checkpoint()
|
||||
.and_then(|block_id| LockTime::from_height(block_id.height).ok())
|
||||
.unwrap_or(LockTime::ZERO)
|
||||
.into(),
|
||||
input: selected_txos
|
||||
.iter()
|
||||
.map(|(_, utxo)| TxIn {
|
||||
previous_output: utxo.outpoint,
|
||||
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
output: outputs,
|
||||
};
|
||||
|
||||
let prevouts = selected_txos
|
||||
.iter()
|
||||
.map(|(_, utxo)| utxo.txout.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let sighash_prevouts = Prevouts::All(&prevouts);
|
||||
|
||||
// first set tx values for plan so that we don't change them while signing
|
||||
for (i, (plan, _)) in selected_txos.iter().enumerate() {
|
||||
if let Some(sequence) = plan.required_sequence() {
|
||||
transaction.input[i].sequence = sequence
|
||||
}
|
||||
}
|
||||
|
||||
// create a short lived transaction
|
||||
let _sighash_tx = transaction.clone();
|
||||
let mut sighash_cache = SighashCache::new(&_sighash_tx);
|
||||
|
||||
for (i, (plan, _)) in selected_txos.iter().enumerate() {
|
||||
let requirements = plan.requirements();
|
||||
let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default();
|
||||
assert!(
|
||||
!requirements.requires_hash_preimages(),
|
||||
"can't have hash pre-images since we didn't provide any"
|
||||
);
|
||||
assert!(
|
||||
requirements.signatures.sign_with_keymap(
|
||||
i,
|
||||
&keymap,
|
||||
&sighash_prevouts,
|
||||
None,
|
||||
None,
|
||||
&mut sighash_cache,
|
||||
&mut auth_data,
|
||||
&Secp256k1::default(),
|
||||
)?,
|
||||
"we should have signed with this input"
|
||||
);
|
||||
|
||||
match plan.try_complete(&auth_data) {
|
||||
bdk_tmp_plan::PlanState::Complete {
|
||||
final_script_sig,
|
||||
final_script_witness,
|
||||
} => {
|
||||
if let Some(witness) = final_script_witness {
|
||||
transaction.input[i].witness = witness;
|
||||
}
|
||||
|
||||
if let Some(script_sig) = final_script_sig {
|
||||
transaction.input[i].script_sig = script_sig;
|
||||
}
|
||||
}
|
||||
bdk_tmp_plan::PlanState::Incomplete(_) => {
|
||||
return Err(anyhow!(
|
||||
"we weren't able to complete the plan with our keys"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let change_info = if selection_meta.drain_value.is_some() {
|
||||
Some((additions, (internal_keychain, change_index)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((transaction, change_info))
|
||||
}
|
||||
|
||||
pub fn handle_commands<C: clap::Subcommand, P>(
|
||||
command: Commands<C>,
|
||||
broadcast: impl FnOnce(&Transaction) -> Result<()>,
|
||||
// we Mutexes around these not because we need them for a simple CLI app but to demonsrate how
|
||||
// all the stuff we're doing can be thread safe and also not keep locks up over an IO bound.
|
||||
tracker: &Mutex<KeychainTracker<Keychain, P>>,
|
||||
store: &Mutex<KeychainStore<Keychain, P>>,
|
||||
network: Network,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
) -> Result<()>
|
||||
where
|
||||
P: ChainPosition,
|
||||
KeychainChangeSet<Keychain, P>: serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
match command {
|
||||
// TODO: Make these functions return stuffs
|
||||
Commands::Address { addr_cmd } => run_address_cmd(&tracker, &store, addr_cmd, network),
|
||||
Commands::Balance => {
|
||||
run_balance_cmd(&tracker);
|
||||
Ok(())
|
||||
}
|
||||
Commands::TxOut { txout_cmd } => {
|
||||
run_txo_cmd(txout_cmd, &tracker, network);
|
||||
Ok(())
|
||||
}
|
||||
Commands::Send {
|
||||
value,
|
||||
address,
|
||||
coin_select,
|
||||
} => {
|
||||
let (transaction, change_index) = {
|
||||
// take mutable ref to construct tx -- it is only open for a short time while building it.
|
||||
let tracker = &mut *tracker.lock().unwrap();
|
||||
let (transaction, change_info) =
|
||||
create_tx(value, address, coin_select, tracker, &keymap)?;
|
||||
|
||||
if let Some((change_derivation_changes, (change_keychain, index))) = change_info {
|
||||
// We must first persist to disk the fact that we've got a new address from the
|
||||
// change keychain so future scans will find the tx we're about to broadcast.
|
||||
// If we're unable to persist this then we don't want to broadcast.
|
||||
let store = &mut *store.lock().unwrap();
|
||||
store.append_changeset(&change_derivation_changes.into())?;
|
||||
|
||||
// We don't want other callers/threads to use this address while we're using it
|
||||
// but we also don't want to scan the tx we just created because it's not
|
||||
// technically in the blockchain yet.
|
||||
tracker.txout_index.mark_used(&change_keychain, index);
|
||||
(transaction, Some((change_keychain, index)))
|
||||
} else {
|
||||
(transaction, None)
|
||||
}
|
||||
};
|
||||
|
||||
match (broadcast)(&transaction) {
|
||||
Ok(_) => {
|
||||
println!("Broadcasted Tx : {}", transaction.txid());
|
||||
let mut tracker = tracker.lock().unwrap();
|
||||
match tracker.insert_tx(transaction.clone(), P::unconfirmed()) {
|
||||
Ok(changeset) => {
|
||||
let store = &mut *store.lock().unwrap();
|
||||
// We know the tx is at least unconfirmed now. Note if persisting here
|
||||
// fails it's not a big deal since we can always find it again form
|
||||
// blockchain.
|
||||
store.append_changeset(&changeset)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => match e {
|
||||
InsertTxError::Chain(e) => match e {
|
||||
// TODO: add insert_unconfirmed_tx to chain graph and sparse chain
|
||||
sparse_chain::InsertTxError::TxTooHigh { .. } => unreachable!("we are inserting at unconfirmed position"),
|
||||
sparse_chain::InsertTxError::TxMovedUnexpectedly { txid, original_pos, ..} => Err(anyhow!("the tx we created {} has already been confirmed at block {:?}", txid, original_pos)),
|
||||
},
|
||||
InsertTxError::UnresolvableConflict(e) => Err(e).context("another tx that conflicts with the one we tried to create has been confirmed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let tracker = &mut *tracker.lock().unwrap();
|
||||
if let Some((keychain, index)) = change_index {
|
||||
// We failed to broadcast so allow our change address to be used in the future
|
||||
tracker.txout_index.unmark_used(&keychain, index);
|
||||
}
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::ChainSpecific(_) => {
|
||||
todo!("example code is meant to handle this!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<C: clap::Subcommand, P>() -> anyhow::Result<(
|
||||
Args<C>,
|
||||
KeyMap,
|
||||
// These don't need to have mutexes around them but we want the cli example code to make it obvious how they
|
||||
// are thread safe so this forces the example developer to show where they would lock and unlock things.
|
||||
Mutex<KeychainTracker<Keychain, P>>,
|
||||
Mutex<KeychainStore<Keychain, P>>,
|
||||
)>
|
||||
where
|
||||
P: sparse_chain::ChainPosition,
|
||||
KeychainChangeSet<Keychain, P>: serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
let args = Args::<C>::parse();
|
||||
let secp = Secp256k1::default();
|
||||
let (descriptor, mut keymap) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
|
||||
|
||||
let mut tracker = KeychainTracker::default();
|
||||
tracker.set_checkpoint_limit(Some(args.cp_limit));
|
||||
|
||||
tracker
|
||||
.txout_index
|
||||
.add_keychain(Keychain::External, descriptor);
|
||||
|
||||
let internal = args
|
||||
.change_descriptor
|
||||
.clone()
|
||||
.map(|descriptor| Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &descriptor))
|
||||
.transpose()?;
|
||||
if let Some((internal_descriptor, internal_keymap)) = internal {
|
||||
keymap.extend(internal_keymap);
|
||||
tracker
|
||||
.txout_index
|
||||
.add_keychain(Keychain::Internal, internal_descriptor);
|
||||
};
|
||||
|
||||
let mut db = KeychainStore::<Keychain, P>::new_from_path(args.db_path.as_path())?;
|
||||
|
||||
if let Err(e) = db.load_into_keychain_tracker(&mut tracker) {
|
||||
match tracker.chain().latest_checkpoint() {
|
||||
Some(checkpoint) => eprintln!("Failed to load all changesets from {}. Last checkpoint was at height {}. Error: {}", args.db_path.display(), checkpoint.height, e),
|
||||
None => eprintln!("Failed to load any checkpoints from {}: {}", args.db_path.display(), e),
|
||||
|
||||
}
|
||||
eprintln!("⚠ Consider running a rescan of chain data.");
|
||||
}
|
||||
|
||||
Ok((args, keymap, Mutex::new(tracker), Mutex::new(db)))
|
||||
}
|
||||
|
||||
pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, P: ChainPosition>(
|
||||
tracker: &'a KeychainTracker<Keychain, P>,
|
||||
assets: &'a bdk_tmp_plan::Assets<AK>,
|
||||
) -> impl Iterator<Item = (bdk_tmp_plan::Plan<AK>, FullTxOut<P>)> + 'a {
|
||||
tracker
|
||||
.full_utxos()
|
||||
.filter_map(|((keychain, derivation_index), full_txout)| {
|
||||
Some((
|
||||
bdk_tmp_plan::plan_satisfaction(
|
||||
&tracker
|
||||
.txout_index
|
||||
.keychains()
|
||||
.get(keychain)
|
||||
.expect("must exist since we have a utxo for it")
|
||||
.at_derivation_index(*derivation_index),
|
||||
assets,
|
||||
)?,
|
||||
full_txout,
|
||||
))
|
||||
})
|
||||
}
|
5
nursery/README.md
Normal file
5
nursery/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Bitcoin Dev Kit Nursery
|
||||
|
||||
This is a directory for crates that are experimental and have not been released yet.
|
||||
Keep in mind that they may never be released.
|
||||
Things in `/example-crates` may use them to demonsrate how things might look in the future.
|
11
nursery/coin_select/Cargo.toml
Normal file
11
nursery/coin_select/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "bdk_coin_select"
|
||||
version = "0.0.1"
|
||||
authors = [ "LLFourn <lloyd.fourn@gmail.com>" ]
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { version = "0.3", path = "../../crates/chain" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
651
nursery/coin_select/src/bnb.rs
Normal file
651
nursery/coin_select/src/bnb.rs
Normal file
@ -0,0 +1,651 @@
|
||||
use super::*;
|
||||
|
||||
/// Strategy in which we should branch.
|
||||
pub enum BranchStrategy {
|
||||
/// We continue exploring subtrees of this node, starting with the inclusion branch.
|
||||
Continue,
|
||||
/// We continue exploring ONY the omission branch of this node, skipping the inclusion branch.
|
||||
SkipInclusion,
|
||||
/// We skip both the inclusion and omission branches of this node.
|
||||
SkipBoth,
|
||||
}
|
||||
|
||||
impl BranchStrategy {
|
||||
pub fn will_continue(&self) -> bool {
|
||||
match self {
|
||||
Self::Continue | Self::SkipInclusion => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closure to decide the branching strategy, alongside a score (if the current selection is a
|
||||
/// candidate solution).
|
||||
pub type DecideStrategy<'c, S> = dyn Fn(&Bnb<'c, S>) -> (BranchStrategy, Option<S>);
|
||||
|
||||
/// [`Bnb`] represents the current state of the BnB algorithm.
|
||||
pub struct Bnb<'c, S> {
|
||||
pub pool: Vec<(usize, &'c WeightedValue)>,
|
||||
pub pool_pos: usize,
|
||||
pub best_score: S,
|
||||
|
||||
pub selection: CoinSelector<'c>,
|
||||
pub rem_abs: u64,
|
||||
pub rem_eff: i64,
|
||||
}
|
||||
|
||||
impl<'c, S: Ord> Bnb<'c, S> {
|
||||
/// Creates a new [`Bnb`].
|
||||
pub fn new(selector: CoinSelector<'c>, pool: Vec<(usize, &'c WeightedValue)>, max: S) -> Self {
|
||||
let (rem_abs, rem_eff) = pool.iter().fold((0, 0), |(abs, eff), (_, c)| {
|
||||
(
|
||||
abs + c.value,
|
||||
eff + c.effective_value(selector.opts.target_feerate),
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
pool,
|
||||
pool_pos: 0,
|
||||
best_score: max,
|
||||
selection: selector,
|
||||
rem_abs,
|
||||
rem_eff,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns our [`Bnb`] state into an iterator.
|
||||
///
|
||||
/// `strategy` should assess our current selection/node and determine the branching strategy and
|
||||
/// whether this selection is a candidate solution (if so, return the score of the selection).
|
||||
pub fn into_iter<'f>(self, strategy: &'f DecideStrategy<'c, S>) -> BnbIter<'c, 'f, S> {
|
||||
BnbIter {
|
||||
state: self,
|
||||
done: false,
|
||||
strategy,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to backtrack to the previously selected node's omission branch, return false
|
||||
/// otherwise (no more solutions).
|
||||
pub fn backtrack(&mut self) -> bool {
|
||||
(0..self.pool_pos)
|
||||
.rev()
|
||||
.find(|&pos| {
|
||||
let (index, candidate) = self.pool[pos];
|
||||
|
||||
if self.selection.is_selected(index) {
|
||||
// deselect last `pos`, so next round will check omission branch
|
||||
self.pool_pos = pos;
|
||||
self.selection.deselect(index);
|
||||
return true;
|
||||
} else {
|
||||
self.rem_abs += candidate.value;
|
||||
self.rem_eff += candidate.effective_value(self.selection.opts.target_feerate);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// Continue down this branch, skip inclusion branch if specified.
|
||||
pub fn forward(&mut self, skip: bool) {
|
||||
let (index, candidate) = self.pool[self.pool_pos];
|
||||
self.rem_abs -= candidate.value;
|
||||
self.rem_eff -= candidate.effective_value(self.selection.opts.target_feerate);
|
||||
|
||||
if !skip {
|
||||
self.selection.select(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare advertised score with current best. New best will be the smaller value. Return true
|
||||
/// if best is replaced.
|
||||
pub fn advertise_new_score(&mut self, score: S) -> bool {
|
||||
if score <= self.best_score {
|
||||
self.best_score = score;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BnbIter<'c, 'f, S> {
|
||||
state: Bnb<'c, S>,
|
||||
done: bool,
|
||||
|
||||
/// Check our current selection (node), and returns the branching strategy, alongside a score
|
||||
/// (if the current selection is a candidate solution).
|
||||
strategy: &'f DecideStrategy<'c, S>,
|
||||
}
|
||||
|
||||
impl<'c, 'f, S: Ord + Copy + Display> Iterator for BnbIter<'c, 'f, S> {
|
||||
type Item = Option<CoinSelector<'c>>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.done {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (strategy, score) = (self.strategy)(&self.state);
|
||||
|
||||
let mut found_best = Option::<CoinSelector>::None;
|
||||
|
||||
if let Some(score) = score {
|
||||
if self.state.advertise_new_score(score) {
|
||||
found_best = Some(self.state.selection.clone());
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
!strategy.will_continue() || self.state.pool_pos < self.state.pool.len(),
|
||||
"Faulty strategy implementation! Strategy suggested that we continue traversing, however we have already reached the end of the candidates pool! pool_len={}, pool_pos={}",
|
||||
self.state.pool.len(), self.state.pool_pos,
|
||||
);
|
||||
|
||||
match strategy {
|
||||
BranchStrategy::Continue => {
|
||||
self.state.forward(false);
|
||||
}
|
||||
BranchStrategy::SkipInclusion => {
|
||||
self.state.forward(true);
|
||||
}
|
||||
BranchStrategy::SkipBoth => {
|
||||
if !self.state.backtrack() {
|
||||
self.done = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// increment selection pool position for next round
|
||||
self.state.pool_pos += 1;
|
||||
|
||||
if found_best.is_some() || !self.done {
|
||||
Some(found_best)
|
||||
} else {
|
||||
// we have traversed all branches
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines how we should limit rounds of branch and bound.
|
||||
pub enum BnbLimit {
|
||||
Rounds(usize),
|
||||
#[cfg(feature = "std")]
|
||||
Duration(core::time::Duration),
|
||||
}
|
||||
|
||||
impl From<usize> for BnbLimit {
|
||||
fn from(v: usize) -> Self {
|
||||
Self::Rounds(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl From<core::time::Duration> for BnbLimit {
|
||||
fn from(v: core::time::Duration) -> Self {
|
||||
Self::Duration(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a variation of the Branch and Bound Coin Selection algorithm designed by Murch (as seen
|
||||
/// in Bitcoin Core).
|
||||
///
|
||||
/// The differences are as follows:
|
||||
/// * In additional to working with effective values, we also work with absolute values.
|
||||
/// This way, we can use bounds of absolute values to enforce `min_absolute_fee` (which is used by
|
||||
/// RBF), and `max_extra_target` (which can be used to increase the possible solution set, given
|
||||
/// that the sender is okay with sending extra to the receiver).
|
||||
///
|
||||
/// Murch's Master Thesis: <https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
|
||||
/// Bitcoin Core Implementation: <https://github.com/bitcoin/bitcoin/blob/23.x/src/wallet/coinselection.cpp#L65>
|
||||
///
|
||||
/// TODO: Another optimization we could do is figure out candidate with smallest waste, and
|
||||
/// if we find a result with waste equal to this, we can just break.
|
||||
pub fn coin_select_bnb<L>(limit: L, selector: CoinSelector) -> Option<CoinSelector>
|
||||
where
|
||||
L: Into<BnbLimit>,
|
||||
{
|
||||
let opts = selector.opts;
|
||||
|
||||
// prepare pool of candidates to select from:
|
||||
// * filter out candidates with negative/zero effective values
|
||||
// * sort candidates by descending effective value
|
||||
let pool = {
|
||||
let mut pool = selector
|
||||
.unselected()
|
||||
.filter(|(_, c)| c.effective_value(opts.target_feerate) > 0)
|
||||
.collect::<Vec<_>>();
|
||||
pool.sort_unstable_by(|(_, a), (_, b)| {
|
||||
let a = a.effective_value(opts.target_feerate);
|
||||
let b = b.effective_value(opts.target_feerate);
|
||||
b.cmp(&a)
|
||||
});
|
||||
pool
|
||||
};
|
||||
|
||||
let feerate_decreases = opts.target_feerate > opts.long_term_feerate();
|
||||
|
||||
let target_abs = opts.target_value.unwrap_or(0) + opts.min_absolute_fee;
|
||||
let target_eff = selector.effective_target();
|
||||
|
||||
let upper_bound_abs = target_abs + (opts.drain_weight as f32 * opts.target_feerate) as u64;
|
||||
let upper_bound_eff = target_eff + opts.drain_waste();
|
||||
|
||||
let strategy = move |bnb: &Bnb<i64>| -> (BranchStrategy, Option<i64>) {
|
||||
let selected_abs = bnb.selection.selected_absolute_value();
|
||||
let selected_eff = bnb.selection.selected_effective_value();
|
||||
|
||||
// backtrack if remaining value is not enough to reach target
|
||||
if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff {
|
||||
return (BranchStrategy::SkipBoth, None);
|
||||
}
|
||||
|
||||
// backtrack if selected value already surpassed upper bounds
|
||||
if selected_abs > upper_bound_abs && selected_eff > upper_bound_eff {
|
||||
return (BranchStrategy::SkipBoth, None);
|
||||
}
|
||||
|
||||
let selected_waste = bnb.selection.selected_waste();
|
||||
|
||||
// when feerate decreases, waste without excess is guaranteed to increase with each
|
||||
// selection. So if we have already surpassed best score, we can backtrack.
|
||||
if feerate_decreases && selected_waste > bnb.best_score {
|
||||
return (BranchStrategy::SkipBoth, None);
|
||||
}
|
||||
|
||||
// solution?
|
||||
if selected_abs >= target_abs && selected_eff >= target_eff {
|
||||
let waste = selected_waste + bnb.selection.current_excess();
|
||||
return (BranchStrategy::SkipBoth, Some(waste));
|
||||
}
|
||||
|
||||
// early bailout optimization:
|
||||
// If the candidate at the previous position is NOT selected and has the same weight and
|
||||
// value as the current candidate, we can skip selecting the current candidate.
|
||||
if bnb.pool_pos > 0 && !bnb.selection.is_empty() {
|
||||
let (_, candidate) = bnb.pool[bnb.pool_pos];
|
||||
let (prev_index, prev_candidate) = bnb.pool[bnb.pool_pos - 1];
|
||||
|
||||
if !bnb.selection.is_selected(prev_index)
|
||||
&& candidate.value == prev_candidate.value
|
||||
&& candidate.weight == prev_candidate.weight
|
||||
{
|
||||
return (BranchStrategy::SkipInclusion, None);
|
||||
}
|
||||
}
|
||||
|
||||
// check out inclusion branch first
|
||||
return (BranchStrategy::Continue, None);
|
||||
};
|
||||
|
||||
// determine sum of absolute and effective values for current selection
|
||||
let (selected_abs, selected_eff) = selector.selected().fold((0, 0), |(abs, eff), (_, c)| {
|
||||
(
|
||||
abs + c.value,
|
||||
eff + c.effective_value(selector.opts.target_feerate),
|
||||
)
|
||||
});
|
||||
|
||||
let bnb = Bnb::new(selector, pool, i64::MAX);
|
||||
|
||||
// not enough to select anyway
|
||||
if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff {
|
||||
return None;
|
||||
}
|
||||
|
||||
match limit.into() {
|
||||
BnbLimit::Rounds(rounds) => {
|
||||
bnb.into_iter(&strategy)
|
||||
.take(rounds)
|
||||
.reduce(|b, c| if c.is_some() { c } else { b })
|
||||
}
|
||||
#[cfg(feature = "std")]
|
||||
BnbLimit::Duration(duration) => {
|
||||
let start = std::time::SystemTime::now();
|
||||
bnb.into_iter(&strategy)
|
||||
.take_while(|_| start.elapsed().expect("failed to get system time") <= duration)
|
||||
.reduce(|b, c| if c.is_some() { c } else { b })
|
||||
}
|
||||
}?
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "miniscript"))]
|
||||
mod test {
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
|
||||
use crate::coin_select::{evaluate_cs::evaluate, ExcessStrategyKind};
|
||||
|
||||
use super::{
|
||||
coin_select_bnb,
|
||||
evaluate_cs::{Evaluation, EvaluationError},
|
||||
tester::Tester,
|
||||
CoinSelector, CoinSelectorOpt, Vec, WeightedValue,
|
||||
};
|
||||
|
||||
fn tester() -> Tester {
|
||||
const DESC_STR: &str = "tr(xprv9uBuvtdjghkz8D1qzsSXS9Vs64mqrUnXqzNccj2xcvnCHPpXKYE1U2Gbh9CDHk8UPyF2VuXpVkDA7fk5ZP4Hd9KnhUmTscKmhee9Dp5sBMK)";
|
||||
Tester::new(&Secp256k1::default(), DESC_STR)
|
||||
}
|
||||
|
||||
fn evaluate_bnb(
|
||||
initial_selector: CoinSelector,
|
||||
max_tries: usize,
|
||||
) -> Result<Evaluation, EvaluationError> {
|
||||
evaluate(initial_selector, |cs| {
|
||||
coin_select_bnb(max_tries, cs.clone()).map_or(false, |new_cs| {
|
||||
*cs = new_cs;
|
||||
true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_enough_coins() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 100_000).into(),
|
||||
t.gen_candidate(1, 100_000).into(),
|
||||
];
|
||||
let opts = t.gen_opts(200_000);
|
||||
let selector = CoinSelector::new(&candidates, &opts);
|
||||
assert!(!coin_select_bnb(10_000, selector).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exactly_enough_coins_preselected() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 100_000).into(), // to preselect
|
||||
t.gen_candidate(1, 100_000).into(), // to preselect
|
||||
t.gen_candidate(2, 100_000).into(),
|
||||
];
|
||||
let opts = CoinSelectorOpt {
|
||||
target_feerate: 0.0,
|
||||
..t.gen_opts(200_000)
|
||||
};
|
||||
let selector = {
|
||||
let mut selector = CoinSelector::new(&candidates, &opts);
|
||||
selector.select(0); // preselect
|
||||
selector.select(1); // preselect
|
||||
selector
|
||||
};
|
||||
|
||||
let evaluation = evaluate_bnb(selector, 10_000).expect("eval failed");
|
||||
println!("{}", evaluation);
|
||||
assert_eq!(evaluation.solution.selected, (0..=1).collect());
|
||||
assert_eq!(evaluation.solution.excess_strategies.len(), 1);
|
||||
assert_eq!(
|
||||
evaluation.feerate_offset(ExcessStrategyKind::ToFee).floor(),
|
||||
0.0
|
||||
);
|
||||
}
|
||||
|
||||
/// `cost_of_change` acts as the upper-bound in Bnb, we check whether these boundaries are
|
||||
/// enforced in code
|
||||
#[test]
|
||||
fn cost_of_change() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 200_000).into(),
|
||||
t.gen_candidate(1, 200_000).into(),
|
||||
t.gen_candidate(2, 200_000).into(),
|
||||
];
|
||||
|
||||
// lowest and highest possible `recipient_value` opts for derived `drain_waste`, assuming
|
||||
// that we want 2 candidates selected
|
||||
let (lowest_opts, highest_opts) = {
|
||||
let opts = t.gen_opts(0);
|
||||
|
||||
let fee_from_inputs =
|
||||
(candidates[0].weight as f32 * opts.target_feerate).ceil() as u64 * 2;
|
||||
let fee_from_template =
|
||||
((opts.base_weight + 2) as f32 * opts.target_feerate).ceil() as u64;
|
||||
|
||||
let lowest_opts = CoinSelectorOpt {
|
||||
target_value: Some(
|
||||
400_000 - fee_from_inputs - fee_from_template - opts.drain_waste() as u64,
|
||||
),
|
||||
..opts
|
||||
};
|
||||
|
||||
let highest_opts = CoinSelectorOpt {
|
||||
target_value: Some(400_000 - fee_from_inputs - fee_from_template),
|
||||
..opts
|
||||
};
|
||||
|
||||
(lowest_opts, highest_opts)
|
||||
};
|
||||
|
||||
// test lowest possible target we are able to select
|
||||
let lowest_eval = evaluate_bnb(CoinSelector::new(&candidates, &lowest_opts), 10_000);
|
||||
assert!(lowest_eval.is_ok());
|
||||
let lowest_eval = lowest_eval.unwrap();
|
||||
println!("LB {}", lowest_eval);
|
||||
assert_eq!(lowest_eval.solution.selected.len(), 2);
|
||||
assert_eq!(lowest_eval.solution.excess_strategies.len(), 1);
|
||||
assert_eq!(
|
||||
lowest_eval
|
||||
.feerate_offset(ExcessStrategyKind::ToFee)
|
||||
.floor(),
|
||||
0.0
|
||||
);
|
||||
|
||||
// test highest possible target we are able to select
|
||||
let highest_eval = evaluate_bnb(CoinSelector::new(&candidates, &highest_opts), 10_000);
|
||||
assert!(highest_eval.is_ok());
|
||||
let highest_eval = highest_eval.unwrap();
|
||||
println!("UB {}", highest_eval);
|
||||
assert_eq!(highest_eval.solution.selected.len(), 2);
|
||||
assert_eq!(highest_eval.solution.excess_strategies.len(), 1);
|
||||
assert_eq!(
|
||||
highest_eval
|
||||
.feerate_offset(ExcessStrategyKind::ToFee)
|
||||
.floor(),
|
||||
0.0
|
||||
);
|
||||
|
||||
// test lower out of bounds
|
||||
let loob_opts = CoinSelectorOpt {
|
||||
target_value: lowest_opts.target_value.map(|v| v - 1),
|
||||
..lowest_opts
|
||||
};
|
||||
let loob_eval = evaluate_bnb(CoinSelector::new(&candidates, &loob_opts), 10_000);
|
||||
assert!(loob_eval.is_err());
|
||||
println!("Lower OOB: {}", loob_eval.unwrap_err());
|
||||
|
||||
// test upper out of bounds
|
||||
let uoob_opts = CoinSelectorOpt {
|
||||
target_value: highest_opts.target_value.map(|v| v + 1),
|
||||
..highest_opts
|
||||
};
|
||||
let uoob_eval = evaluate_bnb(CoinSelector::new(&candidates, &uoob_opts), 10_000);
|
||||
assert!(uoob_eval.is_err());
|
||||
println!("Upper OOB: {}", uoob_eval.unwrap_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_select() {
|
||||
let t = tester();
|
||||
let candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 300_000).into(),
|
||||
t.gen_candidate(1, 300_000).into(),
|
||||
t.gen_candidate(2, 300_000).into(),
|
||||
t.gen_candidate(3, 200_000).into(),
|
||||
t.gen_candidate(4, 200_000).into(),
|
||||
];
|
||||
let make_opts = |v: u64| -> CoinSelectorOpt {
|
||||
CoinSelectorOpt {
|
||||
target_feerate: 0.0,
|
||||
..t.gen_opts(v)
|
||||
}
|
||||
};
|
||||
|
||||
let test_cases = vec![
|
||||
(make_opts(100_000), false, 0),
|
||||
(make_opts(200_000), true, 1),
|
||||
(make_opts(300_000), true, 1),
|
||||
(make_opts(500_000), true, 2),
|
||||
(make_opts(1_000_000), true, 4),
|
||||
(make_opts(1_200_000), false, 0),
|
||||
(make_opts(1_300_000), true, 5),
|
||||
(make_opts(1_400_000), false, 0),
|
||||
];
|
||||
|
||||
for (opts, expect_solution, expect_selected) in test_cases {
|
||||
let res = evaluate_bnb(CoinSelector::new(&candidates, &opts), 10_000);
|
||||
assert_eq!(res.is_ok(), expect_solution);
|
||||
|
||||
match res {
|
||||
Ok(eval) => {
|
||||
println!("{}", eval);
|
||||
assert_eq!(eval.feerate_offset(ExcessStrategyKind::ToFee), 0.0);
|
||||
assert_eq!(eval.solution.selected.len(), expect_selected as _);
|
||||
}
|
||||
Err(err) => println!("expected failure: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn early_bailout_optimization() {
|
||||
let t = tester();
|
||||
|
||||
// target: 300_000
|
||||
// candidates: 2x of 125_000, 1000x of 100_000, 1x of 50_000
|
||||
// expected solution: 2x 125_000, 1x 50_000
|
||||
// set bnb max tries: 1100, should succeed
|
||||
let candidates = {
|
||||
let mut candidates: Vec<WeightedValue> = vec![
|
||||
t.gen_candidate(0, 125_000).into(),
|
||||
t.gen_candidate(1, 125_000).into(),
|
||||
t.gen_candidate(2, 50_000).into(),
|
||||
];
|
||||
(3..3 + 1000_u32)
|
||||
.for_each(|index| candidates.push(t.gen_candidate(index, 100_000).into()));
|
||||
candidates
|
||||
};
|
||||
let opts = CoinSelectorOpt {
|
||||
target_feerate: 0.0,
|
||||
..t.gen_opts(300_000)
|
||||
};
|
||||
|
||||
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 1100);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let eval = result.unwrap();
|
||||
println!("{}", eval);
|
||||
assert_eq!(eval.solution.selected, (0..=2).collect());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_exhaust_iteration() {
|
||||
static MAX_TRIES: usize = 1000;
|
||||
let t = tester();
|
||||
let candidates = (0..MAX_TRIES + 1)
|
||||
.map(|index| t.gen_candidate(index as _, 10_000).into())
|
||||
.collect::<Vec<WeightedValue>>();
|
||||
let opts = t.gen_opts(10_001 * MAX_TRIES as u64);
|
||||
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), MAX_TRIES);
|
||||
assert!(result.is_err());
|
||||
println!("error as expected: {}", result.unwrap_err());
|
||||
}
|
||||
|
||||
/// Solution should have fee >= min_absolute_fee (or no solution at all)
|
||||
#[test]
|
||||
fn min_absolute_fee() {
|
||||
let t = tester();
|
||||
let candidates = {
|
||||
let mut candidates = Vec::new();
|
||||
t.gen_weighted_values(&mut candidates, 5, 10_000);
|
||||
t.gen_weighted_values(&mut candidates, 5, 20_000);
|
||||
t.gen_weighted_values(&mut candidates, 5, 30_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_300);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_500);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_700);
|
||||
t.gen_weighted_values(&mut candidates, 10, 10_900);
|
||||
t.gen_weighted_values(&mut candidates, 10, 11_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 12_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 13_000);
|
||||
candidates
|
||||
};
|
||||
let mut opts = CoinSelectorOpt {
|
||||
min_absolute_fee: 1,
|
||||
..t.gen_opts(100_000)
|
||||
};
|
||||
|
||||
(1..=120_u64).for_each(|fee_factor| {
|
||||
opts.min_absolute_fee = fee_factor * 31;
|
||||
|
||||
let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 21_000);
|
||||
match result {
|
||||
Ok(result) => {
|
||||
println!("Solution {}", result);
|
||||
let fee = result.solution.excess_strategies[&ExcessStrategyKind::ToFee].fee;
|
||||
assert!(fee >= opts.min_absolute_fee);
|
||||
assert_eq!(result.solution.excess_strategies.len(), 1);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("No Solution: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// For a decreasing feerate (longterm feerate is lower than effective feerate), we should
|
||||
/// select less. For increasing feerate (longterm feerate is higher than effective feerate), we
|
||||
/// should select more.
|
||||
#[test]
|
||||
fn feerate_difference() {
|
||||
let t = tester();
|
||||
let candidates = {
|
||||
let mut candidates = Vec::new();
|
||||
t.gen_weighted_values(&mut candidates, 10, 2_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 5_000);
|
||||
t.gen_weighted_values(&mut candidates, 10, 20_000);
|
||||
candidates
|
||||
};
|
||||
|
||||
let decreasing_feerate_opts = CoinSelectorOpt {
|
||||
target_feerate: 1.25,
|
||||
long_term_feerate: Some(0.25),
|
||||
..t.gen_opts(100_000)
|
||||
};
|
||||
|
||||
let increasing_feerate_opts = CoinSelectorOpt {
|
||||
target_feerate: 0.25,
|
||||
long_term_feerate: Some(1.25),
|
||||
..t.gen_opts(100_000)
|
||||
};
|
||||
|
||||
let decreasing_res = evaluate_bnb(
|
||||
CoinSelector::new(&candidates, &decreasing_feerate_opts),
|
||||
21_000,
|
||||
)
|
||||
.expect("no result");
|
||||
let decreasing_len = decreasing_res.solution.selected.len();
|
||||
|
||||
let increasing_res = evaluate_bnb(
|
||||
CoinSelector::new(&candidates, &increasing_feerate_opts),
|
||||
21_000,
|
||||
)
|
||||
.expect("no result");
|
||||
let increasing_len = increasing_res.solution.selected.len();
|
||||
|
||||
println!("decreasing_len: {}", decreasing_len);
|
||||
println!("increasing_len: {}", increasing_len);
|
||||
assert!(decreasing_len < increasing_len);
|
||||
}
|
||||
|
||||
/// TODO: UNIMPLEMENTED TESTS:
|
||||
/// * Excess strategies:
|
||||
/// * We should always have `ExcessStrategy::ToFee`.
|
||||
/// * We should only have `ExcessStrategy::ToRecipient` when `max_extra_target > 0`.
|
||||
/// * We should only have `ExcessStrategy::ToDrain` when `drain_value >= min_drain_value`.
|
||||
/// * Fuzz
|
||||
/// * Solution feerate should never be lower than target feerate
|
||||
/// * Solution fee should never be lower than `min_absolute_fee`
|
||||
/// * Preselected should always remain selected
|
||||
fn _todo() {}
|
||||
}
|
617
nursery/coin_select/src/coin_selector.rs
Normal file
617
nursery/coin_select/src/coin_selector.rs
Normal file
@ -0,0 +1,617 @@
|
||||
use super::*;
|
||||
|
||||
/// A [`WeightedValue`] represents an input candidate for [`CoinSelector`]. This can either be a
|
||||
/// single UTXO, or a group of UTXOs that should be spent together.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct WeightedValue {
|
||||
/// Total value of the UTXO(s) that this [`WeightedValue`] represents.
|
||||
pub value: u64,
|
||||
/// Total weight of including this/these UTXO(s).
|
||||
/// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`,
|
||||
/// `scriptWitness` should all be included.
|
||||
pub weight: u32,
|
||||
/// Total number of inputs; so we can calculate extra `varint` weight due to `vin` len changes.
|
||||
pub input_count: usize,
|
||||
/// Whether this [`WeightedValue`] contains at least one segwit spend.
|
||||
pub is_segwit: bool,
|
||||
}
|
||||
|
||||
impl WeightedValue {
|
||||
/// Create a new [`WeightedValue`] that represents a single input.
|
||||
///
|
||||
/// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen +
|
||||
/// scriptWitness`.
|
||||
pub fn new(value: u64, satisfaction_weight: u32, is_segwit: bool) -> WeightedValue {
|
||||
let weight = TXIN_BASE_WEIGHT + satisfaction_weight;
|
||||
WeightedValue {
|
||||
value,
|
||||
weight,
|
||||
input_count: 1,
|
||||
is_segwit,
|
||||
}
|
||||
}
|
||||
|
||||
/// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`.
|
||||
pub fn effective_value(&self, effective_feerate: f32) -> i64 {
|
||||
// We prefer undershooting the candidate's effective value (so we over estimate the fee of a
|
||||
// candidate). If we overshoot the candidate's effective value, it may be possible to find a
|
||||
// solution which does not meet the target feerate.
|
||||
self.value as i64 - (self.weight as f32 * effective_feerate).ceil() as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CoinSelectorOpt {
|
||||
/// The value we need to select.
|
||||
/// If the value is `None` then the selection will be complete if it can pay for the drain
|
||||
/// output and satisfy the other constraints (e.g. minimum fees).
|
||||
pub target_value: Option<u64>,
|
||||
/// Additional leeway for the target value.
|
||||
pub max_extra_target: u64, // TODO: Maybe out of scope here?
|
||||
|
||||
/// The feerate we should try and achieve in sats per weight unit.
|
||||
pub target_feerate: f32,
|
||||
/// The feerate
|
||||
pub long_term_feerate: Option<f32>, // TODO: Maybe out of scope? (waste)
|
||||
/// The minimum absolute fee. I.e. needed for RBF.
|
||||
pub min_absolute_fee: u64,
|
||||
|
||||
/// The weight of the template transaction including fixed fields and outputs.
|
||||
pub base_weight: u32,
|
||||
/// Additional weight if we include the drain (change) output.
|
||||
pub drain_weight: u32,
|
||||
/// Weight of spending the drain (change) output in the future.
|
||||
pub spend_drain_weight: u32, // TODO: Maybe out of scope? (waste)
|
||||
|
||||
/// Minimum value allowed for a drain (change) output.
|
||||
pub min_drain_value: u64,
|
||||
}
|
||||
|
||||
impl CoinSelectorOpt {
|
||||
fn from_weights(base_weight: u32, drain_weight: u32, spend_drain_weight: u32) -> Self {
|
||||
// 0.25 sats/wu == 1 sat/vb
|
||||
let target_feerate = 0.25_f32;
|
||||
|
||||
// set `min_drain_value` to dust limit
|
||||
let min_drain_value =
|
||||
3 * ((drain_weight + spend_drain_weight) as f32 * target_feerate) as u64;
|
||||
|
||||
Self {
|
||||
target_value: None,
|
||||
max_extra_target: 0,
|
||||
target_feerate,
|
||||
long_term_feerate: None,
|
||||
min_absolute_fee: 0,
|
||||
base_weight,
|
||||
drain_weight,
|
||||
spend_drain_weight,
|
||||
min_drain_value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fund_outputs(
|
||||
txouts: &[TxOut],
|
||||
drain_output: &TxOut,
|
||||
drain_satisfaction_weight: u32,
|
||||
) -> Self {
|
||||
let mut tx = Transaction {
|
||||
input: vec![],
|
||||
version: 1,
|
||||
lock_time: LockTime::ZERO.into(),
|
||||
output: txouts.to_vec(),
|
||||
};
|
||||
let base_weight = tx.weight();
|
||||
// this awkward calculation is necessary since TxOut doesn't have \.weight()
|
||||
let drain_weight = {
|
||||
tx.output.push(drain_output.clone());
|
||||
tx.weight() - base_weight
|
||||
};
|
||||
Self {
|
||||
target_value: if txouts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(txouts.iter().map(|txout| txout.value).sum())
|
||||
},
|
||||
..Self::from_weights(
|
||||
base_weight as u32,
|
||||
drain_weight as u32,
|
||||
TXIN_BASE_WEIGHT + drain_satisfaction_weight,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn long_term_feerate(&self) -> f32 {
|
||||
self.long_term_feerate.unwrap_or(self.target_feerate)
|
||||
}
|
||||
|
||||
pub fn drain_waste(&self) -> i64 {
|
||||
(self.drain_weight as f32 * self.target_feerate
|
||||
+ self.spend_drain_weight as f32 * self.long_term_feerate()) as i64
|
||||
}
|
||||
}
|
||||
|
||||
/// [`CoinSelector`] is responsible for selecting and deselecting from a set of canididates.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoinSelector<'a> {
|
||||
pub opts: &'a CoinSelectorOpt,
|
||||
pub candidates: &'a Vec<WeightedValue>,
|
||||
selected: BTreeSet<usize>,
|
||||
}
|
||||
|
||||
impl<'a> CoinSelector<'a> {
|
||||
pub fn candidate(&self, index: usize) -> &WeightedValue {
|
||||
&self.candidates[index]
|
||||
}
|
||||
|
||||
pub fn new(candidates: &'a Vec<WeightedValue>, opts: &'a CoinSelectorOpt) -> Self {
|
||||
Self {
|
||||
candidates,
|
||||
selected: Default::default(),
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&mut self, index: usize) -> bool {
|
||||
assert!(index < self.candidates.len());
|
||||
self.selected.insert(index)
|
||||
}
|
||||
|
||||
pub fn deselect(&mut self, index: usize) -> bool {
|
||||
self.selected.remove(&index)
|
||||
}
|
||||
|
||||
pub fn is_selected(&self, index: usize) -> bool {
|
||||
self.selected.contains(&index)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selected.is_empty()
|
||||
}
|
||||
|
||||
/// Weight sum of all selected inputs.
|
||||
pub fn selected_weight(&self) -> u32 {
|
||||
self.selected
|
||||
.iter()
|
||||
.map(|&index| self.candidates[index].weight)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Effective value sum of all selected inputs.
|
||||
pub fn selected_effective_value(&self) -> i64 {
|
||||
self.selected
|
||||
.iter()
|
||||
.map(|&index| self.candidates[index].effective_value(self.opts.target_feerate))
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Absolute value sum of all selected inputs.
|
||||
pub fn selected_absolute_value(&self) -> u64 {
|
||||
self.selected
|
||||
.iter()
|
||||
.map(|&index| self.candidates[index].value)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Waste sum of all selected inputs.
|
||||
pub fn selected_waste(&self) -> i64 {
|
||||
(self.selected_weight() as f32 * (self.opts.target_feerate - self.opts.long_term_feerate()))
|
||||
as i64
|
||||
}
|
||||
|
||||
/// Current weight of template tx + selected inputs.
|
||||
pub fn current_weight(&self) -> u32 {
|
||||
let witness_header_extra_weight = self
|
||||
.selected()
|
||||
.find(|(_, wv)| wv.is_segwit)
|
||||
.map(|_| 2)
|
||||
.unwrap_or(0);
|
||||
let vin_count_varint_extra_weight = {
|
||||
let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::<usize>();
|
||||
(varint_size(input_count) - 1) * 4
|
||||
};
|
||||
self.opts.base_weight
|
||||
+ self.selected_weight()
|
||||
+ witness_header_extra_weight
|
||||
+ vin_count_varint_extra_weight
|
||||
}
|
||||
|
||||
/// Current excess.
|
||||
pub fn current_excess(&self) -> i64 {
|
||||
self.selected_effective_value() - self.effective_target()
|
||||
}
|
||||
|
||||
/// This is the effective target value.
|
||||
pub fn effective_target(&self) -> i64 {
|
||||
let (has_segwit, max_input_count) = self
|
||||
.candidates
|
||||
.iter()
|
||||
.fold((false, 0_usize), |(is_segwit, input_count), c| {
|
||||
(is_segwit || c.is_segwit, input_count + c.input_count)
|
||||
});
|
||||
|
||||
let effective_base_weight = self.opts.base_weight
|
||||
+ if has_segwit { 2_u32 } else { 0_u32 }
|
||||
+ (varint_size(max_input_count) - 1) * 4;
|
||||
|
||||
self.opts.target_value.unwrap_or(0) as i64
|
||||
+ (effective_base_weight as f32 * self.opts.target_feerate).ceil() as i64
|
||||
}
|
||||
|
||||
pub fn selected_count(&self) -> usize {
|
||||
self.selected.len()
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> impl Iterator<Item = (usize, &'a WeightedValue)> + '_ {
|
||||
self.selected
|
||||
.iter()
|
||||
.map(move |&index| (index, &self.candidates[index]))
|
||||
}
|
||||
|
||||
pub fn unselected(&self) -> impl Iterator<Item = (usize, &'a WeightedValue)> + '_ {
|
||||
self.candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(index, _)| !self.selected.contains(index))
|
||||
}
|
||||
|
||||
pub fn selected_indexes(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
self.selected.iter().cloned()
|
||||
}
|
||||
|
||||
pub fn unselected_indexes(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
(0..self.candidates.len()).filter(move |index| !self.selected.contains(index))
|
||||
}
|
||||
|
||||
pub fn all_selected(&self) -> bool {
|
||||
self.selected.len() == self.candidates.len()
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
self.selected = (0..self.candidates.len()).collect();
|
||||
}
|
||||
|
||||
pub fn select_until_finished(&mut self) -> Result<Selection, SelectionError> {
|
||||
let mut selection = self.finish();
|
||||
|
||||
if selection.is_ok() {
|
||||
return selection;
|
||||
}
|
||||
|
||||
let unselected = self.unselected_indexes().collect::<Vec<_>>();
|
||||
|
||||
for index in unselected {
|
||||
self.select(index);
|
||||
selection = self.finish();
|
||||
|
||||
if selection.is_ok() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selection
|
||||
}
|
||||
|
||||
pub fn finish(&self) -> Result<Selection, SelectionError> {
|
||||
let weight_without_drain = self.current_weight();
|
||||
let weight_with_drain = weight_without_drain + self.opts.drain_weight;
|
||||
|
||||
let fee_without_drain =
|
||||
(weight_without_drain as f32 * self.opts.target_feerate).ceil() as u64;
|
||||
let fee_with_drain = (weight_with_drain as f32 * self.opts.target_feerate).ceil() as u64;
|
||||
|
||||
let inputs_minus_outputs = {
|
||||
let target_value = self.opts.target_value.unwrap_or(0);
|
||||
let selected = self.selected_absolute_value();
|
||||
|
||||
// find the largest unsatisfied constraint (if any), and return error of that constraint
|
||||
// "selected" should always be greater than or equal to these selected values
|
||||
[
|
||||
(
|
||||
SelectionConstraint::TargetValue,
|
||||
target_value.saturating_sub(selected),
|
||||
),
|
||||
(
|
||||
SelectionConstraint::TargetFee,
|
||||
(target_value + fee_without_drain).saturating_sub(selected),
|
||||
),
|
||||
(
|
||||
SelectionConstraint::MinAbsoluteFee,
|
||||
(target_value + self.opts.min_absolute_fee).saturating_sub(selected),
|
||||
),
|
||||
(
|
||||
SelectionConstraint::MinDrainValue,
|
||||
// when we have no target value (hence no recipient txouts), we need to ensure
|
||||
// the selected amount can satisfy requirements for a drain output (so we at
|
||||
// least have one txout)
|
||||
if self.opts.target_value.is_none() {
|
||||
(fee_with_drain + self.opts.min_drain_value).saturating_sub(selected)
|
||||
} else {
|
||||
0
|
||||
},
|
||||
),
|
||||
]
|
||||
.iter()
|
||||
.filter(|&(_, v)| v > &0)
|
||||
.max_by_key(|&(_, v)| v)
|
||||
.map_or(Ok(()), |(constraint, missing)| {
|
||||
Err(SelectionError {
|
||||
selected,
|
||||
missing: *missing,
|
||||
constraint: *constraint,
|
||||
})
|
||||
})?;
|
||||
|
||||
(selected - target_value) as u64
|
||||
};
|
||||
|
||||
let fee_without_drain = fee_without_drain.max(self.opts.min_absolute_fee);
|
||||
let fee_with_drain = fee_with_drain.max(self.opts.min_absolute_fee);
|
||||
|
||||
let excess_without_drain = inputs_minus_outputs - fee_without_drain;
|
||||
let input_waste = self.selected_waste();
|
||||
|
||||
// begin preparing excess strategies for final selection
|
||||
let mut excess_strategies = HashMap::new();
|
||||
|
||||
// only allow `ToFee` and `ToRecipient` excess strategies when we have a `target_value`,
|
||||
// otherwise we will result in a result with no txouts, or attempt to add value to an output
|
||||
// that does not exist
|
||||
if self.opts.target_value.is_some() {
|
||||
// no drain, excess to fee
|
||||
excess_strategies.insert(
|
||||
ExcessStrategyKind::ToFee,
|
||||
ExcessStrategy {
|
||||
recipient_value: self.opts.target_value,
|
||||
drain_value: None,
|
||||
fee: fee_without_drain + excess_without_drain,
|
||||
weight: weight_without_drain,
|
||||
waste: input_waste + excess_without_drain as i64,
|
||||
},
|
||||
);
|
||||
|
||||
// no drain, excess to recipient
|
||||
// if `excess == 0`, this result will be the same as the previous, so don't consider it
|
||||
// if `max_extra_target == 0`, there is no leeway for this strategy
|
||||
if excess_without_drain > 0 && self.opts.max_extra_target > 0 {
|
||||
let extra_recipient_value =
|
||||
core::cmp::min(self.opts.max_extra_target, excess_without_drain);
|
||||
let extra_fee = excess_without_drain - extra_recipient_value;
|
||||
excess_strategies.insert(
|
||||
ExcessStrategyKind::ToRecipient,
|
||||
ExcessStrategy {
|
||||
recipient_value: self.opts.target_value.map(|v| v + extra_recipient_value),
|
||||
drain_value: None,
|
||||
fee: fee_without_drain + extra_fee,
|
||||
weight: weight_without_drain,
|
||||
waste: input_waste + extra_fee as i64,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// with drain
|
||||
if fee_with_drain >= self.opts.min_absolute_fee
|
||||
&& inputs_minus_outputs >= fee_with_drain + self.opts.min_drain_value
|
||||
{
|
||||
excess_strategies.insert(
|
||||
ExcessStrategyKind::ToDrain,
|
||||
ExcessStrategy {
|
||||
recipient_value: self.opts.target_value,
|
||||
drain_value: Some(inputs_minus_outputs.saturating_sub(fee_with_drain)),
|
||||
fee: fee_with_drain,
|
||||
weight: weight_with_drain,
|
||||
waste: input_waste + self.opts.drain_waste(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
!excess_strategies.is_empty(),
|
||||
"should have at least one excess strategy"
|
||||
);
|
||||
|
||||
Ok(Selection {
|
||||
selected: self.selected.clone(),
|
||||
excess: excess_without_drain,
|
||||
excess_strategies,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SelectionError {
|
||||
selected: u64,
|
||||
missing: u64,
|
||||
constraint: SelectionConstraint,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SelectionError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SelectionError {
|
||||
selected,
|
||||
missing,
|
||||
constraint,
|
||||
} => write!(
|
||||
f,
|
||||
"insufficient coins selected; selected={}, missing={}, unsatisfied_constraint={:?}",
|
||||
selected, missing, constraint
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SelectionError {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SelectionConstraint {
|
||||
/// The target is not met
|
||||
TargetValue,
|
||||
/// The target fee (given the feerate) is not met
|
||||
TargetFee,
|
||||
/// Min absolute fee is not met
|
||||
MinAbsoluteFee,
|
||||
/// Min drain value is not met
|
||||
MinDrainValue,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SelectionConstraint {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SelectionConstraint::TargetValue => core::write!(f, "target_value"),
|
||||
SelectionConstraint::TargetFee => core::write!(f, "target_fee"),
|
||||
SelectionConstraint::MinAbsoluteFee => core::write!(f, "min_absolute_fee"),
|
||||
SelectionConstraint::MinDrainValue => core::write!(f, "min_drain_value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Selection {
|
||||
pub selected: BTreeSet<usize>,
|
||||
pub excess: u64,
|
||||
pub excess_strategies: HashMap<ExcessStrategyKind, ExcessStrategy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
|
||||
pub enum ExcessStrategyKind {
|
||||
ToFee,
|
||||
ToRecipient,
|
||||
ToDrain,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ExcessStrategy {
|
||||
pub recipient_value: Option<u64>,
|
||||
pub drain_value: Option<u64>,
|
||||
pub fee: u64,
|
||||
pub weight: u32,
|
||||
pub waste: i64,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
pub fn apply_selection<'a, T>(
|
||||
&'a self,
|
||||
candidates: &'a [T],
|
||||
) -> impl Iterator<Item = &'a T> + 'a {
|
||||
self.selected.iter().map(move |i| &candidates[*i])
|
||||
}
|
||||
|
||||
/// Returns the [`ExcessStrategy`] that results in the least waste.
|
||||
pub fn best_strategy(&self) -> (&ExcessStrategyKind, &ExcessStrategy) {
|
||||
self.excess_strategies
|
||||
.iter()
|
||||
.min_by_key(|&(_, a)| a.waste)
|
||||
.expect("selection has no excess strategy")
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ExcessStrategyKind {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ExcessStrategyKind::ToFee => core::write!(f, "to_fee"),
|
||||
ExcessStrategyKind::ToRecipient => core::write!(f, "to_recipient"),
|
||||
ExcessStrategyKind::ToDrain => core::write!(f, "to_drain"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExcessStrategy {
|
||||
/// Returns feerate in sats/wu.
|
||||
pub fn feerate(&self) -> f32 {
|
||||
self.fee as f32 / self.weight as f32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{ExcessStrategyKind, SelectionConstraint};
|
||||
|
||||
use super::{CoinSelector, CoinSelectorOpt, WeightedValue};
|
||||
|
||||
/// Ensure `target_value` is respected. Can't have no disrespect.
|
||||
#[test]
|
||||
fn target_value_respected() {
|
||||
let target_value = 1000_u64;
|
||||
|
||||
let candidates = (500..1500_u64)
|
||||
.map(|value| WeightedValue {
|
||||
value,
|
||||
weight: 100,
|
||||
input_count: 1,
|
||||
is_segwit: false,
|
||||
})
|
||||
.collect::<super::Vec<_>>();
|
||||
|
||||
let opts = CoinSelectorOpt {
|
||||
target_value: Some(target_value),
|
||||
max_extra_target: 0,
|
||||
target_feerate: 0.00,
|
||||
long_term_feerate: None,
|
||||
min_absolute_fee: 0,
|
||||
base_weight: 10,
|
||||
drain_weight: 10,
|
||||
spend_drain_weight: 10,
|
||||
min_drain_value: 10,
|
||||
};
|
||||
|
||||
for (index, v) in candidates.iter().enumerate() {
|
||||
let mut selector = CoinSelector::new(&candidates, &opts);
|
||||
assert!(selector.select(index));
|
||||
|
||||
let res = selector.finish();
|
||||
if v.value < opts.target_value.unwrap_or(0) {
|
||||
let err = res.expect_err("should have failed");
|
||||
assert_eq!(err.selected, v.value);
|
||||
assert_eq!(err.missing, target_value - v.value);
|
||||
assert_eq!(err.constraint, SelectionConstraint::MinAbsoluteFee);
|
||||
} else {
|
||||
let sel = res.expect("should have succeeded");
|
||||
assert_eq!(sel.excess, v.value - opts.target_value.unwrap_or(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_all() {
|
||||
let candidates = (0..100)
|
||||
.map(|_| WeightedValue {
|
||||
value: 666,
|
||||
weight: 166,
|
||||
input_count: 1,
|
||||
is_segwit: false,
|
||||
})
|
||||
.collect::<super::Vec<_>>();
|
||||
|
||||
let opts = CoinSelectorOpt {
|
||||
target_value: None,
|
||||
max_extra_target: 0,
|
||||
target_feerate: 0.25,
|
||||
long_term_feerate: None,
|
||||
min_absolute_fee: 0,
|
||||
base_weight: 10,
|
||||
drain_weight: 100,
|
||||
spend_drain_weight: 66,
|
||||
min_drain_value: 1000,
|
||||
};
|
||||
|
||||
let selection = CoinSelector::new(&candidates, &opts)
|
||||
.select_until_finished()
|
||||
.expect("should succeed");
|
||||
|
||||
assert!(selection.selected.len() > 1);
|
||||
assert_eq!(selection.excess_strategies.len(), 1);
|
||||
|
||||
let (kind, strategy) = selection.best_strategy();
|
||||
assert_eq!(*kind, ExcessStrategyKind::ToDrain);
|
||||
assert!(strategy.recipient_value.is_none());
|
||||
assert!(strategy.drain_value.is_some());
|
||||
}
|
||||
|
||||
/// TODO: Tests to add:
|
||||
/// * `finish` should ensure at least `target_value` is selected.
|
||||
/// * actual feerate should be equal or higher than `target_feerate`.
|
||||
/// * actual drain value should be equal or higher than `min_drain_value` (or else no drain).
|
||||
fn _todo() {}
|
||||
}
|
33
nursery/coin_select/src/lib.rs
Normal file
33
nursery/coin_select/src/lib.rs
Normal file
@ -0,0 +1,33 @@
|
||||
#![no_std]
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
extern crate bdk_chain;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bdk_chain::{
|
||||
bitcoin,
|
||||
collections::{BTreeSet, HashMap},
|
||||
};
|
||||
use bitcoin::{LockTime, Transaction, TxOut};
|
||||
use core::fmt::{Debug, Display};
|
||||
|
||||
mod coin_selector;
|
||||
pub use coin_selector::*;
|
||||
|
||||
mod bnb;
|
||||
pub use bnb::*;
|
||||
|
||||
/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4). This does not include
|
||||
/// `scriptSigLen` or `scriptSig`.
|
||||
pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4;
|
||||
|
||||
/// Helper to calculate varint size. `v` is the value the varint represents.
|
||||
// Shamelessly copied from
|
||||
// https://github.com/rust-bitcoin/rust-miniscript/blob/d5615acda1a7fdc4041a11c1736af139b8c7ebe8/src/util.rs#L8
|
||||
pub(crate) fn varint_size(v: usize) -> u32 {
|
||||
bitcoin::VarInt(v as u64).len() as u32
|
||||
}
|
13
nursery/tmp_plan/Cargo.toml
Normal file
13
nursery/tmp_plan/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "bdk_tmp_plan"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { version = "0.3", features = ["miniscript"], path = "../../crates/chain" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
3
nursery/tmp_plan/README.md
Normal file
3
nursery/tmp_plan/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Temporary planning module
|
||||
|
||||
A temporary place to hold the planning module until https://github.com/rust-bitcoin/rust-miniscript/pull/481 is merged and released
|
13
nursery/tmp_plan/bdk_tmp_plan/Cargo.toml
Normal file
13
nursery/tmp_plan/bdk_tmp_plan/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "bdk_tmp_plan"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { version = "0.3", features = ["miniscript"] }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
3
nursery/tmp_plan/bdk_tmp_plan/README.md
Normal file
3
nursery/tmp_plan/bdk_tmp_plan/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Temporary planning module
|
||||
|
||||
A temporary place to hold the planning module until https://github.com/rust-bitcoin/rust-miniscript/pull/481 is merged and released
|
436
nursery/tmp_plan/bdk_tmp_plan/src/lib.rs
Normal file
436
nursery/tmp_plan/bdk_tmp_plan/src/lib.rs
Normal file
@ -0,0 +1,436 @@
|
||||
#![allow(unused)]
|
||||
#![allow(missing_docs)]
|
||||
//! A spending plan or *plan* for short is a representation of a particular spending path on a
|
||||
//! descriptor. This allows us to analayze a choice of spending path without producing any
|
||||
//! signatures or other witness data for it.
|
||||
//!
|
||||
//! To make a plan you provide the descriptor with "assets" like which keys you are able to use, hash
|
||||
//! pre-images you have access to, the current block height etc.
|
||||
//!
|
||||
//! Once you've got a plan it can tell you its expected satisfaction weight which can be useful for
|
||||
//! doing coin selection. Furthermore it provides which subset of those keys and hash pre-images you
|
||||
//! will actually need as well as what locktime or sequence number you need to set.
|
||||
//!
|
||||
//! Once you've obstained signatures, hash pre-images etc required by the plan, it can create a
|
||||
//! witness/script_sig for the input.
|
||||
use bdk_chain::{bitcoin, collections::*, miniscript};
|
||||
use bitcoin::{
|
||||
blockdata::{locktime::LockTime, transaction::Sequence},
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
secp256k1::Secp256k1,
|
||||
util::{
|
||||
address::WitnessVersion,
|
||||
bip32::{DerivationPath, Fingerprint, KeySource},
|
||||
taproot::{LeafVersion, TapBranchHash, TapLeafHash},
|
||||
},
|
||||
EcdsaSig, SchnorrSig, Script, TxIn, Witness,
|
||||
};
|
||||
use miniscript::{
|
||||
descriptor::{InnerXKey, Tr},
|
||||
hash256, DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, ScriptContext, ToPublicKey,
|
||||
};
|
||||
|
||||
pub(crate) fn varint_len(v: usize) -> usize {
|
||||
bitcoin::VarInt(v as u64).len() as usize
|
||||
}
|
||||
|
||||
mod plan_impls;
|
||||
mod requirements;
|
||||
mod template;
|
||||
pub use requirements::*;
|
||||
pub use template::PlanKey;
|
||||
use template::TemplateItem;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum TrSpend {
|
||||
KeySpend,
|
||||
LeafSpend {
|
||||
script: Script,
|
||||
leaf_version: LeafVersion,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Target {
|
||||
Legacy,
|
||||
Segwitv0 {
|
||||
script_code: Script,
|
||||
},
|
||||
Segwitv1 {
|
||||
tr: Tr<DefiniteDescriptorKey>,
|
||||
tr_plan: TrSpend,
|
||||
},
|
||||
}
|
||||
|
||||
impl Target {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A plan represents a particular spending path for a descriptor.
|
||||
///
|
||||
/// See the module level documentation for more info.
|
||||
pub struct Plan<AK> {
|
||||
template: Vec<TemplateItem<AK>>,
|
||||
target: Target,
|
||||
set_locktime: Option<LockTime>,
|
||||
set_sequence: Option<Sequence>,
|
||||
}
|
||||
|
||||
impl Default for Target {
|
||||
fn default() -> Self {
|
||||
Target::Legacy
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// Signatures and hash pre-images that can be used to complete a plan.
|
||||
pub struct SatisfactionMaterial {
|
||||
/// Schnorr signautres under their keys
|
||||
pub schnorr_sigs: BTreeMap<DefiniteDescriptorKey, SchnorrSig>,
|
||||
/// ECDSA signatures under their keys
|
||||
pub ecdsa_sigs: BTreeMap<DefiniteDescriptorKey, EcdsaSig>,
|
||||
/// SHA256 pre-images under their images
|
||||
pub sha256_preimages: BTreeMap<sha256::Hash, Vec<u8>>,
|
||||
/// hash160 pre-images under their images
|
||||
pub hash160_preimages: BTreeMap<hash160::Hash, Vec<u8>>,
|
||||
/// hash256 pre-images under their images
|
||||
pub hash256_preimages: BTreeMap<hash256::Hash, Vec<u8>>,
|
||||
/// ripemd160 pre-images under their images
|
||||
pub ripemd160_preimages: BTreeMap<ripemd160::Hash, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl<Ak> Plan<Ak>
|
||||
where
|
||||
Ak: Clone,
|
||||
{
|
||||
/// The expected satisfaction weight for the plan if it is completed.
|
||||
pub fn expected_weight(&self) -> usize {
|
||||
let script_sig_size = match self.target {
|
||||
Target::Legacy => unimplemented!(), // self
|
||||
// .template
|
||||
// .iter()
|
||||
// .map(|step| {
|
||||
// let size = step.expected_size();
|
||||
// size + push_opcode_size(size)
|
||||
// })
|
||||
// .sum()
|
||||
Target::Segwitv0 { .. } | Target::Segwitv1 { .. } => 1,
|
||||
};
|
||||
let witness_elem_sizes: Option<Vec<usize>> = match &self.target {
|
||||
Target::Legacy => None,
|
||||
Target::Segwitv0 { .. } => Some(
|
||||
self.template
|
||||
.iter()
|
||||
.map(|step| step.expected_size())
|
||||
.collect(),
|
||||
),
|
||||
Target::Segwitv1 { tr, tr_plan } => {
|
||||
let mut witness_elems = self
|
||||
.template
|
||||
.iter()
|
||||
.map(|step| step.expected_size())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let TrSpend::LeafSpend {
|
||||
script,
|
||||
leaf_version,
|
||||
} = tr_plan
|
||||
{
|
||||
let control_block = tr
|
||||
.spend_info()
|
||||
.control_block(&(script.clone(), *leaf_version))
|
||||
.expect("must exist");
|
||||
witness_elems.push(script.len());
|
||||
witness_elems.push(control_block.size());
|
||||
}
|
||||
|
||||
Some(witness_elems)
|
||||
}
|
||||
};
|
||||
|
||||
let witness_size: usize = match witness_elem_sizes {
|
||||
Some(elems) => {
|
||||
varint_len(elems.len())
|
||||
+ elems
|
||||
.into_iter()
|
||||
.map(|elem| varint_len(elem) + elem)
|
||||
.sum::<usize>()
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
script_sig_size * 4 + witness_size
|
||||
}
|
||||
|
||||
pub fn requirements(&self) -> Requirements<Ak> {
|
||||
match self.try_complete(&SatisfactionMaterial::default()) {
|
||||
PlanState::Complete { .. } => Requirements::default(),
|
||||
PlanState::Incomplete(requirements) => requirements,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_complete(&self, auth_data: &SatisfactionMaterial) -> PlanState<Ak> {
|
||||
let unsatisfied_items = self
|
||||
.template
|
||||
.iter()
|
||||
.filter(|step| match step {
|
||||
TemplateItem::Sign(key) => {
|
||||
!auth_data.schnorr_sigs.contains_key(&key.descriptor_key)
|
||||
}
|
||||
TemplateItem::Hash160(image) => !auth_data.hash160_preimages.contains_key(image),
|
||||
TemplateItem::Hash256(image) => !auth_data.hash256_preimages.contains_key(image),
|
||||
TemplateItem::Sha256(image) => !auth_data.sha256_preimages.contains_key(image),
|
||||
TemplateItem::Ripemd160(image) => {
|
||||
!auth_data.ripemd160_preimages.contains_key(image)
|
||||
}
|
||||
TemplateItem::Pk { .. } | TemplateItem::One | TemplateItem::Zero => false,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if unsatisfied_items.is_empty() {
|
||||
let mut witness = self
|
||||
.template
|
||||
.iter()
|
||||
.flat_map(|step| step.to_witness_stack(&auth_data))
|
||||
.collect::<Vec<_>>();
|
||||
match &self.target {
|
||||
Target::Segwitv0 { .. } => todo!(),
|
||||
Target::Legacy => todo!(),
|
||||
Target::Segwitv1 {
|
||||
tr_plan: TrSpend::KeySpend,
|
||||
..
|
||||
} => PlanState::Complete {
|
||||
final_script_sig: None,
|
||||
final_script_witness: Some(Witness::from_vec(witness)),
|
||||
},
|
||||
Target::Segwitv1 {
|
||||
tr,
|
||||
tr_plan:
|
||||
TrSpend::LeafSpend {
|
||||
script,
|
||||
leaf_version,
|
||||
},
|
||||
} => {
|
||||
let spend_info = tr.spend_info();
|
||||
let control_block = spend_info
|
||||
.control_block(&(script.clone(), *leaf_version))
|
||||
.expect("must exist");
|
||||
witness.push(script.clone().into_bytes());
|
||||
witness.push(control_block.serialize());
|
||||
|
||||
PlanState::Complete {
|
||||
final_script_sig: None,
|
||||
final_script_witness: Some(Witness::from_vec(witness)),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut requirements = Requirements::default();
|
||||
|
||||
match &self.target {
|
||||
Target::Legacy => {
|
||||
todo!()
|
||||
}
|
||||
Target::Segwitv0 { .. } => {
|
||||
todo!()
|
||||
}
|
||||
Target::Segwitv1 { tr, tr_plan } => {
|
||||
let spend_info = tr.spend_info();
|
||||
match tr_plan {
|
||||
TrSpend::KeySpend => match &self.template[..] {
|
||||
[TemplateItem::Sign(ref plan_key)] => {
|
||||
requirements.signatures = RequiredSignatures::TapKey {
|
||||
merkle_root: spend_info.merkle_root(),
|
||||
plan_key: plan_key.clone(),
|
||||
};
|
||||
}
|
||||
_ => unreachable!("tapkey spend will always have only one sign step"),
|
||||
},
|
||||
TrSpend::LeafSpend {
|
||||
script,
|
||||
leaf_version,
|
||||
} => {
|
||||
let leaf_hash = TapLeafHash::from_script(&script, *leaf_version);
|
||||
requirements.signatures = RequiredSignatures::TapScript {
|
||||
leaf_hash,
|
||||
plan_keys: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let required_signatures = match requirements.signatures {
|
||||
RequiredSignatures::Legacy { .. } => todo!(),
|
||||
RequiredSignatures::Segwitv0 { .. } => todo!(),
|
||||
RequiredSignatures::TapKey { .. } => return PlanState::Incomplete(requirements),
|
||||
RequiredSignatures::TapScript {
|
||||
plan_keys: ref mut keys,
|
||||
..
|
||||
} => keys,
|
||||
};
|
||||
|
||||
for step in unsatisfied_items {
|
||||
match step {
|
||||
TemplateItem::Sign(plan_key) => {
|
||||
required_signatures.push(plan_key.clone());
|
||||
}
|
||||
TemplateItem::Hash160(image) => {
|
||||
requirements.hash160_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Hash256(image) => {
|
||||
requirements.hash256_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Sha256(image) => {
|
||||
requirements.sha256_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Ripemd160(image) => {
|
||||
requirements.ripemd160_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Pk { .. } | TemplateItem::One | TemplateItem::Zero => { /* no requirements */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlanState::Incomplete(requirements)
|
||||
}
|
||||
}
|
||||
|
||||
/// Witness version for the plan
|
||||
pub fn witness_version(&self) -> Option<WitnessVersion> {
|
||||
match self.target {
|
||||
Target::Legacy => None,
|
||||
Target::Segwitv0 { .. } => Some(WitnessVersion::V0),
|
||||
Target::Segwitv1 { .. } => Some(WitnessVersion::V1),
|
||||
}
|
||||
}
|
||||
|
||||
/// The minimum required locktime height or time on the transaction using the plan.
|
||||
pub fn required_locktime(&self) -> Option<LockTime> {
|
||||
self.set_locktime.clone()
|
||||
}
|
||||
|
||||
/// The minimum required sequence (height or time) on the input to satisfy the plan
|
||||
pub fn required_sequence(&self) -> Option<Sequence> {
|
||||
self.set_sequence.clone()
|
||||
}
|
||||
|
||||
/// The minmum required transaction version required on the transaction using the plan.
|
||||
pub fn min_version(&self) -> Option<u32> {
|
||||
if let Some(_) = self.set_sequence {
|
||||
Some(2)
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The returned value from [`Plan::try_complete`].
|
||||
pub enum PlanState<Ak> {
|
||||
/// The plan is complete
|
||||
Complete {
|
||||
/// The script sig that should be set on the input
|
||||
final_script_sig: Option<Script>,
|
||||
/// The witness that should be set on the input
|
||||
final_script_witness: Option<Witness>,
|
||||
},
|
||||
Incomplete(Requirements<Ak>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Assets<K> {
|
||||
pub keys: Vec<K>,
|
||||
pub txo_age: Option<Sequence>,
|
||||
pub max_locktime: Option<LockTime>,
|
||||
pub sha256: Vec<sha256::Hash>,
|
||||
pub hash256: Vec<hash256::Hash>,
|
||||
pub ripemd160: Vec<ripemd160::Hash>,
|
||||
pub hash160: Vec<hash160::Hash>,
|
||||
}
|
||||
|
||||
impl<K> Default for Assets<K> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keys: Default::default(),
|
||||
txo_age: Default::default(),
|
||||
max_locktime: Default::default(),
|
||||
sha256: Default::default(),
|
||||
hash256: Default::default(),
|
||||
ripemd160: Default::default(),
|
||||
hash160: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CanDerive {
|
||||
fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath>;
|
||||
}
|
||||
|
||||
impl CanDerive for KeySource {
|
||||
fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath> {
|
||||
match DescriptorPublicKey::from(key.clone()) {
|
||||
DescriptorPublicKey::Single(single_pub) => {
|
||||
path_to_child(self, single_pub.origin.as_ref()?, None)
|
||||
}
|
||||
DescriptorPublicKey::XPub(dxk) => {
|
||||
let origin = dxk.origin.clone().unwrap_or_else(|| {
|
||||
let secp = Secp256k1::signing_only();
|
||||
(dxk.xkey.xkey_fingerprint(&secp), DerivationPath::master())
|
||||
});
|
||||
|
||||
path_to_child(self, &origin, Some(&dxk.derivation_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanDerive for DescriptorPublicKey {
|
||||
fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath> {
|
||||
match (self, DescriptorPublicKey::from(key.clone())) {
|
||||
(parent, child) if parent == &child => Some(DerivationPath::master()),
|
||||
(DescriptorPublicKey::XPub(parent), _) => {
|
||||
let origin = parent.origin.clone().unwrap_or_else(|| {
|
||||
let secp = Secp256k1::signing_only();
|
||||
(
|
||||
parent.xkey.xkey_fingerprint(&secp),
|
||||
DerivationPath::master(),
|
||||
)
|
||||
});
|
||||
KeySource::from(origin).can_derive(key)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_child(
|
||||
parent: &KeySource,
|
||||
child_origin: &(Fingerprint, DerivationPath),
|
||||
child_derivation: Option<&DerivationPath>,
|
||||
) -> Option<DerivationPath> {
|
||||
if parent.0 == child_origin.0 {
|
||||
let mut remaining_derivation =
|
||||
DerivationPath::from(child_origin.1[..].strip_prefix(&parent.1[..])?);
|
||||
remaining_derivation =
|
||||
remaining_derivation.extend(child_derivation.unwrap_or(&DerivationPath::master()));
|
||||
Some(remaining_derivation)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plan_satisfaction<Ak>(
|
||||
desc: &Descriptor<DefiniteDescriptorKey>,
|
||||
assets: &Assets<Ak>,
|
||||
) -> Option<Plan<Ak>>
|
||||
where
|
||||
Ak: CanDerive + Clone,
|
||||
{
|
||||
match desc {
|
||||
Descriptor::Bare(_) => todo!(),
|
||||
Descriptor::Pkh(_) => todo!(),
|
||||
Descriptor::Wpkh(_) => todo!(),
|
||||
Descriptor::Sh(_) => todo!(),
|
||||
Descriptor::Wsh(_) => todo!(),
|
||||
Descriptor::Tr(tr) => crate::plan_impls::plan_satisfaction_tr(tr, assets),
|
||||
}
|
||||
}
|
323
nursery/tmp_plan/bdk_tmp_plan/src/plan_impls.rs
Normal file
323
nursery/tmp_plan/bdk_tmp_plan/src/plan_impls.rs
Normal file
@ -0,0 +1,323 @@
|
||||
use bdk_chain::{bitcoin, miniscript};
|
||||
use bitcoin::locktime::{Height, Time};
|
||||
use miniscript::Terminal;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<Ak> TermPlan<Ak> {
|
||||
fn combine(self, other: Self) -> Option<Self> {
|
||||
let min_locktime = {
|
||||
match (self.min_locktime, other.min_locktime) {
|
||||
(Some(lhs), Some(rhs)) => {
|
||||
if lhs.is_same_unit(rhs) {
|
||||
Some(if lhs.to_consensus_u32() > rhs.to_consensus_u32() {
|
||||
lhs
|
||||
} else {
|
||||
rhs
|
||||
})
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => self.min_locktime.or(other.min_locktime),
|
||||
}
|
||||
};
|
||||
|
||||
let min_sequence = {
|
||||
match (self.min_sequence, other.min_sequence) {
|
||||
(Some(lhs), Some(rhs)) => {
|
||||
if lhs.is_height_locked() == rhs.is_height_locked() {
|
||||
Some(if lhs.to_consensus_u32() > rhs.to_consensus_u32() {
|
||||
lhs
|
||||
} else {
|
||||
rhs
|
||||
})
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => self.min_sequence.or(other.min_sequence),
|
||||
}
|
||||
};
|
||||
|
||||
let mut template = self.template;
|
||||
template.extend(other.template);
|
||||
|
||||
Some(Self {
|
||||
min_locktime,
|
||||
min_sequence,
|
||||
template,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn expected_size(&self) -> usize {
|
||||
self.template.iter().map(|step| step.expected_size()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
// impl crate::descriptor::Pkh<DefiniteDescriptorKey> {
|
||||
// pub(crate) fn plan_satisfaction<Ak>(&self, assets: &Assets<Ak>) -> Option<Plan<Ak>>
|
||||
// where
|
||||
// Ak: CanDerive + Clone,
|
||||
// {
|
||||
// let (asset_key, derivation_hint) = assets.keys.iter().find_map(|asset_key| {
|
||||
// let derivation_hint = asset_key.can_derive(self.as_inner())?;
|
||||
// Some((asset_key, derivation_hint))
|
||||
// })?;
|
||||
|
||||
// Some(Plan {
|
||||
// template: vec![TemplateItem::Sign(PlanKey {
|
||||
// asset_key: asset_key.clone(),
|
||||
// descriptor_key: self.as_inner().clone(),
|
||||
// derivation_hint,
|
||||
// })],
|
||||
// target: Target::Legacy,
|
||||
// set_locktime: None,
|
||||
// set_sequence: None,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl crate::descriptor::Wpkh<DefiniteDescriptorKey> {
|
||||
// pub(crate) fn plan_satisfaction<Ak>(&self, assets: &Assets<Ak>) -> Option<Plan<Ak>>
|
||||
// where
|
||||
// Ak: CanDerive + Clone,
|
||||
// {
|
||||
// let (asset_key, derivation_hint) = assets.keys.iter().find_map(|asset_key| {
|
||||
// let derivation_hint = asset_key.can_derive(self.as_inner())?;
|
||||
// Some((asset_key, derivation_hint))
|
||||
// })?;
|
||||
|
||||
// Some(Plan {
|
||||
// template: vec![TemplateItem::Sign(PlanKey {
|
||||
// asset_key: asset_key.clone(),
|
||||
// descriptor_key: self.as_inner().clone(),
|
||||
// derivation_hint,
|
||||
// })],
|
||||
// target: Target::Segwitv0,
|
||||
// set_locktime: None,
|
||||
// set_sequence: None,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
pub(crate) fn plan_satisfaction_tr<Ak>(
|
||||
tr: &miniscript::descriptor::Tr<DefiniteDescriptorKey>,
|
||||
assets: &Assets<Ak>,
|
||||
) -> Option<Plan<Ak>>
|
||||
where
|
||||
Ak: CanDerive + Clone,
|
||||
{
|
||||
let key_path_spend = assets.keys.iter().find_map(|asset_key| {
|
||||
let derivation_hint = asset_key.can_derive(tr.internal_key())?;
|
||||
Some((asset_key, derivation_hint))
|
||||
});
|
||||
|
||||
if let Some((asset_key, derivation_hint)) = key_path_spend {
|
||||
return Some(Plan {
|
||||
template: vec![TemplateItem::Sign(PlanKey {
|
||||
asset_key: asset_key.clone(),
|
||||
descriptor_key: tr.internal_key().clone(),
|
||||
derivation_hint,
|
||||
})],
|
||||
target: Target::Segwitv1 {
|
||||
tr: tr.clone(),
|
||||
tr_plan: TrSpend::KeySpend,
|
||||
},
|
||||
set_locktime: None,
|
||||
set_sequence: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mut plans = tr
|
||||
.iter_scripts()
|
||||
.filter_map(|(_, ms)| Some((ms, (plan_steps(&ms.node, assets)?))))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
plans.sort_by_cached_key(|(_, plan)| plan.expected_size());
|
||||
|
||||
let (script, best_plan) = plans.into_iter().next()?;
|
||||
|
||||
Some(Plan {
|
||||
target: Target::Segwitv1 {
|
||||
tr: tr.clone(),
|
||||
tr_plan: TrSpend::LeafSpend {
|
||||
script: script.encode(),
|
||||
leaf_version: LeafVersion::TapScript,
|
||||
},
|
||||
},
|
||||
set_locktime: best_plan.min_locktime.clone(),
|
||||
set_sequence: best_plan.min_sequence.clone(),
|
||||
template: best_plan.template,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TermPlan<Ak> {
|
||||
pub min_locktime: Option<LockTime>,
|
||||
pub min_sequence: Option<Sequence>,
|
||||
pub template: Vec<TemplateItem<Ak>>,
|
||||
}
|
||||
|
||||
impl<Ak> TermPlan<Ak> {
|
||||
fn new(template: Vec<TemplateItem<Ak>>) -> Self {
|
||||
TermPlan {
|
||||
template,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ak> Default for TermPlan<Ak> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_locktime: Default::default(),
|
||||
min_sequence: Default::default(),
|
||||
template: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_steps<Ak: Clone + CanDerive, Ctx: ScriptContext>(
|
||||
term: &Terminal<DefiniteDescriptorKey, Ctx>,
|
||||
assets: &Assets<Ak>,
|
||||
) -> Option<TermPlan<Ak>> {
|
||||
match term {
|
||||
Terminal::True => Some(TermPlan::new(vec![])),
|
||||
Terminal::False => return None,
|
||||
Terminal::PkH(key) => {
|
||||
let (asset_key, derivation_hint) = assets
|
||||
.keys
|
||||
.iter()
|
||||
.find_map(|asset_key| Some((asset_key, asset_key.can_derive(key)?)))?;
|
||||
Some(TermPlan::new(vec![
|
||||
TemplateItem::Sign(PlanKey {
|
||||
asset_key: asset_key.clone(),
|
||||
derivation_hint,
|
||||
descriptor_key: key.clone(),
|
||||
}),
|
||||
TemplateItem::Pk { key: key.clone() },
|
||||
]))
|
||||
}
|
||||
Terminal::PkK(key) => {
|
||||
let (asset_key, derivation_hint) = assets
|
||||
.keys
|
||||
.iter()
|
||||
.find_map(|asset_key| Some((asset_key, asset_key.can_derive(key)?)))?;
|
||||
Some(TermPlan::new(vec![TemplateItem::Sign(PlanKey {
|
||||
asset_key: asset_key.clone(),
|
||||
derivation_hint,
|
||||
descriptor_key: key.clone(),
|
||||
})]))
|
||||
}
|
||||
Terminal::RawPkH(_pk_hash) => {
|
||||
/* TODO */
|
||||
None
|
||||
}
|
||||
Terminal::After(locktime) => {
|
||||
let max_locktime = assets.max_locktime?;
|
||||
let locktime = LockTime::from(locktime);
|
||||
let (height, time) = match max_locktime {
|
||||
LockTime::Blocks(height) => (height, Time::from_consensus(0).unwrap()),
|
||||
LockTime::Seconds(seconds) => (Height::from_consensus(0).unwrap(), seconds),
|
||||
};
|
||||
if max_locktime.is_satisfied_by(height, time) {
|
||||
Some(TermPlan {
|
||||
min_locktime: Some(locktime),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Older(older) => {
|
||||
// FIXME: older should be a height or time not a sequence.
|
||||
let max_sequence = assets.txo_age?;
|
||||
//TODO: this whole thing is probably wrong but upstream should provide a way of
|
||||
// doing it properly.
|
||||
if max_sequence.is_height_locked() == older.is_height_locked() {
|
||||
if max_sequence.to_consensus_u32() >= older.to_consensus_u32() {
|
||||
Some(TermPlan {
|
||||
min_sequence: Some(*older),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Sha256(image) => {
|
||||
if assets.sha256.contains(&image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Sha256(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Hash256(image) => {
|
||||
if assets.hash256.contains(image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Hash256(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Ripemd160(image) => {
|
||||
if assets.ripemd160.contains(&image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Ripemd160(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Hash160(image) => {
|
||||
if assets.hash160.contains(&image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Hash160(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Alt(ms)
|
||||
| Terminal::Swap(ms)
|
||||
| Terminal::Check(ms)
|
||||
| Terminal::Verify(ms)
|
||||
| Terminal::NonZero(ms)
|
||||
| Terminal::ZeroNotEqual(ms) => plan_steps(&ms.node, assets),
|
||||
Terminal::DupIf(ms) => {
|
||||
let mut plan = plan_steps(&ms.node, assets)?;
|
||||
plan.template.push(TemplateItem::One);
|
||||
Some(plan)
|
||||
}
|
||||
Terminal::AndV(l, r) | Terminal::AndB(l, r) => {
|
||||
let lhs = plan_steps(&l.node, assets)?;
|
||||
let rhs = plan_steps(&r.node, assets)?;
|
||||
lhs.combine(rhs)
|
||||
}
|
||||
Terminal::AndOr(_, _, _) => todo!(),
|
||||
Terminal::OrB(_, _) => todo!(),
|
||||
Terminal::OrD(_, _) => todo!(),
|
||||
Terminal::OrC(_, _) => todo!(),
|
||||
Terminal::OrI(lhs, rhs) => {
|
||||
let lplan = plan_steps(&lhs.node, assets).map(|mut plan| {
|
||||
plan.template.push(TemplateItem::One);
|
||||
plan
|
||||
});
|
||||
let rplan = plan_steps(&rhs.node, assets).map(|mut plan| {
|
||||
plan.template.push(TemplateItem::Zero);
|
||||
plan
|
||||
});
|
||||
match (lplan, rplan) {
|
||||
(Some(lplan), Some(rplan)) => {
|
||||
if lplan.expected_size() <= rplan.expected_size() {
|
||||
Some(lplan)
|
||||
} else {
|
||||
Some(rplan)
|
||||
}
|
||||
}
|
||||
(lplan, rplan) => lplan.or(rplan),
|
||||
}
|
||||
}
|
||||
Terminal::Thresh(_, _) => todo!(),
|
||||
Terminal::Multi(_, _) => todo!(),
|
||||
Terminal::MultiA(_, _) => todo!(),
|
||||
}
|
||||
}
|
218
nursery/tmp_plan/bdk_tmp_plan/src/requirements.rs
Normal file
218
nursery/tmp_plan/bdk_tmp_plan/src/requirements.rs
Normal file
@ -0,0 +1,218 @@
|
||||
use bdk_chain::{bitcoin, collections::*, miniscript};
|
||||
use core::ops::Deref;
|
||||
|
||||
use bitcoin::{
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
psbt::Prevouts,
|
||||
secp256k1::{KeyPair, Message, PublicKey, Signing, Verification},
|
||||
util::{bip32, sighash, sighash::SighashCache, taproot},
|
||||
EcdsaSighashType, SchnorrSighashType, Transaction, TxOut, XOnlyPublicKey,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use miniscript::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
hash256,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Signatures and hash pre-images that must be provided to complete the plan.
|
||||
pub struct Requirements<Ak> {
|
||||
/// required signatures
|
||||
pub signatures: RequiredSignatures<Ak>,
|
||||
/// required sha256 pre-images
|
||||
pub sha256_images: HashSet<sha256::Hash>,
|
||||
/// required hash160 pre-images
|
||||
pub hash160_images: HashSet<hash160::Hash>,
|
||||
/// required hash256 pre-images
|
||||
pub hash256_images: HashSet<hash256::Hash>,
|
||||
/// required ripemd160 pre-images
|
||||
pub ripemd160_images: HashSet<ripemd160::Hash>,
|
||||
}
|
||||
|
||||
impl<Ak> Default for RequiredSignatures<Ak> {
|
||||
fn default() -> Self {
|
||||
RequiredSignatures::Legacy {
|
||||
keys: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ak> Default for Requirements<Ak> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
signatures: Default::default(),
|
||||
sha256_images: Default::default(),
|
||||
hash160_images: Default::default(),
|
||||
hash256_images: Default::default(),
|
||||
ripemd160_images: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ak> Requirements<Ak> {
|
||||
/// Whether any hash pre-images are required in the plan
|
||||
pub fn requires_hash_preimages(&self) -> bool {
|
||||
!(self.sha256_images.is_empty()
|
||||
&& self.hash160_images.is_empty()
|
||||
&& self.hash256_images.is_empty()
|
||||
&& self.ripemd160_images.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// The signatures required to complete the plan
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RequiredSignatures<Ak> {
|
||||
/// Legacy ECDSA signatures are required
|
||||
Legacy { keys: Vec<PlanKey<Ak>> },
|
||||
/// Segwitv0 ECDSA signatures are required
|
||||
Segwitv0 { keys: Vec<PlanKey<Ak>> },
|
||||
/// A Taproot key spend signature is required
|
||||
TapKey {
|
||||
/// the internal key
|
||||
plan_key: PlanKey<Ak>,
|
||||
/// The merkle root of the taproot output
|
||||
merkle_root: Option<TapBranchHash>,
|
||||
},
|
||||
/// Taproot script path signatures are required
|
||||
TapScript {
|
||||
/// The leaf hash of the script being used
|
||||
leaf_hash: TapLeafHash,
|
||||
/// The keys in the script that require signatures
|
||||
plan_keys: Vec<PlanKey<Ak>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SigningError {
|
||||
SigHashError(sighash::Error),
|
||||
DerivationError(bip32::Error),
|
||||
}
|
||||
|
||||
impl From<sighash::Error> for SigningError {
|
||||
fn from(e: sighash::Error) -> Self {
|
||||
Self::SigHashError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SigningError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SigningError::SigHashError(e) => e.fmt(f),
|
||||
SigningError::DerivationError(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bip32::Error> for SigningError {
|
||||
fn from(e: bip32::Error) -> Self {
|
||||
Self::DerivationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SigningError {}
|
||||
|
||||
impl RequiredSignatures<DescriptorPublicKey> {
|
||||
pub fn sign_with_keymap<T: Deref<Target = Transaction>>(
|
||||
&self,
|
||||
input_index: usize,
|
||||
keymap: &KeyMap,
|
||||
prevouts: &Prevouts<'_, impl core::borrow::Borrow<TxOut>>,
|
||||
schnorr_sighashty: Option<SchnorrSighashType>,
|
||||
_ecdsa_sighashty: Option<EcdsaSighashType>,
|
||||
sighash_cache: &mut SighashCache<T>,
|
||||
auth_data: &mut SatisfactionMaterial,
|
||||
secp: &Secp256k1<impl Signing + Verification>,
|
||||
) -> Result<bool, SigningError> {
|
||||
match self {
|
||||
RequiredSignatures::Legacy { .. } | RequiredSignatures::Segwitv0 { .. } => todo!(),
|
||||
RequiredSignatures::TapKey {
|
||||
plan_key,
|
||||
merkle_root,
|
||||
} => {
|
||||
let schnorr_sighashty = schnorr_sighashty.unwrap_or(SchnorrSighashType::Default);
|
||||
let sighash = sighash_cache.taproot_key_spend_signature_hash(
|
||||
input_index,
|
||||
prevouts,
|
||||
schnorr_sighashty,
|
||||
)?;
|
||||
let secret_key = match keymap.get(&plan_key.asset_key) {
|
||||
Some(secret_key) => secret_key,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let secret_key = match secret_key {
|
||||
DescriptorSecretKey::Single(single) => single.key.inner,
|
||||
DescriptorSecretKey::XPrv(xprv) => {
|
||||
xprv.xkey
|
||||
.derive_priv(&secp, &plan_key.derivation_hint)?
|
||||
.private_key
|
||||
}
|
||||
};
|
||||
|
||||
let pubkey = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
let x_only_pubkey = XOnlyPublicKey::from(pubkey);
|
||||
|
||||
let tweak =
|
||||
taproot::TapTweakHash::from_key_and_tweak(x_only_pubkey, merkle_root.clone());
|
||||
let keypair = KeyPair::from_secret_key(&secp, &secret_key.clone())
|
||||
.add_xonly_tweak(&secp, &tweak.to_scalar())
|
||||
.unwrap();
|
||||
|
||||
let msg = Message::from_slice(sighash.as_ref()).expect("Sighashes are 32 bytes");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
||||
|
||||
let bitcoin_sig = SchnorrSig {
|
||||
sig,
|
||||
hash_ty: schnorr_sighashty,
|
||||
};
|
||||
|
||||
auth_data
|
||||
.schnorr_sigs
|
||||
.insert(plan_key.descriptor_key.clone(), bitcoin_sig);
|
||||
Ok(true)
|
||||
}
|
||||
RequiredSignatures::TapScript {
|
||||
leaf_hash,
|
||||
plan_keys,
|
||||
} => {
|
||||
let sighash_type = schnorr_sighashty.unwrap_or(SchnorrSighashType::Default);
|
||||
let sighash = sighash_cache.taproot_script_spend_signature_hash(
|
||||
input_index,
|
||||
prevouts,
|
||||
*leaf_hash,
|
||||
sighash_type,
|
||||
)?;
|
||||
|
||||
let mut modified = false;
|
||||
|
||||
for plan_key in plan_keys {
|
||||
if let Some(secret_key) = keymap.get(&plan_key.asset_key) {
|
||||
let secret_key = match secret_key {
|
||||
DescriptorSecretKey::Single(single) => single.key.inner,
|
||||
DescriptorSecretKey::XPrv(xprv) => {
|
||||
xprv.xkey
|
||||
.derive_priv(&secp, &plan_key.derivation_hint)?
|
||||
.private_key
|
||||
}
|
||||
};
|
||||
let keypair = KeyPair::from_secret_key(&secp, &secret_key.clone());
|
||||
let msg =
|
||||
Message::from_slice(sighash.as_ref()).expect("Sighashes are 32 bytes");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
||||
let bitcoin_sig = SchnorrSig {
|
||||
sig,
|
||||
hash_ty: sighash_type,
|
||||
};
|
||||
|
||||
auth_data
|
||||
.schnorr_sigs
|
||||
.insert(plan_key.descriptor_key.clone(), bitcoin_sig);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
Ok(modified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
nursery/tmp_plan/bdk_tmp_plan/src/template.rs
Normal file
76
nursery/tmp_plan/bdk_tmp_plan/src/template.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use bdk_chain::{bitcoin, miniscript};
|
||||
use bitcoin::{
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
util::bip32::DerivationPath,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{hash256, varint_len, DefiniteDescriptorKey};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum TemplateItem<Ak> {
|
||||
Sign(PlanKey<Ak>),
|
||||
Pk { key: DefiniteDescriptorKey },
|
||||
One,
|
||||
Zero,
|
||||
Sha256(sha256::Hash),
|
||||
Hash256(hash256::Hash),
|
||||
Ripemd160(ripemd160::Hash),
|
||||
Hash160(hash160::Hash),
|
||||
}
|
||||
|
||||
/// A plan key contains the asset key originally provided along with key in the descriptor it
|
||||
/// purports to be able to derive for along with a "hint" on how to derive it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlanKey<Ak> {
|
||||
/// The key the planner will sign with
|
||||
pub asset_key: Ak,
|
||||
/// A hint from how to get from the asset key to the concrete key we need to sign with.
|
||||
pub derivation_hint: DerivationPath,
|
||||
/// The key that was in the descriptor that we are satisfying with the signature from the asset
|
||||
/// key.
|
||||
pub descriptor_key: DefiniteDescriptorKey,
|
||||
}
|
||||
|
||||
impl<Ak> TemplateItem<Ak> {
|
||||
pub fn expected_size(&self) -> usize {
|
||||
match self {
|
||||
TemplateItem::Sign { .. } => 64, /*size of sig TODO: take into consideration sighash falg*/
|
||||
TemplateItem::Pk { .. } => 32,
|
||||
TemplateItem::One => varint_len(1),
|
||||
TemplateItem::Zero => 0, /* zero means an empty witness element */
|
||||
// I'm not sure if it should be 32 here (it's a 20 byte hash) but that's what other
|
||||
// parts of the code were doing.
|
||||
TemplateItem::Hash160(_) | TemplateItem::Ripemd160(_) => 32,
|
||||
TemplateItem::Sha256(_) | TemplateItem::Hash256(_) => 32,
|
||||
}
|
||||
}
|
||||
|
||||
// this can only be called if we are sure that auth_data has what we need
|
||||
pub(super) fn to_witness_stack(&self, auth_data: &SatisfactionMaterial) -> Vec<Vec<u8>> {
|
||||
match self {
|
||||
TemplateItem::Sign(plan_key) => {
|
||||
vec![auth_data
|
||||
.schnorr_sigs
|
||||
.get(&plan_key.descriptor_key)
|
||||
.unwrap()
|
||||
.to_vec()]
|
||||
}
|
||||
TemplateItem::One => vec![vec![1]],
|
||||
TemplateItem::Zero => vec![vec![]],
|
||||
TemplateItem::Sha256(image) => {
|
||||
vec![auth_data.sha256_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Hash160(image) => {
|
||||
vec![auth_data.hash160_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Ripemd160(image) => {
|
||||
vec![auth_data.ripemd160_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Hash256(image) => {
|
||||
vec![auth_data.hash256_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Pk { key } => vec![key.to_public_key().to_bytes()],
|
||||
}
|
||||
}
|
||||
}
|
436
nursery/tmp_plan/src/lib.rs
Normal file
436
nursery/tmp_plan/src/lib.rs
Normal file
@ -0,0 +1,436 @@
|
||||
#![allow(unused)]
|
||||
#![allow(missing_docs)]
|
||||
//! A spending plan or *plan* for short is a representation of a particular spending path on a
|
||||
//! descriptor. This allows us to analayze a choice of spending path without producing any
|
||||
//! signatures or other witness data for it.
|
||||
//!
|
||||
//! To make a plan you provide the descriptor with "assets" like which keys you are able to use, hash
|
||||
//! pre-images you have access to, the current block height etc.
|
||||
//!
|
||||
//! Once you've got a plan it can tell you its expected satisfaction weight which can be useful for
|
||||
//! doing coin selection. Furthermore it provides which subset of those keys and hash pre-images you
|
||||
//! will actually need as well as what locktime or sequence number you need to set.
|
||||
//!
|
||||
//! Once you've obstained signatures, hash pre-images etc required by the plan, it can create a
|
||||
//! witness/script_sig for the input.
|
||||
use bdk_chain::{bitcoin, collections::*, miniscript};
|
||||
use bitcoin::{
|
||||
blockdata::{locktime::LockTime, transaction::Sequence},
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
secp256k1::Secp256k1,
|
||||
util::{
|
||||
address::WitnessVersion,
|
||||
bip32::{DerivationPath, Fingerprint, KeySource},
|
||||
taproot::{LeafVersion, TapBranchHash, TapLeafHash},
|
||||
},
|
||||
EcdsaSig, SchnorrSig, Script, TxIn, Witness,
|
||||
};
|
||||
use miniscript::{
|
||||
descriptor::{InnerXKey, Tr},
|
||||
hash256, DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, ScriptContext, ToPublicKey,
|
||||
};
|
||||
|
||||
pub(crate) fn varint_len(v: usize) -> usize {
|
||||
bitcoin::VarInt(v as u64).len() as usize
|
||||
}
|
||||
|
||||
mod plan_impls;
|
||||
mod requirements;
|
||||
mod template;
|
||||
pub use requirements::*;
|
||||
pub use template::PlanKey;
|
||||
use template::TemplateItem;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum TrSpend {
|
||||
KeySpend,
|
||||
LeafSpend {
|
||||
script: Script,
|
||||
leaf_version: LeafVersion,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Target {
|
||||
Legacy,
|
||||
Segwitv0 {
|
||||
script_code: Script,
|
||||
},
|
||||
Segwitv1 {
|
||||
tr: Tr<DefiniteDescriptorKey>,
|
||||
tr_plan: TrSpend,
|
||||
},
|
||||
}
|
||||
|
||||
impl Target {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A plan represents a particular spending path for a descriptor.
|
||||
///
|
||||
/// See the module level documentation for more info.
|
||||
pub struct Plan<AK> {
|
||||
template: Vec<TemplateItem<AK>>,
|
||||
target: Target,
|
||||
set_locktime: Option<LockTime>,
|
||||
set_sequence: Option<Sequence>,
|
||||
}
|
||||
|
||||
impl Default for Target {
|
||||
fn default() -> Self {
|
||||
Target::Legacy
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// Signatures and hash pre-images that can be used to complete a plan.
|
||||
pub struct SatisfactionMaterial {
|
||||
/// Schnorr signautres under their keys
|
||||
pub schnorr_sigs: BTreeMap<DefiniteDescriptorKey, SchnorrSig>,
|
||||
/// ECDSA signatures under their keys
|
||||
pub ecdsa_sigs: BTreeMap<DefiniteDescriptorKey, EcdsaSig>,
|
||||
/// SHA256 pre-images under their images
|
||||
pub sha256_preimages: BTreeMap<sha256::Hash, Vec<u8>>,
|
||||
/// hash160 pre-images under their images
|
||||
pub hash160_preimages: BTreeMap<hash160::Hash, Vec<u8>>,
|
||||
/// hash256 pre-images under their images
|
||||
pub hash256_preimages: BTreeMap<hash256::Hash, Vec<u8>>,
|
||||
/// ripemd160 pre-images under their images
|
||||
pub ripemd160_preimages: BTreeMap<ripemd160::Hash, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl<Ak> Plan<Ak>
|
||||
where
|
||||
Ak: Clone,
|
||||
{
|
||||
/// The expected satisfaction weight for the plan if it is completed.
|
||||
pub fn expected_weight(&self) -> usize {
|
||||
let script_sig_size = match self.target {
|
||||
Target::Legacy => unimplemented!(), // self
|
||||
// .template
|
||||
// .iter()
|
||||
// .map(|step| {
|
||||
// let size = step.expected_size();
|
||||
// size + push_opcode_size(size)
|
||||
// })
|
||||
// .sum()
|
||||
Target::Segwitv0 { .. } | Target::Segwitv1 { .. } => 1,
|
||||
};
|
||||
let witness_elem_sizes: Option<Vec<usize>> = match &self.target {
|
||||
Target::Legacy => None,
|
||||
Target::Segwitv0 { .. } => Some(
|
||||
self.template
|
||||
.iter()
|
||||
.map(|step| step.expected_size())
|
||||
.collect(),
|
||||
),
|
||||
Target::Segwitv1 { tr, tr_plan } => {
|
||||
let mut witness_elems = self
|
||||
.template
|
||||
.iter()
|
||||
.map(|step| step.expected_size())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let TrSpend::LeafSpend {
|
||||
script,
|
||||
leaf_version,
|
||||
} = tr_plan
|
||||
{
|
||||
let control_block = tr
|
||||
.spend_info()
|
||||
.control_block(&(script.clone(), *leaf_version))
|
||||
.expect("must exist");
|
||||
witness_elems.push(script.len());
|
||||
witness_elems.push(control_block.size());
|
||||
}
|
||||
|
||||
Some(witness_elems)
|
||||
}
|
||||
};
|
||||
|
||||
let witness_size: usize = match witness_elem_sizes {
|
||||
Some(elems) => {
|
||||
varint_len(elems.len())
|
||||
+ elems
|
||||
.into_iter()
|
||||
.map(|elem| varint_len(elem) + elem)
|
||||
.sum::<usize>()
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
script_sig_size * 4 + witness_size
|
||||
}
|
||||
|
||||
pub fn requirements(&self) -> Requirements<Ak> {
|
||||
match self.try_complete(&SatisfactionMaterial::default()) {
|
||||
PlanState::Complete { .. } => Requirements::default(),
|
||||
PlanState::Incomplete(requirements) => requirements,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_complete(&self, auth_data: &SatisfactionMaterial) -> PlanState<Ak> {
|
||||
let unsatisfied_items = self
|
||||
.template
|
||||
.iter()
|
||||
.filter(|step| match step {
|
||||
TemplateItem::Sign(key) => {
|
||||
!auth_data.schnorr_sigs.contains_key(&key.descriptor_key)
|
||||
}
|
||||
TemplateItem::Hash160(image) => !auth_data.hash160_preimages.contains_key(image),
|
||||
TemplateItem::Hash256(image) => !auth_data.hash256_preimages.contains_key(image),
|
||||
TemplateItem::Sha256(image) => !auth_data.sha256_preimages.contains_key(image),
|
||||
TemplateItem::Ripemd160(image) => {
|
||||
!auth_data.ripemd160_preimages.contains_key(image)
|
||||
}
|
||||
TemplateItem::Pk { .. } | TemplateItem::One | TemplateItem::Zero => false,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if unsatisfied_items.is_empty() {
|
||||
let mut witness = self
|
||||
.template
|
||||
.iter()
|
||||
.flat_map(|step| step.to_witness_stack(&auth_data))
|
||||
.collect::<Vec<_>>();
|
||||
match &self.target {
|
||||
Target::Segwitv0 { .. } => todo!(),
|
||||
Target::Legacy => todo!(),
|
||||
Target::Segwitv1 {
|
||||
tr_plan: TrSpend::KeySpend,
|
||||
..
|
||||
} => PlanState::Complete {
|
||||
final_script_sig: None,
|
||||
final_script_witness: Some(Witness::from_vec(witness)),
|
||||
},
|
||||
Target::Segwitv1 {
|
||||
tr,
|
||||
tr_plan:
|
||||
TrSpend::LeafSpend {
|
||||
script,
|
||||
leaf_version,
|
||||
},
|
||||
} => {
|
||||
let spend_info = tr.spend_info();
|
||||
let control_block = spend_info
|
||||
.control_block(&(script.clone(), *leaf_version))
|
||||
.expect("must exist");
|
||||
witness.push(script.clone().into_bytes());
|
||||
witness.push(control_block.serialize());
|
||||
|
||||
PlanState::Complete {
|
||||
final_script_sig: None,
|
||||
final_script_witness: Some(Witness::from_vec(witness)),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut requirements = Requirements::default();
|
||||
|
||||
match &self.target {
|
||||
Target::Legacy => {
|
||||
todo!()
|
||||
}
|
||||
Target::Segwitv0 { .. } => {
|
||||
todo!()
|
||||
}
|
||||
Target::Segwitv1 { tr, tr_plan } => {
|
||||
let spend_info = tr.spend_info();
|
||||
match tr_plan {
|
||||
TrSpend::KeySpend => match &self.template[..] {
|
||||
[TemplateItem::Sign(ref plan_key)] => {
|
||||
requirements.signatures = RequiredSignatures::TapKey {
|
||||
merkle_root: spend_info.merkle_root(),
|
||||
plan_key: plan_key.clone(),
|
||||
};
|
||||
}
|
||||
_ => unreachable!("tapkey spend will always have only one sign step"),
|
||||
},
|
||||
TrSpend::LeafSpend {
|
||||
script,
|
||||
leaf_version,
|
||||
} => {
|
||||
let leaf_hash = TapLeafHash::from_script(&script, *leaf_version);
|
||||
requirements.signatures = RequiredSignatures::TapScript {
|
||||
leaf_hash,
|
||||
plan_keys: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let required_signatures = match requirements.signatures {
|
||||
RequiredSignatures::Legacy { .. } => todo!(),
|
||||
RequiredSignatures::Segwitv0 { .. } => todo!(),
|
||||
RequiredSignatures::TapKey { .. } => return PlanState::Incomplete(requirements),
|
||||
RequiredSignatures::TapScript {
|
||||
plan_keys: ref mut keys,
|
||||
..
|
||||
} => keys,
|
||||
};
|
||||
|
||||
for step in unsatisfied_items {
|
||||
match step {
|
||||
TemplateItem::Sign(plan_key) => {
|
||||
required_signatures.push(plan_key.clone());
|
||||
}
|
||||
TemplateItem::Hash160(image) => {
|
||||
requirements.hash160_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Hash256(image) => {
|
||||
requirements.hash256_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Sha256(image) => {
|
||||
requirements.sha256_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Ripemd160(image) => {
|
||||
requirements.ripemd160_images.insert(image.clone());
|
||||
}
|
||||
TemplateItem::Pk { .. } | TemplateItem::One | TemplateItem::Zero => { /* no requirements */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlanState::Incomplete(requirements)
|
||||
}
|
||||
}
|
||||
|
||||
/// Witness version for the plan
|
||||
pub fn witness_version(&self) -> Option<WitnessVersion> {
|
||||
match self.target {
|
||||
Target::Legacy => None,
|
||||
Target::Segwitv0 { .. } => Some(WitnessVersion::V0),
|
||||
Target::Segwitv1 { .. } => Some(WitnessVersion::V1),
|
||||
}
|
||||
}
|
||||
|
||||
/// The minimum required locktime height or time on the transaction using the plan.
|
||||
pub fn required_locktime(&self) -> Option<LockTime> {
|
||||
self.set_locktime.clone()
|
||||
}
|
||||
|
||||
/// The minimum required sequence (height or time) on the input to satisfy the plan
|
||||
pub fn required_sequence(&self) -> Option<Sequence> {
|
||||
self.set_sequence.clone()
|
||||
}
|
||||
|
||||
/// The minmum required transaction version required on the transaction using the plan.
|
||||
pub fn min_version(&self) -> Option<u32> {
|
||||
if let Some(_) = self.set_sequence {
|
||||
Some(2)
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The returned value from [`Plan::try_complete`].
|
||||
pub enum PlanState<Ak> {
|
||||
/// The plan is complete
|
||||
Complete {
|
||||
/// The script sig that should be set on the input
|
||||
final_script_sig: Option<Script>,
|
||||
/// The witness that should be set on the input
|
||||
final_script_witness: Option<Witness>,
|
||||
},
|
||||
Incomplete(Requirements<Ak>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Assets<K> {
|
||||
pub keys: Vec<K>,
|
||||
pub txo_age: Option<Sequence>,
|
||||
pub max_locktime: Option<LockTime>,
|
||||
pub sha256: Vec<sha256::Hash>,
|
||||
pub hash256: Vec<hash256::Hash>,
|
||||
pub ripemd160: Vec<ripemd160::Hash>,
|
||||
pub hash160: Vec<hash160::Hash>,
|
||||
}
|
||||
|
||||
impl<K> Default for Assets<K> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keys: Default::default(),
|
||||
txo_age: Default::default(),
|
||||
max_locktime: Default::default(),
|
||||
sha256: Default::default(),
|
||||
hash256: Default::default(),
|
||||
ripemd160: Default::default(),
|
||||
hash160: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CanDerive {
|
||||
fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath>;
|
||||
}
|
||||
|
||||
impl CanDerive for KeySource {
|
||||
fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath> {
|
||||
match DescriptorPublicKey::from(key.clone()) {
|
||||
DescriptorPublicKey::Single(single_pub) => {
|
||||
path_to_child(self, single_pub.origin.as_ref()?, None)
|
||||
}
|
||||
DescriptorPublicKey::XPub(dxk) => {
|
||||
let origin = dxk.origin.clone().unwrap_or_else(|| {
|
||||
let secp = Secp256k1::signing_only();
|
||||
(dxk.xkey.xkey_fingerprint(&secp), DerivationPath::master())
|
||||
});
|
||||
|
||||
path_to_child(self, &origin, Some(&dxk.derivation_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanDerive for DescriptorPublicKey {
|
||||
fn can_derive(&self, key: &DefiniteDescriptorKey) -> Option<DerivationPath> {
|
||||
match (self, DescriptorPublicKey::from(key.clone())) {
|
||||
(parent, child) if parent == &child => Some(DerivationPath::master()),
|
||||
(DescriptorPublicKey::XPub(parent), _) => {
|
||||
let origin = parent.origin.clone().unwrap_or_else(|| {
|
||||
let secp = Secp256k1::signing_only();
|
||||
(
|
||||
parent.xkey.xkey_fingerprint(&secp),
|
||||
DerivationPath::master(),
|
||||
)
|
||||
});
|
||||
KeySource::from(origin).can_derive(key)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_child(
|
||||
parent: &KeySource,
|
||||
child_origin: &(Fingerprint, DerivationPath),
|
||||
child_derivation: Option<&DerivationPath>,
|
||||
) -> Option<DerivationPath> {
|
||||
if parent.0 == child_origin.0 {
|
||||
let mut remaining_derivation =
|
||||
DerivationPath::from(child_origin.1[..].strip_prefix(&parent.1[..])?);
|
||||
remaining_derivation =
|
||||
remaining_derivation.extend(child_derivation.unwrap_or(&DerivationPath::master()));
|
||||
Some(remaining_derivation)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plan_satisfaction<Ak>(
|
||||
desc: &Descriptor<DefiniteDescriptorKey>,
|
||||
assets: &Assets<Ak>,
|
||||
) -> Option<Plan<Ak>>
|
||||
where
|
||||
Ak: CanDerive + Clone,
|
||||
{
|
||||
match desc {
|
||||
Descriptor::Bare(_) => todo!(),
|
||||
Descriptor::Pkh(_) => todo!(),
|
||||
Descriptor::Wpkh(_) => todo!(),
|
||||
Descriptor::Sh(_) => todo!(),
|
||||
Descriptor::Wsh(_) => todo!(),
|
||||
Descriptor::Tr(tr) => crate::plan_impls::plan_satisfaction_tr(tr, assets),
|
||||
}
|
||||
}
|
323
nursery/tmp_plan/src/plan_impls.rs
Normal file
323
nursery/tmp_plan/src/plan_impls.rs
Normal file
@ -0,0 +1,323 @@
|
||||
use bdk_chain::{bitcoin, miniscript};
|
||||
use bitcoin::locktime::{Height, Time};
|
||||
use miniscript::Terminal;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<Ak> TermPlan<Ak> {
|
||||
fn combine(self, other: Self) -> Option<Self> {
|
||||
let min_locktime = {
|
||||
match (self.min_locktime, other.min_locktime) {
|
||||
(Some(lhs), Some(rhs)) => {
|
||||
if lhs.is_same_unit(rhs) {
|
||||
Some(if lhs.to_consensus_u32() > rhs.to_consensus_u32() {
|
||||
lhs
|
||||
} else {
|
||||
rhs
|
||||
})
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => self.min_locktime.or(other.min_locktime),
|
||||
}
|
||||
};
|
||||
|
||||
let min_sequence = {
|
||||
match (self.min_sequence, other.min_sequence) {
|
||||
(Some(lhs), Some(rhs)) => {
|
||||
if lhs.is_height_locked() == rhs.is_height_locked() {
|
||||
Some(if lhs.to_consensus_u32() > rhs.to_consensus_u32() {
|
||||
lhs
|
||||
} else {
|
||||
rhs
|
||||
})
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => self.min_sequence.or(other.min_sequence),
|
||||
}
|
||||
};
|
||||
|
||||
let mut template = self.template;
|
||||
template.extend(other.template);
|
||||
|
||||
Some(Self {
|
||||
min_locktime,
|
||||
min_sequence,
|
||||
template,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn expected_size(&self) -> usize {
|
||||
self.template.iter().map(|step| step.expected_size()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
// impl crate::descriptor::Pkh<DefiniteDescriptorKey> {
|
||||
// pub(crate) fn plan_satisfaction<Ak>(&self, assets: &Assets<Ak>) -> Option<Plan<Ak>>
|
||||
// where
|
||||
// Ak: CanDerive + Clone,
|
||||
// {
|
||||
// let (asset_key, derivation_hint) = assets.keys.iter().find_map(|asset_key| {
|
||||
// let derivation_hint = asset_key.can_derive(self.as_inner())?;
|
||||
// Some((asset_key, derivation_hint))
|
||||
// })?;
|
||||
|
||||
// Some(Plan {
|
||||
// template: vec![TemplateItem::Sign(PlanKey {
|
||||
// asset_key: asset_key.clone(),
|
||||
// descriptor_key: self.as_inner().clone(),
|
||||
// derivation_hint,
|
||||
// })],
|
||||
// target: Target::Legacy,
|
||||
// set_locktime: None,
|
||||
// set_sequence: None,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl crate::descriptor::Wpkh<DefiniteDescriptorKey> {
|
||||
// pub(crate) fn plan_satisfaction<Ak>(&self, assets: &Assets<Ak>) -> Option<Plan<Ak>>
|
||||
// where
|
||||
// Ak: CanDerive + Clone,
|
||||
// {
|
||||
// let (asset_key, derivation_hint) = assets.keys.iter().find_map(|asset_key| {
|
||||
// let derivation_hint = asset_key.can_derive(self.as_inner())?;
|
||||
// Some((asset_key, derivation_hint))
|
||||
// })?;
|
||||
|
||||
// Some(Plan {
|
||||
// template: vec![TemplateItem::Sign(PlanKey {
|
||||
// asset_key: asset_key.clone(),
|
||||
// descriptor_key: self.as_inner().clone(),
|
||||
// derivation_hint,
|
||||
// })],
|
||||
// target: Target::Segwitv0,
|
||||
// set_locktime: None,
|
||||
// set_sequence: None,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
pub(crate) fn plan_satisfaction_tr<Ak>(
|
||||
tr: &miniscript::descriptor::Tr<DefiniteDescriptorKey>,
|
||||
assets: &Assets<Ak>,
|
||||
) -> Option<Plan<Ak>>
|
||||
where
|
||||
Ak: CanDerive + Clone,
|
||||
{
|
||||
let key_path_spend = assets.keys.iter().find_map(|asset_key| {
|
||||
let derivation_hint = asset_key.can_derive(tr.internal_key())?;
|
||||
Some((asset_key, derivation_hint))
|
||||
});
|
||||
|
||||
if let Some((asset_key, derivation_hint)) = key_path_spend {
|
||||
return Some(Plan {
|
||||
template: vec![TemplateItem::Sign(PlanKey {
|
||||
asset_key: asset_key.clone(),
|
||||
descriptor_key: tr.internal_key().clone(),
|
||||
derivation_hint,
|
||||
})],
|
||||
target: Target::Segwitv1 {
|
||||
tr: tr.clone(),
|
||||
tr_plan: TrSpend::KeySpend,
|
||||
},
|
||||
set_locktime: None,
|
||||
set_sequence: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mut plans = tr
|
||||
.iter_scripts()
|
||||
.filter_map(|(_, ms)| Some((ms, (plan_steps(&ms.node, assets)?))))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
plans.sort_by_cached_key(|(_, plan)| plan.expected_size());
|
||||
|
||||
let (script, best_plan) = plans.into_iter().next()?;
|
||||
|
||||
Some(Plan {
|
||||
target: Target::Segwitv1 {
|
||||
tr: tr.clone(),
|
||||
tr_plan: TrSpend::LeafSpend {
|
||||
script: script.encode(),
|
||||
leaf_version: LeafVersion::TapScript,
|
||||
},
|
||||
},
|
||||
set_locktime: best_plan.min_locktime.clone(),
|
||||
set_sequence: best_plan.min_sequence.clone(),
|
||||
template: best_plan.template,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TermPlan<Ak> {
|
||||
pub min_locktime: Option<LockTime>,
|
||||
pub min_sequence: Option<Sequence>,
|
||||
pub template: Vec<TemplateItem<Ak>>,
|
||||
}
|
||||
|
||||
impl<Ak> TermPlan<Ak> {
|
||||
fn new(template: Vec<TemplateItem<Ak>>) -> Self {
|
||||
TermPlan {
|
||||
template,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ak> Default for TermPlan<Ak> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_locktime: Default::default(),
|
||||
min_sequence: Default::default(),
|
||||
template: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_steps<Ak: Clone + CanDerive, Ctx: ScriptContext>(
|
||||
term: &Terminal<DefiniteDescriptorKey, Ctx>,
|
||||
assets: &Assets<Ak>,
|
||||
) -> Option<TermPlan<Ak>> {
|
||||
match term {
|
||||
Terminal::True => Some(TermPlan::new(vec![])),
|
||||
Terminal::False => return None,
|
||||
Terminal::PkH(key) => {
|
||||
let (asset_key, derivation_hint) = assets
|
||||
.keys
|
||||
.iter()
|
||||
.find_map(|asset_key| Some((asset_key, asset_key.can_derive(key)?)))?;
|
||||
Some(TermPlan::new(vec![
|
||||
TemplateItem::Sign(PlanKey {
|
||||
asset_key: asset_key.clone(),
|
||||
derivation_hint,
|
||||
descriptor_key: key.clone(),
|
||||
}),
|
||||
TemplateItem::Pk { key: key.clone() },
|
||||
]))
|
||||
}
|
||||
Terminal::PkK(key) => {
|
||||
let (asset_key, derivation_hint) = assets
|
||||
.keys
|
||||
.iter()
|
||||
.find_map(|asset_key| Some((asset_key, asset_key.can_derive(key)?)))?;
|
||||
Some(TermPlan::new(vec![TemplateItem::Sign(PlanKey {
|
||||
asset_key: asset_key.clone(),
|
||||
derivation_hint,
|
||||
descriptor_key: key.clone(),
|
||||
})]))
|
||||
}
|
||||
Terminal::RawPkH(_pk_hash) => {
|
||||
/* TODO */
|
||||
None
|
||||
}
|
||||
Terminal::After(locktime) => {
|
||||
let max_locktime = assets.max_locktime?;
|
||||
let locktime = LockTime::from(locktime);
|
||||
let (height, time) = match max_locktime {
|
||||
LockTime::Blocks(height) => (height, Time::from_consensus(0).unwrap()),
|
||||
LockTime::Seconds(seconds) => (Height::from_consensus(0).unwrap(), seconds),
|
||||
};
|
||||
if max_locktime.is_satisfied_by(height, time) {
|
||||
Some(TermPlan {
|
||||
min_locktime: Some(locktime),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Older(older) => {
|
||||
// FIXME: older should be a height or time not a sequence.
|
||||
let max_sequence = assets.txo_age?;
|
||||
//TODO: this whole thing is probably wrong but upstream should provide a way of
|
||||
// doing it properly.
|
||||
if max_sequence.is_height_locked() == older.is_height_locked() {
|
||||
if max_sequence.to_consensus_u32() >= older.to_consensus_u32() {
|
||||
Some(TermPlan {
|
||||
min_sequence: Some(*older),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Sha256(image) => {
|
||||
if assets.sha256.contains(&image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Sha256(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Hash256(image) => {
|
||||
if assets.hash256.contains(image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Hash256(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Ripemd160(image) => {
|
||||
if assets.ripemd160.contains(&image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Ripemd160(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Hash160(image) => {
|
||||
if assets.hash160.contains(&image) {
|
||||
Some(TermPlan::new(vec![TemplateItem::Hash160(image.clone())]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Terminal::Alt(ms)
|
||||
| Terminal::Swap(ms)
|
||||
| Terminal::Check(ms)
|
||||
| Terminal::Verify(ms)
|
||||
| Terminal::NonZero(ms)
|
||||
| Terminal::ZeroNotEqual(ms) => plan_steps(&ms.node, assets),
|
||||
Terminal::DupIf(ms) => {
|
||||
let mut plan = plan_steps(&ms.node, assets)?;
|
||||
plan.template.push(TemplateItem::One);
|
||||
Some(plan)
|
||||
}
|
||||
Terminal::AndV(l, r) | Terminal::AndB(l, r) => {
|
||||
let lhs = plan_steps(&l.node, assets)?;
|
||||
let rhs = plan_steps(&r.node, assets)?;
|
||||
lhs.combine(rhs)
|
||||
}
|
||||
Terminal::AndOr(_, _, _) => todo!(),
|
||||
Terminal::OrB(_, _) => todo!(),
|
||||
Terminal::OrD(_, _) => todo!(),
|
||||
Terminal::OrC(_, _) => todo!(),
|
||||
Terminal::OrI(lhs, rhs) => {
|
||||
let lplan = plan_steps(&lhs.node, assets).map(|mut plan| {
|
||||
plan.template.push(TemplateItem::One);
|
||||
plan
|
||||
});
|
||||
let rplan = plan_steps(&rhs.node, assets).map(|mut plan| {
|
||||
plan.template.push(TemplateItem::Zero);
|
||||
plan
|
||||
});
|
||||
match (lplan, rplan) {
|
||||
(Some(lplan), Some(rplan)) => {
|
||||
if lplan.expected_size() <= rplan.expected_size() {
|
||||
Some(lplan)
|
||||
} else {
|
||||
Some(rplan)
|
||||
}
|
||||
}
|
||||
(lplan, rplan) => lplan.or(rplan),
|
||||
}
|
||||
}
|
||||
Terminal::Thresh(_, _) => todo!(),
|
||||
Terminal::Multi(_, _) => todo!(),
|
||||
Terminal::MultiA(_, _) => todo!(),
|
||||
}
|
||||
}
|
218
nursery/tmp_plan/src/requirements.rs
Normal file
218
nursery/tmp_plan/src/requirements.rs
Normal file
@ -0,0 +1,218 @@
|
||||
use bdk_chain::{bitcoin, collections::*, miniscript};
|
||||
use core::ops::Deref;
|
||||
|
||||
use bitcoin::{
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
psbt::Prevouts,
|
||||
secp256k1::{KeyPair, Message, PublicKey, Signing, Verification},
|
||||
util::{bip32, sighash, sighash::SighashCache, taproot},
|
||||
EcdsaSighashType, SchnorrSighashType, Transaction, TxOut, XOnlyPublicKey,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use miniscript::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
hash256,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Signatures and hash pre-images that must be provided to complete the plan.
|
||||
pub struct Requirements<Ak> {
|
||||
/// required signatures
|
||||
pub signatures: RequiredSignatures<Ak>,
|
||||
/// required sha256 pre-images
|
||||
pub sha256_images: HashSet<sha256::Hash>,
|
||||
/// required hash160 pre-images
|
||||
pub hash160_images: HashSet<hash160::Hash>,
|
||||
/// required hash256 pre-images
|
||||
pub hash256_images: HashSet<hash256::Hash>,
|
||||
/// required ripemd160 pre-images
|
||||
pub ripemd160_images: HashSet<ripemd160::Hash>,
|
||||
}
|
||||
|
||||
impl<Ak> Default for RequiredSignatures<Ak> {
|
||||
fn default() -> Self {
|
||||
RequiredSignatures::Legacy {
|
||||
keys: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ak> Default for Requirements<Ak> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
signatures: Default::default(),
|
||||
sha256_images: Default::default(),
|
||||
hash160_images: Default::default(),
|
||||
hash256_images: Default::default(),
|
||||
ripemd160_images: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ak> Requirements<Ak> {
|
||||
/// Whether any hash pre-images are required in the plan
|
||||
pub fn requires_hash_preimages(&self) -> bool {
|
||||
!(self.sha256_images.is_empty()
|
||||
&& self.hash160_images.is_empty()
|
||||
&& self.hash256_images.is_empty()
|
||||
&& self.ripemd160_images.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// The signatures required to complete the plan
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RequiredSignatures<Ak> {
|
||||
/// Legacy ECDSA signatures are required
|
||||
Legacy { keys: Vec<PlanKey<Ak>> },
|
||||
/// Segwitv0 ECDSA signatures are required
|
||||
Segwitv0 { keys: Vec<PlanKey<Ak>> },
|
||||
/// A Taproot key spend signature is required
|
||||
TapKey {
|
||||
/// the internal key
|
||||
plan_key: PlanKey<Ak>,
|
||||
/// The merkle root of the taproot output
|
||||
merkle_root: Option<TapBranchHash>,
|
||||
},
|
||||
/// Taproot script path signatures are required
|
||||
TapScript {
|
||||
/// The leaf hash of the script being used
|
||||
leaf_hash: TapLeafHash,
|
||||
/// The keys in the script that require signatures
|
||||
plan_keys: Vec<PlanKey<Ak>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SigningError {
|
||||
SigHashError(sighash::Error),
|
||||
DerivationError(bip32::Error),
|
||||
}
|
||||
|
||||
impl From<sighash::Error> for SigningError {
|
||||
fn from(e: sighash::Error) -> Self {
|
||||
Self::SigHashError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for SigningError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
SigningError::SigHashError(e) => e.fmt(f),
|
||||
SigningError::DerivationError(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bip32::Error> for SigningError {
|
||||
fn from(e: bip32::Error) -> Self {
|
||||
Self::DerivationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SigningError {}
|
||||
|
||||
impl RequiredSignatures<DescriptorPublicKey> {
|
||||
pub fn sign_with_keymap<T: Deref<Target = Transaction>>(
|
||||
&self,
|
||||
input_index: usize,
|
||||
keymap: &KeyMap,
|
||||
prevouts: &Prevouts<'_, impl core::borrow::Borrow<TxOut>>,
|
||||
schnorr_sighashty: Option<SchnorrSighashType>,
|
||||
_ecdsa_sighashty: Option<EcdsaSighashType>,
|
||||
sighash_cache: &mut SighashCache<T>,
|
||||
auth_data: &mut SatisfactionMaterial,
|
||||
secp: &Secp256k1<impl Signing + Verification>,
|
||||
) -> Result<bool, SigningError> {
|
||||
match self {
|
||||
RequiredSignatures::Legacy { .. } | RequiredSignatures::Segwitv0 { .. } => todo!(),
|
||||
RequiredSignatures::TapKey {
|
||||
plan_key,
|
||||
merkle_root,
|
||||
} => {
|
||||
let schnorr_sighashty = schnorr_sighashty.unwrap_or(SchnorrSighashType::Default);
|
||||
let sighash = sighash_cache.taproot_key_spend_signature_hash(
|
||||
input_index,
|
||||
prevouts,
|
||||
schnorr_sighashty,
|
||||
)?;
|
||||
let secret_key = match keymap.get(&plan_key.asset_key) {
|
||||
Some(secret_key) => secret_key,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let secret_key = match secret_key {
|
||||
DescriptorSecretKey::Single(single) => single.key.inner,
|
||||
DescriptorSecretKey::XPrv(xprv) => {
|
||||
xprv.xkey
|
||||
.derive_priv(&secp, &plan_key.derivation_hint)?
|
||||
.private_key
|
||||
}
|
||||
};
|
||||
|
||||
let pubkey = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
let x_only_pubkey = XOnlyPublicKey::from(pubkey);
|
||||
|
||||
let tweak =
|
||||
taproot::TapTweakHash::from_key_and_tweak(x_only_pubkey, merkle_root.clone());
|
||||
let keypair = KeyPair::from_secret_key(&secp, &secret_key.clone())
|
||||
.add_xonly_tweak(&secp, &tweak.to_scalar())
|
||||
.unwrap();
|
||||
|
||||
let msg = Message::from_slice(sighash.as_ref()).expect("Sighashes are 32 bytes");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
||||
|
||||
let bitcoin_sig = SchnorrSig {
|
||||
sig,
|
||||
hash_ty: schnorr_sighashty,
|
||||
};
|
||||
|
||||
auth_data
|
||||
.schnorr_sigs
|
||||
.insert(plan_key.descriptor_key.clone(), bitcoin_sig);
|
||||
Ok(true)
|
||||
}
|
||||
RequiredSignatures::TapScript {
|
||||
leaf_hash,
|
||||
plan_keys,
|
||||
} => {
|
||||
let sighash_type = schnorr_sighashty.unwrap_or(SchnorrSighashType::Default);
|
||||
let sighash = sighash_cache.taproot_script_spend_signature_hash(
|
||||
input_index,
|
||||
prevouts,
|
||||
*leaf_hash,
|
||||
sighash_type,
|
||||
)?;
|
||||
|
||||
let mut modified = false;
|
||||
|
||||
for plan_key in plan_keys {
|
||||
if let Some(secret_key) = keymap.get(&plan_key.asset_key) {
|
||||
let secret_key = match secret_key {
|
||||
DescriptorSecretKey::Single(single) => single.key.inner,
|
||||
DescriptorSecretKey::XPrv(xprv) => {
|
||||
xprv.xkey
|
||||
.derive_priv(&secp, &plan_key.derivation_hint)?
|
||||
.private_key
|
||||
}
|
||||
};
|
||||
let keypair = KeyPair::from_secret_key(&secp, &secret_key.clone());
|
||||
let msg =
|
||||
Message::from_slice(sighash.as_ref()).expect("Sighashes are 32 bytes");
|
||||
let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
|
||||
let bitcoin_sig = SchnorrSig {
|
||||
sig,
|
||||
hash_ty: sighash_type,
|
||||
};
|
||||
|
||||
auth_data
|
||||
.schnorr_sigs
|
||||
.insert(plan_key.descriptor_key.clone(), bitcoin_sig);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
Ok(modified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
nursery/tmp_plan/src/template.rs
Normal file
76
nursery/tmp_plan/src/template.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use bdk_chain::{bitcoin, miniscript};
|
||||
use bitcoin::{
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
util::bip32::DerivationPath,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{hash256, varint_len, DefiniteDescriptorKey};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum TemplateItem<Ak> {
|
||||
Sign(PlanKey<Ak>),
|
||||
Pk { key: DefiniteDescriptorKey },
|
||||
One,
|
||||
Zero,
|
||||
Sha256(sha256::Hash),
|
||||
Hash256(hash256::Hash),
|
||||
Ripemd160(ripemd160::Hash),
|
||||
Hash160(hash160::Hash),
|
||||
}
|
||||
|
||||
/// A plan key contains the asset key originally provided along with key in the descriptor it
|
||||
/// purports to be able to derive for along with a "hint" on how to derive it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlanKey<Ak> {
|
||||
/// The key the planner will sign with
|
||||
pub asset_key: Ak,
|
||||
/// A hint from how to get from the asset key to the concrete key we need to sign with.
|
||||
pub derivation_hint: DerivationPath,
|
||||
/// The key that was in the descriptor that we are satisfying with the signature from the asset
|
||||
/// key.
|
||||
pub descriptor_key: DefiniteDescriptorKey,
|
||||
}
|
||||
|
||||
impl<Ak> TemplateItem<Ak> {
|
||||
pub fn expected_size(&self) -> usize {
|
||||
match self {
|
||||
TemplateItem::Sign { .. } => 64, /*size of sig TODO: take into consideration sighash falg*/
|
||||
TemplateItem::Pk { .. } => 32,
|
||||
TemplateItem::One => varint_len(1),
|
||||
TemplateItem::Zero => 0, /* zero means an empty witness element */
|
||||
// I'm not sure if it should be 32 here (it's a 20 byte hash) but that's what other
|
||||
// parts of the code were doing.
|
||||
TemplateItem::Hash160(_) | TemplateItem::Ripemd160(_) => 32,
|
||||
TemplateItem::Sha256(_) | TemplateItem::Hash256(_) => 32,
|
||||
}
|
||||
}
|
||||
|
||||
// this can only be called if we are sure that auth_data has what we need
|
||||
pub(super) fn to_witness_stack(&self, auth_data: &SatisfactionMaterial) -> Vec<Vec<u8>> {
|
||||
match self {
|
||||
TemplateItem::Sign(plan_key) => {
|
||||
vec![auth_data
|
||||
.schnorr_sigs
|
||||
.get(&plan_key.descriptor_key)
|
||||
.unwrap()
|
||||
.to_vec()]
|
||||
}
|
||||
TemplateItem::One => vec![vec![1]],
|
||||
TemplateItem::Zero => vec![vec![]],
|
||||
TemplateItem::Sha256(image) => {
|
||||
vec![auth_data.sha256_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Hash160(image) => {
|
||||
vec![auth_data.hash160_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Ripemd160(image) => {
|
||||
vec![auth_data.ripemd160_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Hash256(image) => {
|
||||
vec![auth_data.hash256_preimages.get(image).unwrap().to_vec()]
|
||||
}
|
||||
TemplateItem::Pk { key } => vec![key.to_public_key().to_bytes()],
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user