diff --git a/api-docs/kotlin/src/main/kotlin/org/bitcoindevkit/bdk.kt b/api-docs/kotlin/src/main/kotlin/org/bitcoindevkit/bdk.kt index e9edd735..abce77f4 100644 --- a/api-docs/kotlin/src/main/kotlin/org/bitcoindevkit/bdk.kt +++ b/api-docs/kotlin/src/main/kotlin/org/bitcoindevkit/bdk.kt @@ -521,6 +521,9 @@ class Wallet( /** Return the list of unspent outputs of this wallet. Note that this method only operates on the internal database, which first needs to be [Wallet.sync] manually. */ fun listUnspent(): List {} + + /** Get the corresponding PSBT Input for a LocalUtxo. */ + fun getPsbtInput(utxo: LocalUtxo, sighashType: PsbtSighashType?, onlyWitnessUtxo: Boolean): Input {} } /** @@ -557,6 +560,43 @@ class TxBuilder() { /** Add an outpoint to the internal list of UTXOs that must be spent. These have priority over the "unspendable" utxos, meaning that if a utxo is present both in the "utxos" and the "unspendable" list, it will be spent. */ fun addUtxo(outpoint: OutPoint): TxBuilder {} + /** + * Add a foreign UTXO i.e. a UTXO not owned by this wallet. + * At a minimum to add a foreign UTXO we need: + * outpoint: To add it to the raw transaction. + * psbt_input: To know the value. + * satisfaction_weight: To know how much weight/vbytes the input will add to the transaction for fee calculation. + * + * There are several security concerns about adding foreign UTXOs that application developers should consider. + * First, how do you know the value of the input is correct? If a non_witness_utxo is provided in the + * psbt_input then this method implicitly verifies the value by checking it against the transaction. + * If only a witness_utxo is provided then this method does not verify the value but just takes it as a + * given – it is up to you to check that whoever sent you the input_psbt was not lying! + * + * Secondly, you must somehow provide satisfaction_weight of the input. Depending on your application + * it may be important that this be known precisely. If not, a malicious counterparty may fool you into putting in + * a value that is too low, giving the transaction a lower than expected feerate. They could also fool you + * into putting a value that is too high causing you to pay a fee that is too high. The party who is broadcasting + * the transaction can of course check the real input weight matches the expected weight prior to broadcasting. + * + * To guarantee the satisfaction_weight is correct, you can require the party providing the psbt_input provide + * a miniscript descriptor for the input so you can check it against the script_pubkey and then ask it for the + * max_satisfaction_weight. + * + * Errors + * This method returns errors in the following circumstances: + * The psbt_input does not contain a witness_utxo or non_witness_utxo. + * The data in non_witness_utxo does not match what is in outpoint. + * + * Note unless you set only_witness_utxo any non-taproot psbt_input you pass to this method must + * have non_witness_utxo set otherwise you will get an error when finish is called. + * + * @param outpoint The outpoint of the UTXO to add. + * @param input The PSBT input that contains the value of the UTXO. + * @param satisfactionWeight how much weight/vbytes the input will add to the transaction for fee calculation. + */ + fun addForeignUtxo(outpoint: OutPoint, input: Input, satisfactionWeight: ULong): TxBuilder {} + /** * Add the list of outpoints to the internal list of UTXOs that must be spent. If an error * occurs while adding any of the UTXOs then none of them are added and the error is returned. @@ -715,6 +755,72 @@ class DescriptorSecretKey(network: Network, mnemonic: Mnemonic, password: String fun asString(): String {} } +/** + * A Signature hash type for the corresponding input. As of taproot upgrade, the signature hash + * type can be either [`EcdsaSighashType`] or [`SchnorrSighashType`] but it is not possible to know + * directly which signature hash type the user is dealing with. Therefore, the user is responsible + * for converting to/from [`PsbtSighashType`] from/to the desired signature hash type they need. + * + */ +class PsbtSighashType() { + + companion object { + fun `fromEcdsa`(`ecdsaHashTy`: EcdsaSighashType): PsbtSighashType + fun `fromSchnorr`(`schnorrHashTy`: SchnorrSighashType): PsbtSighashType + } +} + +/** + * Hashtype of an input's signature, encoded in the last byte of the signature. + * Fixed values so they can be cast as integer types for encoding (see also + * `SchnorrSighashType`). + */ +enum class EcdsaSighashType { + /** 0x1: Sign all outputs. */ + ALL, + /** 0x2: Sign no outputs --- anyone can choose the destination. */ + NONE, + /** + * 0x3: Sign the output whose index matches this input's index. If none exists, + * sign the hash `0000000000000000000000000000000000000000000000000000000000000001`. + * (This rule is probably an unintentional C++ism, but it's consensus so we have + * to follow it.) + */ + SINGLE, + /** 0x81: Sign all outputs but only this input. */ + ALL_PLUS_ANYONE_CAN_PAY, + /** 0x82: Sign no outputs and only this input. */ + NONE_PLUS_ANYONE_CAN_PAY, + /** 0x83: Sign one output and only this input (see `Single` for what "one output" means). */ + SINGLE_PLUS_ANYONE_CAN_PAY; +} + +/** + * Hashtype of an input's signature, encoded in the last byte of the signature. + * Fixed values so they can be cast as integer types for encoding. + */ +enum class SchnorrSighashType { + /** 0x0: Used when not explicitly specified, defaults to [`SchnorrSighashType::All`] */ + DEFAULT, + /** 0x1: Sign all outputs. */ + ALL, + /** 0x2: Sign no outputs --- anyone can choose the destination. */ + NONE, + /** + * 0x3: Sign the output whose index matches this input's index. If none exists, + * sign the hash `0000000000000000000000000000000000000000000000000000000000000001`. + * (This rule is probably an unintentional C++ism, but it's consensus so we have + * to follow it.) + */ + SINGLE, + /** 0x81: Sign all outputs but only this input. */ + ALL_PLUS_ANYONE_CAN_PAY, + /** 0x82: Sign no outputs and only this input. */ + NONE_PLUS_ANYONE_CAN_PAY, + /** 0x83: Sign one output and only this input (see `Single` for what "one output" means). */ + SINGLE_PLUS_ANYONE_CAN_PAY; +} + /** * An extended public key. * @@ -785,6 +891,21 @@ class Descriptor(descriptor: String, network: Network) { */ fun newBip84Public(publicKey: DescriptorPublicKey, fingerprint: String, keychain: KeychainKind, network: Network) {} + /** + * Computes an upper bound on the weight of a satisfying witness to the + * transaction. + * + * Assumes all ec-signatures are 73 bytes, including push opcode and + * sighash suffix. Includes the weight of the VarInts encoding the + * scriptSig and witness stack length. + * + * # Errors + * When the descriptor is impossible to satisfy (ex: sh(OP_FALSE)). + * + * @return max satisfaction weight + */ + fun maxSatisfactionWeight(): UInt {} + /** Return the public version of the output descriptor. */ fun asString(): String {} @@ -895,6 +1016,20 @@ enum class WitnessVersion { V0, V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15, V16 } +/** + * A key-value map for an input of the corresponding index in the unsigned transaction. + * + * @constructor Create a new PSBT Input from a JSON String. + */ +class Input(inputJson: String) { + + /** + * Serialize the PSBT Input data structure as a JSON String. + * + */ + fun jsonSerialize(): String; +} + /** * Mnemonic phrases are a human-readable version of the private keys. Supported number of words are 12, 15, 18, 21 and 24. * diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index 6a31deaa..1d5e3f95 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -235,6 +235,9 @@ interface Wallet { [Throws=BdkError] sequence list_unspent(); + [Throws=BdkError] + Input get_psbt_input(LocalUtxo utxo, PsbtSighashType? sighash_type, boolean only_witness_utxo); + [Throws=BdkError] sequence list_transactions(boolean include_raw); @@ -314,6 +317,40 @@ interface PartiallySignedTransaction { string json_serialize(); }; +interface Input { + [Name=from_json,Throws=BdkError] + constructor(string input_json); + + string json_serialize(); +}; + +interface PsbtSighashType { + [Name=from_ecdsa] + constructor(EcdsaSighashType ecdsa_hash_ty); + + [Name=from_schnorr] + constructor(SchnorrSighashType schnorr_hash_ty); +}; + +enum EcdsaSighashType { + "All", + "None", + "Single", + "AllPlusAnyoneCanPay", + "NonePlusAnyoneCanPay", + "SinglePlusAnyoneCanPay", +}; + +enum SchnorrSighashType { + "Default", + "All", + "None", + "Single", + "AllPlusAnyoneCanPay", + "NonePlusAnyoneCanPay", + "SinglePlusAnyoneCanPay", +}; + dictionary TxBuilderResult { PartiallySignedTransaction psbt; TransactionDetails transaction_details; @@ -326,9 +363,11 @@ interface TxBuilder { TxBuilder add_unspendable(OutPoint unspendable); + TxBuilder add_utxos(sequence outpoints); + TxBuilder add_utxo(OutPoint outpoint); - TxBuilder add_utxos(sequence outpoints); + TxBuilder add_foreign_utxo(OutPoint outpoint, Input psbt_input, u64 satisfaction_weight); TxBuilder do_not_spend_change(); @@ -442,6 +481,9 @@ interface Descriptor { [Name=new_bip84_public] constructor(DescriptorPublicKey public_key, string fingerprint, KeychainKind keychain, Network network); + [Throws=BdkError] + u64 max_satisfaction_weight(); + string as_string(); string as_string_private(); diff --git a/bdk-ffi/src/descriptor.rs b/bdk-ffi/src/descriptor.rs index e33df686..a8062ed2 100644 --- a/bdk-ffi/src/descriptor.rs +++ b/bdk-ffi/src/descriptor.rs @@ -10,6 +10,7 @@ use bdk::template::{ Bip44, Bip44Public, Bip49, Bip49Public, Bip84, Bip84Public, DescriptorTemplate, }; use bdk::KeychainKind; +use std::convert::TryFrom; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; @@ -183,6 +184,22 @@ impl Descriptor { } } + /// Computes an upper bound on the weight of a satisfying witness to the + /// transaction. + /// + /// Assumes all ec-signatures are 73 bytes, including push opcode and + /// sighash suffix. Includes the weight of the VarInts encoding the + /// scriptSig and witness stack length. + /// + /// # Errors + /// When the descriptor is impossible to satisfy (ex: sh(OP_FALSE)). + pub(crate) fn max_satisfaction_weight(&self) -> Result { + self.extended_descriptor + .max_satisfaction_weight() + .map(|w| u64::try_from(w).unwrap()) + .map_err(BdkError::Miniscript) + } + pub(crate) fn as_string_private(&self) -> String { let descriptor = &self.extended_descriptor; let key_map = &self.key_map; diff --git a/bdk-ffi/src/lib.rs b/bdk-ffi/src/lib.rs index 4c1e70b3..7c5becf9 100644 --- a/bdk-ffi/src/lib.rs +++ b/bdk-ffi/src/lib.rs @@ -12,7 +12,9 @@ use crate::database::DatabaseConfig; use crate::descriptor::Descriptor; use crate::keys::DerivationPath; use crate::keys::{DescriptorPublicKey, DescriptorSecretKey, Mnemonic}; +use crate::psbt::Input; use crate::psbt::PartiallySignedTransaction; +use crate::psbt::PsbtSighashType; use crate::wallet::SignOptions; use crate::wallet::{BumpFeeTxBuilder, TxBuilder, Wallet}; use bdk::bitcoin::blockdata::script::Script as BdkScript; @@ -24,6 +26,7 @@ use bdk::bitcoin::util::address::{Payload as BdkPayload, WitnessVersion}; use bdk::bitcoin::{ Address as BdkAddress, Network, OutPoint as BdkOutPoint, Transaction as BdkTransaction, Txid, }; +use bdk::bitcoin::{EcdsaSighashType, SchnorrSighashType}; use bdk::blockchain::Progress as BdkProgress; use bdk::database::any::{SledDbConfiguration, SqliteDbConfiguration}; use bdk::keys::bip39::WordCount; @@ -161,6 +164,15 @@ impl From<&OutPoint> for BdkOutPoint { } } +impl From for BdkOutPoint { + fn from(outpoint: OutPoint) -> Self { + BdkOutPoint { + txid: Txid::from_str(&outpoint.txid).unwrap(), + vout: outpoint.vout, + } + } +} + pub struct Balance { // All coinbase outputs not yet matured pub immature: u64, @@ -209,6 +221,15 @@ impl From<&BdkTxOut> for TxOut { } } +impl From for BdkTxOut { + fn from(tx_out: TxOut) -> Self { + BdkTxOut { + value: tx_out.value, + script_pubkey: tx_out.script_pubkey.script.clone(), + } + } +} + pub struct LocalUtxo { outpoint: OutPoint, txout: TxOut, @@ -235,6 +256,17 @@ impl From for LocalUtxo { } } +impl From for BdkLocalUtxo { + fn from(local_utxo: LocalUtxo) -> Self { + BdkLocalUtxo { + outpoint: local_utxo.outpoint.into(), + txout: local_utxo.txout.into(), + keychain: local_utxo.keychain, + is_spent: local_utxo.is_spent, + } + } +} + /// Trait that logs at level INFO every update received (if any). pub trait Progress: Send + Sync + 'static { /// Send a new progress update. The progress value should be in the range 0.0 - 100.0, and the message value is an diff --git a/bdk-ffi/src/psbt.rs b/bdk-ffi/src/psbt.rs index 9529c773..7117ad12 100644 --- a/bdk-ffi/src/psbt.rs +++ b/bdk-ffi/src/psbt.rs @@ -1,5 +1,8 @@ use bdk::bitcoin::hashes::hex::ToHex; +use bdk::bitcoin::util::psbt::Input as BdkInput; use bdk::bitcoin::util::psbt::PartiallySignedTransaction as BdkPartiallySignedTransaction; +use bdk::bitcoin::util::psbt::PsbtSighashType as BdkPsbtSighashType; +use bdk::bitcoin::{EcdsaSighashType, SchnorrSighashType}; use bdk::bitcoincore_rpc::jsonrpc::serde_json; use bdk::psbt::PsbtUtils; use std::ops::Deref; @@ -76,6 +79,67 @@ impl PartiallySignedTransaction { } } +/// A key-value map for an input of the corresponding index in the unsigned transaction. +#[derive(Clone, Debug)] +pub(crate) struct Input { + inner: BdkInput, +} + +impl Input { + /// Create a new PSBT Input from a JSON String. + pub(crate) fn from_json(input_json: String) -> Result { + let input = serde_json::from_str(input_json.as_str())?; + Ok(Self { inner: input }) + } + + /// Serialize the PSBT Input data structure as a JSON String. + pub(crate) fn json_serialize(&self) -> String { + let input = &self.inner; + serde_json::to_string(input).unwrap() + } +} + +impl From for Input { + fn from(input: BdkInput) -> Self { + Input { inner: input } + } +} + +impl From for BdkInput { + fn from(input: Input) -> Self { + input.inner + } +} + +/// A Signature hash type for the corresponding input. As of taproot upgrade, the signature hash +/// type can be either [`EcdsaSighashType`] or [`SchnorrSighashType`] but it is not possible to know +/// directly which signature hash type the user is dealing with. Therefore, the user is responsible +/// for converting to/from [`PsbtSighashType`] from/to the desired signature hash type they need. +#[derive(Debug)] +pub(crate) struct PsbtSighashType { + inner: BdkPsbtSighashType, +} + +impl PsbtSighashType { + pub(crate) fn from_ecdsa(ecdsa_hash_ty: EcdsaSighashType) -> Self { + PsbtSighashType { + inner: BdkPsbtSighashType::from(ecdsa_hash_ty), + } + } + + pub(crate) fn from_schnorr(schnorr_hash_ty: SchnorrSighashType) -> Self { + PsbtSighashType { + inner: BdkPsbtSighashType::from(schnorr_hash_ty), + } + } +} + +impl From<&PsbtSighashType> for BdkPsbtSighashType { + fn from(psbt_hash_ty: &PsbtSighashType) -> Self { + psbt_hash_ty.inner + } +} + // The goal of these tests to to ensure `bdk-ffi` intermediate code correctly calls `bdk` APIs. // These tests should not be used to verify `bdk` behavior that is already tested in the `bdk` // crate. diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index c8549a0b..7e5613d2 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -1,3 +1,11 @@ +use crate::blockchain::Blockchain; +use crate::database::DatabaseConfig; +use crate::descriptor::Descriptor; +use crate::psbt::{Input, PartiallySignedTransaction, PsbtSighashType}; +use crate::{ + AddressIndex, AddressInfo, Balance, BdkError, LocalUtxo, OutPoint, Progress, ProgressHolder, + RbfValue, Script, ScriptAmount, TransactionDetails, TxBuilderResult, +}; use bdk::bitcoin::blockdata::script::Script as BdkScript; use bdk::bitcoin::{Address as BdkAddress, Network, OutPoint as BdkOutPoint, Sequence, Txid}; use bdk::database::any::AnyDatabase; @@ -12,15 +20,6 @@ use std::ops::Deref; use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; -use crate::blockchain::Blockchain; -use crate::database::DatabaseConfig; -use crate::descriptor::Descriptor; -use crate::psbt::PartiallySignedTransaction; -use crate::{ - AddressIndex, AddressInfo, Balance, BdkError, LocalUtxo, OutPoint, Progress, ProgressHolder, - RbfValue, Script, ScriptAmount, TransactionDetails, TxBuilderResult, -}; - #[derive(Debug)] pub(crate) struct Wallet { pub(crate) wallet_mutex: Mutex>, @@ -156,6 +155,22 @@ impl Wallet { let unspents: Vec = self.get_wallet().list_unspent()?; Ok(unspents.into_iter().map(LocalUtxo::from).collect()) } + + /// Get the corresponding PSBT Input for a LocalUtxo. + pub(crate) fn get_psbt_input( + &self, + utxo: LocalUtxo, + sighash_type: Option>, + only_witness_utxo: bool, + ) -> Result, BdkError> { + self.get_wallet() + .get_psbt_input( + utxo.into(), + sighash_type.map(|s| s.deref().into()), + only_witness_utxo, + ) + .map(|i| Arc::new(i.into())) + } } /// Options for a software signer @@ -249,6 +264,7 @@ pub(crate) struct TxBuilder { pub(crate) drain_to: Option, pub(crate) rbf: Option, pub(crate) data: Vec, + pub(crate) foreign_utxos: Vec<(OutPoint, Arc, u64)>, } impl TxBuilder { @@ -265,6 +281,7 @@ impl TxBuilder { drain_to: None, rbf: None, data: Vec::new(), + foreign_utxos: Vec::new(), } } @@ -318,6 +335,53 @@ impl TxBuilder { }) } + /// Add a foreign UTXO i.e. a UTXO not owned by this wallet. + /// At a minimum to add a foreign UTXO we need: + /// outpoint: To add it to the raw transaction. + /// psbt_input: To know the value. + /// satisfaction_weight: To know how much weight/vbytes the input will add to the transaction for fee calculation. + /// + /// There are several security concerns about adding foreign UTXOs that application developers should consider. + /// First, how do you know the value of the input is correct? If a non_witness_utxo is provided in the + /// psbt_input then this method implicitly verifies the value by checking it against the transaction. + /// If only a witness_utxo is provided then this method does not verify the value but just takes it as a + /// given – it is up to you to check that whoever sent you the input_psbt was not lying! + /// + /// Secondly, you must somehow provide satisfaction_weight of the input. Depending on your application + /// it may be important that this be known precisely. If not, a malicious counterparty may fool you into putting in + /// a value that is too low, giving the transaction a lower than expected feerate. They could also fool you + /// into putting a value that is too high causing you to pay a fee that is too high. The party who is broadcasting + /// the transaction can of course check the real input weight matches the expected weight prior to broadcasting. + /// + /// To guarantee the satisfaction_weight is correct, you can require the party providing the psbt_input provide + /// a miniscript descriptor for the input so you can check it against the script_pubkey and then ask it for the + /// max_satisfaction_weight. + /// + /// Errors + /// This method returns errors in the following circumstances: + /// The psbt_input does not contain a witness_utxo or non_witness_utxo. + /// The data in non_witness_utxo does not match what is in outpoint. + /// + /// Note unless you set only_witness_utxo any non-taproot psbt_input you pass to this method must + /// have non_witness_utxo set otherwise you will get an error when finish is called. + pub(crate) fn add_foreign_utxo( + &self, + outpoint: OutPoint, + psbt_input: Arc, + satisfaction_weight: u64, + ) -> Arc { + // TODO: Why doesn't the OutPoint parameter here need an Arc? + + let mut current_foreign_utxos: Vec<(OutPoint, Arc, u64)> = + self.foreign_utxos.clone(); + let new_foreign_utxo = (outpoint, psbt_input, satisfaction_weight); + current_foreign_utxos.push(new_foreign_utxo); + Arc::new(TxBuilder { + foreign_utxos: current_foreign_utxos, + ..self.clone() + }) + } + /// Do not spend change outputs. This effectively adds all the change outputs to the "unspendable" list. See TxBuilder.unspendable. pub(crate) fn do_not_spend_change(&self) -> Arc { Arc::new(TxBuilder { @@ -430,6 +494,26 @@ impl TxBuilder { let utxos: &[BdkOutPoint] = &bdk_utxos; tx_builder.add_utxos(utxos)?; } + if !self.foreign_utxos.is_empty() { + // TODO: Not sure why the double dereference ** is needed here... it just works? + // I really just need to grab the Input inside the Arc but not sure how else to do it. + for (outpoint, input, value) in self.foreign_utxos.iter() { + let input_new: Input = (**input).clone(); + tx_builder.add_foreign_utxo(outpoint.into(), input_new.into(), *value as usize)?; + } + + // let bdk_foreign_utxos: Vec<(OutPoint, Arc, u64)> = self + // .foreign_utxos.iter().map(|(outpoint, input, value)| { + // (outpoint, input.clone(), *value) + // } + // ).collect(); + // let foreign_utxos: Vec<(OutPoint, Arc, u64)> = bdk_foreign_utxos; + // for (outpoint, input, value) in foreign_utxos.iter() { + + // foreign_utxos.forEach(|(outpoint, input, value)| { + // tx_builder.add_foreign_utxo(outpoint, input, value)?; + // }); + } if !self.unspendable.is_empty() { let bdk_unspendable: Vec = self.unspendable.iter().map(BdkOutPoint::from).collect();