From 43bf58c0c99d78789b5a11714ebc686b4268fa06 Mon Sep 17 00:00:00 2001 From: Fabio Lama <42901763+lamafab@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:03:43 +0100 Subject: [PATCH] [PoC/Bitcoin/Utxo] Correctly Consider Fees for UTXO Selection (#3667) * change utxo selection logic for tw_utxo::Compiler * small cleanup * update selected utxos, init input_selection test module * expand tests for change output generation * handle change output correctly in fee estimation, expand tests * calculate the effective fee rate, not the theoretical * add extra sanity checks * add input_selection_select_ascending test * test for empty inputs and outputs, extend error variant * additional comments and sanity checks * simplify transaction builder for BRC20, update tests * update reveal transaction signing in BRC20 builder * remove weight and fee_estimate checks in tw_utxo, plan for later * extra check for error * add input_selection_insufficient_inputs test * scrap the BitcoinPlanBuilder * remove utils module * cargo fmt * calculate the effective fee after setting the change output, if enabled * add UseAll selection tests, check for 'effective' fee * remove fee_estimate.rs, covered in input_selection.rs * use consts for alice and bob info * do extra checks in compile_impl * update change amount correctly in the native Transaction type before calculating sighash * build index of private keys AFTER selecting UTXOs * cargo fmt * [BRC20]: Take Inscription amount as a string * [BRC20]: Take Inscription amount as a string in C++ * [BRC20]: Add a test for BitcoinV2 bridge through the legacy `SigningInput` --------- Co-authored-by: Satoshi Otomakan --- rust/tw_bitcoin/src/entry.rs | 60 +- rust/tw_bitcoin/src/lib.rs | 1 - rust/tw_bitcoin/src/modules/mod.rs | 2 - rust/tw_bitcoin/src/modules/plan_builder.rs | 230 ------ rust/tw_bitcoin/src/modules/signer.rs | 96 ++- .../src/modules/transactions/brc20.rs | 6 +- .../src/modules/transactions/input_builder.rs | 9 +- .../transactions/input_claim_builder.rs | 10 +- .../modules/transactions/output_builder.rs | 9 +- rust/tw_bitcoin/src/modules/utils.rs | 188 ----- rust/tw_bitcoin/tests/brc20.rs | 4 +- rust/tw_bitcoin/tests/free_estimate.rs | 246 ------ rust/tw_bitcoin/tests/input_selection.rs | 747 ++++++++++++++++++ rust/tw_bitcoin/tests/legacy_build_sign.rs | 3 +- rust/tw_bitcoin/tests/legacy_scripts.rs | 7 +- rust/tw_bitcoin/tests/p2pkh.rs | 15 - rust/tw_bitcoin/tests/p2tr_script_path.rs | 3 +- rust/tw_bitcoin/tests/plan_builder.rs | 192 ----- rust/tw_utxo/src/compiler.rs | 281 ++++--- rust/tw_utxo/tests/input_selection.rs | 10 +- rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs | 10 +- src/Bitcoin/Script.cpp | 4 +- src/Bitcoin/Script.h | 2 +- src/interface/TWBitcoinScript.cpp | 2 +- src/proto/BitcoinV2.proto | 5 +- src/proto/Utxo.proto | 22 +- .../chains/Bitcoin/TWBitcoinSigningTests.cpp | 184 ++--- 27 files changed, 1159 insertions(+), 1189 deletions(-) delete mode 100644 rust/tw_bitcoin/src/modules/plan_builder.rs delete mode 100644 rust/tw_bitcoin/src/modules/utils.rs delete mode 100644 rust/tw_bitcoin/tests/free_estimate.rs create mode 100644 rust/tw_bitcoin/tests/input_selection.rs delete mode 100644 rust/tw_bitcoin/tests/plan_builder.rs diff --git a/rust/tw_bitcoin/src/entry.rs b/rust/tw_bitcoin/src/entry.rs index 0ba49752e50..18bab2ce741 100644 --- a/rust/tw_bitcoin/src/entry.rs +++ b/rust/tw_bitcoin/src/entry.rs @@ -1,4 +1,3 @@ -use crate::modules::plan_builder::BitcoinPlanBuilder; use crate::modules::signer::Signer; use crate::{Error, Result}; use bitcoin::address::NetworkChecked; @@ -11,6 +10,7 @@ use tw_coin_entry::derivation::Derivation; use tw_coin_entry::error::{AddressError, AddressResult}; use tw_coin_entry::modules::json_signer::NoJsonSigner; use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::plan_builder::NoPlanBuilder; use tw_coin_entry::modules::wallet_connector::NoWalletConnector; use tw_coin_entry::prefix::NoPrefix; use tw_coin_entry::signing_output_error; @@ -44,7 +44,7 @@ impl CoinEntry for BitcoinEntry { // Optional modules: type JsonSigner = NoJsonSigner; - type PlanBuilder = BitcoinPlanBuilder; + type PlanBuilder = NoPlanBuilder; type MessageSigner = NoMessageSigner; type WalletConnector = NoWalletConnector; @@ -128,7 +128,7 @@ impl CoinEntry for BitcoinEntry { #[inline] fn plan_builder(&self) -> Option { - Some(BitcoinPlanBuilder) + None } } @@ -147,14 +147,15 @@ impl BitcoinEntry { .map(crate::modules::transactions::InputBuilder::utxo_from_proto) .collect::>>()?; - // Convert output builders into Utxo outputs. + // Convert output builders into Utxo outputs (does not contain the change output). let mut utxo_outputs = proto .outputs .iter() .map(crate::modules::transactions::OutputBuilder::utxo_from_proto) .collect::>>()?; - // If automatic change output is enabled, a change script must be provided. + // If automatic change output creation is enabled (by default), a change + // script must be provided. let change_script_pubkey = if proto.disable_change_output { Cow::default() } else { @@ -186,17 +187,23 @@ impl BitcoinEntry { disable_change_output: proto.disable_change_output, }; - // Generate the sighashes to be signed. + // Generate the sighashes to be signed. This also selects the inputs + // according to the input selecter and appends a change output, if + // enabled. let utxo_presigning = tw_utxo::compiler::Compiler::preimage_hashes(utxo_signing); handle_utxo_error(&utxo_presigning.error)?; - // If a change output was created by the Utxo compiler, we return it here too. - if utxo_presigning.outputs.len() == utxo_outputs.len() + 1 { + // Check whether the change output is present. + if !proto.disable_change_output { + // Change output has been added. + debug_assert_eq!(utxo_presigning.outputs.len(), utxo_outputs.len() + 1); + let change_output = utxo_presigning .outputs .last() .expect("expected change output"); + // Push it to the list of outputs. utxo_outputs.push(Proto::mod_PreSigningOutput::TxOut { value: change_output.value, script_pubkey: change_output.script_pubkey.to_vec().into(), @@ -205,6 +212,10 @@ impl BitcoinEntry { }) } + // Sanity check. + debug_assert!(utxo_presigning.inputs.len() <= proto.inputs.len()); + debug_assert_eq!(utxo_presigning.outputs.len(), utxo_outputs.len()); + Ok(Proto::PreSigningOutput { error: Proto::Error::OK, error_message: Default::default(), @@ -241,13 +252,14 @@ impl BitcoinEntry { crate::modules::transactions::InputClaimBuilder::utxo_claim_from_proto( input, signature, )?; + utxo_input_claims.push(utxo_claim); } - // Process all the outputs. + // Prepare all the outputs. let mut utxo_outputs = vec![]; - for output in proto.outputs { - let utxo = crate::modules::transactions::OutputBuilder::utxo_from_proto(&output)?; + for output in &proto.outputs { + let utxo = crate::modules::transactions::OutputBuilder::utxo_from_proto(output)?; utxo_outputs.push(utxo); } @@ -272,9 +284,13 @@ impl BitcoinEntry { let utxo_serialized = tw_utxo::compiler::Compiler::compile(utxo_preserializtion); handle_utxo_error(&utxo_serialized.error)?; - // Prepare `Proto::TransactionInput` protobufs for signing output. + let mut total_input_amount = 0; + + // Prepare `Proto::TransactionInput` for end result. let mut proto_inputs = vec![]; for input in utxo_input_claims { + total_input_amount += input.value; + proto_inputs.push(Proto::TransactionInput { txid: Cow::Owned(input.txid.to_vec()), vout: input.vout, @@ -288,7 +304,7 @@ impl BitcoinEntry { }); } - // Prepare `Proto::TransactionOutput` protobufs for output. + // Prepare `Proto::TransactionOutput` for end result. let mut proto_outputs = vec![]; for output in utxo_outputs { proto_outputs.push(Proto::TransactionOutput { @@ -299,7 +315,7 @@ impl BitcoinEntry { }); } - // Prepare `Proto::Transaction` protobuf for output. + // Prepare `Proto::Transaction` for end result. let transaction = Proto::Transaction { version: proto.version, lock_time: proto.lock_time, @@ -307,6 +323,21 @@ impl BitcoinEntry { outputs: proto_outputs, }; + let total_output_amount = transaction + .outputs + .iter() + .map(|output| output.value) + .sum::(); + + // Sanity check. + debug_assert_eq!(transaction.inputs.len(), proto.inputs.len()); + debug_assert_eq!(transaction.outputs.len(), proto.outputs.len()); + // Every output is accounted for, including the fee. + debug_assert_eq!( + total_input_amount, + total_output_amount + utxo_serialized.fee + ); + // Return the full protobuf output. Ok(Proto::SigningOutput { error: Proto::Error::OK, @@ -353,6 +384,7 @@ fn handle_utxo_error(utxo_err: &UtxoProto::Error) -> Result<()> { UtxoProto::Error::Error_missing_sighash_method => Proto::Error::Error_utxo_missing_sighash_method, UtxoProto::Error::Error_failed_encoding => Proto::Error::Error_utxo_failed_encoding, UtxoProto::Error::Error_insufficient_inputs => Proto::Error::Error_utxo_insufficient_inputs, + UtxoProto::Error::Error_no_outputs_specified => Proto::Error::Error_utxo_no_outputs_specified, UtxoProto::Error::Error_missing_change_script_pubkey => Proto::Error::Error_utxo_missing_change_script_pubkey, }; diff --git a/rust/tw_bitcoin/src/lib.rs b/rust/tw_bitcoin/src/lib.rs index a3bb3d0ac69..be3e835b6a6 100644 --- a/rust/tw_bitcoin/src/lib.rs +++ b/rust/tw_bitcoin/src/lib.rs @@ -16,7 +16,6 @@ pub type Result = std::result::Result; #[derive(Debug)] pub struct Error(Proto::Error); -// TODO: We can improve this. impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/rust/tw_bitcoin/src/modules/mod.rs b/rust/tw_bitcoin/src/modules/mod.rs index ff6024a4895..e6842d7bfc7 100644 --- a/rust/tw_bitcoin/src/modules/mod.rs +++ b/rust/tw_bitcoin/src/modules/mod.rs @@ -1,5 +1,3 @@ pub mod legacy; -pub mod plan_builder; pub mod signer; pub mod transactions; -mod utils; diff --git a/rust/tw_bitcoin/src/modules/plan_builder.rs b/rust/tw_bitcoin/src/modules/plan_builder.rs deleted file mode 100644 index fd8eb880cad..00000000000 --- a/rust/tw_bitcoin/src/modules/plan_builder.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::modules::utils::hard_clone_proto_output; -use crate::{aliases::*, pre_processor, BitcoinEntry}; -use crate::{Error, Result}; -use tw_coin_entry::coin_entry::CoinEntry; -use tw_coin_entry::modules::plan_builder::PlanBuilder; -use tw_coin_entry::signing_output_error; -use tw_proto::BitcoinV2::Proto; -use tw_proto::BitcoinV2::Proto::mod_Input::InputBrc20Inscription; -use tw_proto::Utxo::Proto as UtxoProto; - -pub struct BitcoinPlanBuilder; - -impl PlanBuilder for BitcoinPlanBuilder { - type SigningInput<'a> = Proto::ComposePlan<'a>; - type Plan = Proto::TransactionPlan<'static>; - - #[inline] - fn plan( - &self, - _coin: &dyn tw_coin_entry::coin_context::CoinContext, - proto: Self::SigningInput<'_>, - ) -> Self::Plan { - self.plan_impl(_coin, proto) - .unwrap_or_else(|err| signing_output_error!(Proto::TransactionPlan, err)) - } -} - -impl BitcoinPlanBuilder { - fn plan_impl( - &self, - _coin: &dyn tw_coin_entry::coin_context::CoinContext, - proto: Proto::ComposePlan<'_>, - ) -> Result> { - let plan = match proto.compose { - Proto::mod_ComposePlan::OneOfcompose::brc20(plan) => { - let built_plan = self.plan_brc20(_coin, plan)?; - - Proto::TransactionPlan { - error: Proto::Error::OK, - error_message: Default::default(), - plan: Proto::mod_TransactionPlan::OneOfplan::brc20(built_plan), - } - }, - _ => panic!(), - }; - - Ok(plan) - } - fn plan_brc20( - &self, - _coin: &dyn tw_coin_entry::coin_context::CoinContext, - proto: Proto::mod_ComposePlan::ComposeBrc20Plan<'_>, - ) -> Result> { - // Hard-clones - let inscription = proto - .inscription - .ok_or_else(|| Error::from(Proto::Error::Error_missing_inscription))?; - - let brc20_info = InputBrc20Inscription { - one_prevout: inscription.one_prevout, - inscribe_to: inscription.inscribe_to.to_vec().into(), - ticker: inscription.ticker.to_string().into(), - transfer_amount: inscription.transfer_amount, - }; - - let tagged_output = super::utils::hard_clone_proto_output( - proto - .tagged_output - .ok_or_else(|| Error::from(Proto::Error::Error_missing_tagged_output))?, - )?; - - // First, we create the reveal transaction in order to calculate its input requirement (fee + dust limit). - - // We can use a zeroed Txid here. - let txid = vec![0; 32]; - let dummy_brc20_input = Proto::Input { - txid: txid.into(), - // The value is not relevant here, but we raise it above the output - // or we get an error. - value: u64::MAX, - sighash_type: UtxoProto::SighashType::UseDefault, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::brc20_inscribe(brc20_info.clone()), - }), - ..Default::default() - }; - - let dummy_reveal = Proto::SigningInput { - inputs: vec![dummy_brc20_input], - outputs: vec![tagged_output.clone()], - input_selector: UtxoProto::InputSelector::UseAll, - // Disable change output creation. - fee_per_vb: proto.fee_per_vb, - disable_change_output: true, - ..Default::default() - }; - - // We can now determine the fee of the reveal transaction. - let dummy_presigned = BitcoinEntry.preimage_hashes(_coin, dummy_reveal); - if dummy_presigned.error != Proto::Error::OK { - return Err(Error::from(dummy_presigned.error)); - } - - assert_eq!(dummy_presigned.error, Proto::Error::OK); - let reveal_fee_estimate = dummy_presigned.fee_estimate; - - // Create the BRC20 output for the COMMIT transaction; we set the - // amount to the estimated fee (REVEAL) plus the dust limit (`tagged_output.value`). - let brc20_output = Proto::Output { - value: reveal_fee_estimate + tagged_output.value, - to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { - variant: ProtoOutputBuilder::brc20_inscribe( - Proto::mod_Output::OutputBrc20Inscription { - inscribe_to: brc20_info.inscribe_to.to_vec().into(), - ticker: brc20_info.ticker.to_string().into(), - transfer_amount: brc20_info.transfer_amount, - }, - ), - }), - }; - - let brc20_output_value = brc20_output.value; - - // Clone the change output, if provided. - let change_output = if let Some(change) = proto.change_output { - Some(super::utils::hard_clone_proto_output(change)?) - } else { - None - }; - - // Create the full COMMIT transaction with the appropriately selected inputs. - let commit_signing = Proto::SigningInput { - private_key: proto.private_key.to_vec().into(), - inputs: proto - .inputs - .iter() - .cloned() - .map(super::utils::hard_clone_proto_input) - .collect::>()?, - outputs: vec![brc20_output], - input_selector: proto.input_selector, - change_output: change_output.clone(), - fee_per_vb: proto.fee_per_vb, - disable_change_output: proto.disable_change_output, - ..Default::default() - }; - - let mut commit_signing = pre_processor(commit_signing); - - // We now determine the Txid of the COMMIT transaction, which we will have - // to use in the REVEAL transaction. - let presigned = BitcoinEntry.preimage_hashes(_coin, commit_signing.clone()); - if presigned.error != Proto::Error::OK { - return Err(Error::from(presigned.error)); - } - - assert_eq!(presigned.error, Proto::Error::OK); - let commit_txid: Vec = presigned.txid.to_vec().iter().copied().rev().collect(); - - // Create a list of the selected input Txids, as indicated by the - // `InputSelector`. - let selected_txids: Vec<_> = presigned - .utxo_inputs - .iter() - .map(|utxo| utxo.txid.clone()) - .collect(); - - // Create the list of selected inputs and update the COMMIT transaction. - let selected_inputs: Vec<_> = proto - .inputs - .into_iter() - .filter(|input| selected_txids.contains(&input.txid)) - .map(super::utils::hard_clone_proto_input) - .collect::>()?; - - commit_signing.inputs = selected_inputs; - - // Update the change amount to calculated amount. - if !proto.disable_change_output && presigned.utxo_outputs.len() == 2 { - let change_amount = presigned - .utxo_outputs - .last() - .expect("No Utxo outputs generated") - .value; - - let mut change_output = change_output.expect("change output expected"); - change_output.value = change_amount; - - commit_signing - .outputs - .push(hard_clone_proto_output(change_output)?); - } - - commit_signing.input_selector = UtxoProto::InputSelector::UseAll; - commit_signing.disable_change_output = true; - commit_signing.fee_per_vb = 0; - commit_signing.change_output = Default::default(); - - // Now we construct the *actual* REVEAL transaction. - - let brc20_input = Proto::Input { - value: brc20_output_value, - txid: commit_txid.into(), // Reference COMMIT transaction. - sighash_type: UtxoProto::SighashType::UseDefault, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::brc20_inscribe(brc20_info.clone()), - }), - ..Default::default() - }; - - // Build the REVEAL transaction. - let reveal_signing = Proto::SigningInput { - private_key: proto.private_key.to_vec().into(), - inputs: vec![brc20_input], - outputs: vec![tagged_output], - input_selector: UtxoProto::InputSelector::UseAll, - change_output: Default::default(), - fee_per_vb: 0, - disable_change_output: true, - ..Default::default() - }; - - let reveal_signing = pre_processor(reveal_signing); - - Ok(Proto::mod_TransactionPlan::Brc20Plan { - commit: Some(commit_signing), - reveal: Some(reveal_signing), - }) - } -} diff --git a/rust/tw_bitcoin/src/modules/signer.rs b/rust/tw_bitcoin/src/modules/signer.rs index cdf17b68eef..76089cad6f7 100644 --- a/rust/tw_bitcoin/src/modules/signer.rs +++ b/rust/tw_bitcoin/src/modules/signer.rs @@ -20,7 +20,58 @@ impl Signer { // `preimage_hashes_impl` and `compile_impl`. But we're leaving this // here in case this methods gets extended and the pre-processing does // not get accidentally forgotten. - let proto = crate::entry::pre_processor(proto); + let mut proto = crate::entry::pre_processor(proto); + + // Generate the sighashes. + let pre_signed = BitcoinEntry.preimage_hashes_impl(_coin, proto.clone())?; + if pre_signed.error != Proto::Error::OK { + return Err(Error::from(pre_signed.error)); + } + + // Sanity check. + debug_assert!(proto.inputs.len() >= pre_signed.utxo_inputs.len()); + debug_assert_eq!(pre_signed.utxo_inputs.len(), pre_signed.sighashes.len()); + + if proto.disable_change_output { + debug_assert_eq!(proto.outputs.len(), pre_signed.utxo_outputs.len()); + } else { + // If a change output was generated... + debug_assert_eq!(proto.outputs.len() + 1, pre_signed.utxo_outputs.len()); // plus change output. + + // Update the given change output with the specified amount and push + // it to the proto structure. + let change_output_amount = pre_signed + .utxo_outputs + .last() + .expect("No change output provided") + .value; + + let mut change_output = proto.change_output.clone().expect("change output expected"); + change_output.value = change_output_amount; + proto.outputs.push(change_output); + } + + // The `pre_signed` result contains a list of selected inputs in order + // to cover the output amount and fees, assuming the input selector was + // used. We therefore need to update the proto structure. + + // Clear the inputs. + let available = std::mem::take(&mut proto.inputs); + + for selected in &pre_signed.utxo_inputs { + // Find the input in the passed on UTXO list. + let index = available + .iter() + .position(|input| input.txid == selected.txid && input.vout == selected.vout) + .expect("Selected input not found in proto structure"); + + // Update the input with the selected input. + proto.inputs.push(available[index].clone()); + } + + // Sanity check. + debug_assert_eq!(proto.outputs.len(), pre_signed.utxo_outputs.len()); + debug_assert_eq!(proto.inputs.len(), pre_signed.utxo_inputs.len()); // Collect individual private keys per input, if there are any. let mut individual_keys = HashMap::new(); @@ -30,14 +81,6 @@ impl Signer { } } - // Generate the sighashes. - let pre_signed = BitcoinEntry.preimage_hashes_impl(_coin, proto.clone())?; - - // Check for error. - if pre_signed.error != Proto::Error::OK { - return Err(Error::from(pre_signed.error)); - } - // Sign the sighashes. let signatures = crate::modules::signer::Signer::signatures_from_proto( &pre_signed, @@ -46,8 +89,41 @@ impl Signer { proto.dangerous_use_fixed_schnorr_rng, )?; + // Sanity check. + debug_assert_eq!(signatures.len(), proto.inputs.len()); + debug_assert_eq!(signatures.len(), pre_signed.sighashes.len()); + + // Prepare values for sanity check. + let total_input_amount = proto.inputs.iter().map(|input| input.value).sum::(); + let total_output_amount = proto.outputs.iter().map(|output| output.value).sum::(); + // Construct the final transaction. - BitcoinEntry.compile_impl(_coin, proto, signatures, vec![]) + let mut compiled = BitcoinEntry.compile_impl(_coin, proto, signatures, vec![])?; + + // Note: the fee that we used for estimation might be SLIGHLY off + // from the final fee. This is due to the fact that we must set a + // change output (which must consider the fee) before we can calculate + // the final fee. This leads to a chicken-and-egg problem. However, + // the fee difference, should there be one, is generally as small as + // one weight unit. Hence, we overwrite the final fee with the + // estimated fee. + compiled.weight = pre_signed.weight_estimate; + + // Sanity check. + let compiled_total_output_amount = compiled + .transaction + .as_ref() + .expect("No transaction was constructed") + .outputs + .iter() + .map(|output| output.value) + .sum::(); + + // Every output is accounted for, including the fee. + debug_assert_eq!(total_output_amount, compiled_total_output_amount); + debug_assert_eq!(total_input_amount, total_output_amount + compiled.fee); + + Ok(compiled) } pub fn signatures_from_proto( input: &Proto::PreSigningOutput<'_>, diff --git a/rust/tw_bitcoin/src/modules/transactions/brc20.rs b/rust/tw_bitcoin/src/modules/transactions/brc20.rs index 51afda68777..e67b81f24bb 100644 --- a/rust/tw_bitcoin/src/modules/transactions/brc20.rs +++ b/rust/tw_bitcoin/src/modules/transactions/brc20.rs @@ -38,12 +38,12 @@ impl BRC20TransferPayload { impl BRC20TransferPayload { const OPERATION: &str = "transfer"; - fn new(ticker: Brc20Ticker, value: u64) -> Self { + fn new(ticker: Brc20Ticker, amount: String) -> Self { BRC20TransferPayload { protocol: Self::PROTOCOL_ID.to_string(), operation: Self::OPERATION.to_string(), ticker, - amount: value.to_string(), + amount, } } } @@ -54,7 +54,7 @@ impl BRC20TransferInscription { pub fn new( recipient: PublicKey, ticker: Brc20Ticker, - value: u64, + value: String, ) -> Result { let data: BRC20TransferPayload = BRC20TransferPayload::new(ticker, value); diff --git a/rust/tw_bitcoin/src/modules/transactions/input_builder.rs b/rust/tw_bitcoin/src/modules/transactions/input_builder.rs index 4d1660cb61f..b9402b11680 100644 --- a/rust/tw_bitcoin/src/modules/transactions/input_builder.rs +++ b/rust/tw_bitcoin/src/modules/transactions/input_builder.rs @@ -213,9 +213,12 @@ impl InputBuilder { let pubkey = bitcoin::PublicKey::from_slice(brc20.inscribe_to.as_ref())?; let ticker = Brc20Ticker::new(brc20.ticker.to_string())?; - let transfer = - BRC20TransferInscription::new(pubkey, ticker, brc20.transfer_amount) - .expect("invalid BRC20 transfer construction"); + let transfer = BRC20TransferInscription::new( + pubkey, + ticker, + brc20.transfer_amount.to_string(), + ) + .expect("invalid BRC20 transfer construction"); // We construct a control block to estimate the fee, // otherwise we do not need it here. diff --git a/rust/tw_bitcoin/src/modules/transactions/input_claim_builder.rs b/rust/tw_bitcoin/src/modules/transactions/input_claim_builder.rs index b93269031fc..d2b3fc4400f 100644 --- a/rust/tw_bitcoin/src/modules/transactions/input_claim_builder.rs +++ b/rust/tw_bitcoin/src/modules/transactions/input_claim_builder.rs @@ -116,9 +116,12 @@ impl InputClaimBuilder { let ticker = Brc20Ticker::new(brc20.ticker.to_string())?; // Construct the BRC20 transfer inscription. - let transfer = - BRC20TransferInscription::new(pubkey, ticker, brc20.transfer_amount) - .expect("invalid BRC20 transfer construction"); + let transfer = BRC20TransferInscription::new( + pubkey, + ticker, + brc20.transfer_amount.to_string(), + ) + .expect("invalid BRC20 transfer construction"); // Create a control block for that inscription. let control_block = transfer @@ -158,6 +161,7 @@ impl InputClaimBuilder { let claim = UtxoProto::TxInClaim { txid: input.txid.to_vec().into(), vout: input.vout, + value: input.value, sequence: input.sequence, script_sig: script_sig.to_vec().into(), witness_items: witness diff --git a/rust/tw_bitcoin/src/modules/transactions/output_builder.rs b/rust/tw_bitcoin/src/modules/transactions/output_builder.rs index 72fef4c0c39..83e3ea13bef 100644 --- a/rust/tw_bitcoin/src/modules/transactions/output_builder.rs +++ b/rust/tw_bitcoin/src/modules/transactions/output_builder.rs @@ -140,9 +140,12 @@ impl OutputBuilder { let xonly = XOnlyPublicKey::from(pubkey.inner); let ticker = Brc20Ticker::new(brc20.ticker.to_string())?; - let transfer = - BRC20TransferInscription::new(pubkey, ticker, brc20.transfer_amount) - .expect("invalid BRC20 transfer construction"); + let transfer = BRC20TransferInscription::new( + pubkey, + ticker, + brc20.transfer_amount.to_string(), + ) + .expect("invalid BRC20 transfer construction"); // Construct the control block. let control_block = transfer diff --git a/rust/tw_bitcoin/src/modules/utils.rs b/rust/tw_bitcoin/src/modules/utils.rs deleted file mode 100644 index f64f9bfb29e..00000000000 --- a/rust/tw_bitcoin/src/modules/utils.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::aliases::*; -use crate::{Error, Result}; -use tw_proto::BitcoinV2::Proto; - -// Convenience function: our protobuf library wraps certain types (such as -// `bytes`) in `Cow`, but given that calling `clone()` on a `Cow::Borrowed(T)` -// does not actually clone the underlying data (but the smart pointer instead), -// we must hard-clone individual fields manually. This is unfortunately required -// due to how protobuf library works and our use of the 'static constraints. -pub fn hard_clone_proto_input(proto: Proto::Input<'_>) -> Result> { - fn new_builder(variant: ProtoInputBuilder<'static>) -> ProtoInputRecipient<'static> { - ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { variant }) - } - - let to_recipient = match proto.to_recipient { - ProtoInputRecipient::builder(builder) => match builder.variant { - ProtoInputBuilder::p2sh(script) => { - new_builder(ProtoInputBuilder::p2sh(script.to_vec().into())) - }, - ProtoInputBuilder::p2pkh(script) => { - new_builder(ProtoInputBuilder::p2pkh(script.to_vec().into())) - }, - ProtoInputBuilder::p2wsh(script) => { - new_builder(ProtoInputBuilder::p2wsh(script.to_vec().into())) - }, - ProtoInputBuilder::p2wpkh(script) => { - new_builder(ProtoInputBuilder::p2wpkh(script.to_vec().into())) - }, - ProtoInputBuilder::p2tr_key_path(key_path) => new_builder( - ProtoInputBuilder::p2tr_key_path(Proto::mod_Input::InputTaprootKeyPath { - one_prevout: key_path.one_prevout, - public_key: key_path.public_key.to_vec().into(), - }), - ), - ProtoInputBuilder::p2tr_script_path(script) => new_builder( - ProtoInputBuilder::p2tr_script_path(Proto::mod_Input::InputTaprootScriptPath { - one_prevout: script.one_prevout, - payload: script.payload.to_vec().into(), - control_block: script.control_block.to_vec().into(), - }), - ), - ProtoInputBuilder::brc20_inscribe(brc20) => new_builder( - ProtoInputBuilder::brc20_inscribe(Proto::mod_Input::InputBrc20Inscription { - one_prevout: brc20.one_prevout, - inscribe_to: brc20.inscribe_to.to_vec().into(), - ticker: brc20.ticker.to_string().into(), - transfer_amount: brc20.transfer_amount, - }), - ), - ProtoInputBuilder::ordinal_inscribe(ord) => new_builder( - ProtoInputBuilder::ordinal_inscribe(Proto::mod_Input::InputOrdinalInscription { - one_prevout: ord.one_prevout, - inscribe_to: ord.inscribe_to.to_vec().into(), - mime_type: ord.mime_type.to_string().into(), - payload: ord.payload.to_vec().into(), - }), - ), - ProtoInputBuilder::None => { - return Err(Error::from(Proto::Error::Error_missing_input_builder)) - }, - }, - ProtoInputRecipient::custom_script(custom) => { - ProtoInputRecipient::custom_script(Proto::mod_Input::InputScriptWitness { - script_pubkey: custom.script_pubkey.to_vec().into(), - script_sig: custom.script_sig.to_vec().into(), - witness_items: custom - .witness_items - .iter() - .map(|item| item.to_vec().into()) - .collect(), - signing_method: custom.signing_method, - }) - }, - ProtoInputRecipient::None => { - return Err(Error::from(Proto::Error::Error_missing_recipient)) - }, - }; - - Ok(Proto::Input { - private_key: proto.private_key.to_vec().into(), - txid: proto.txid.to_vec().into(), - to_recipient, - ..proto - }) -} - -// Convenience function: our protobuf library wraps certain types (such as -// `bytes`) in `Cow`, but given that calling `clone()` on a `Cow::Borrowed(T)` -// does not actually clone the underlying data (but the smart pointer instead), -// we must hard-clone individual fields manually. This is unfortunately required -// due to how protobuf library works and our use of the 'static constraints. -pub fn hard_clone_proto_output(proto: Proto::Output<'_>) -> Result> { - fn new_builder(variant: ProtoOutputBuilder<'static>) -> ProtoOutputRecipient<'static> { - ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { variant }) - } - - fn new_script_or_hash( - proto: Proto::mod_Output::OutputRedeemScriptOrHash<'_>, - ) -> Result> { - let variant = match proto.variant { - ProtoOutputRedeemScriptOrHashBuilder::redeem_script(script) => { - ProtoOutputRedeemScriptOrHashBuilder::redeem_script(script.to_vec().into()) - }, - ProtoOutputRedeemScriptOrHashBuilder::hash(hash) => { - ProtoOutputRedeemScriptOrHashBuilder::hash(hash.to_vec().into()) - }, - ProtoOutputRedeemScriptOrHashBuilder::None => { - return Err(Error::from(Proto::Error::Error_missing_recipient)) - }, - }; - - Ok(Proto::mod_Output::OutputRedeemScriptOrHash { variant }) - } - - fn new_pubkey_or_hash( - proto: Proto::ToPublicKeyOrHash<'_>, - ) -> Result> { - let to_address = match proto.to_address { - ProtoPubkeyOrHash::pubkey(pubkey) => ProtoPubkeyOrHash::pubkey(pubkey.to_vec().into()), - ProtoPubkeyOrHash::hash(hash) => ProtoPubkeyOrHash::hash(hash.to_vec().into()), - ProtoPubkeyOrHash::None => { - return Err(Error::from(Proto::Error::Error_missing_recipient)) - }, - }; - - Ok(Proto::ToPublicKeyOrHash { to_address }) - } - - let to_recipient = match proto.to_recipient { - ProtoOutputRecipient::builder(builder) => match builder.variant { - ProtoOutputBuilder::p2sh(script_or_hash) => new_builder(ProtoOutputBuilder::p2sh( - new_script_or_hash(script_or_hash)?, - )), - ProtoOutputBuilder::p2pkh(pubkey_or_hash) => new_builder(ProtoOutputBuilder::p2pkh( - new_pubkey_or_hash(pubkey_or_hash)?, - )), - ProtoOutputBuilder::p2wsh(script_or_hash) => new_builder(ProtoOutputBuilder::p2wsh( - new_script_or_hash(script_or_hash)?, - )), - ProtoOutputBuilder::p2wpkh(pubkey_or_hash) => new_builder(ProtoOutputBuilder::p2wpkh( - new_pubkey_or_hash(pubkey_or_hash)?, - )), - ProtoOutputBuilder::p2tr_key_path(pubkey) => { - new_builder(ProtoOutputBuilder::p2tr_key_path(pubkey.to_vec().into())) - }, - ProtoOutputBuilder::p2tr_script_path(script_path) => new_builder( - ProtoOutputBuilder::p2tr_script_path(Proto::mod_Output::OutputTaprootScriptPath { - internal_key: script_path.internal_key.to_vec().into(), - merkle_root: script_path.merkle_root.to_vec().into(), - }), - ), - ProtoOutputBuilder::p2tr_dangerous_assume_tweaked(tweaked) => new_builder( - ProtoOutputBuilder::p2tr_dangerous_assume_tweaked(tweaked.to_vec().into()), - ), - ProtoOutputBuilder::brc20_inscribe(brc20) => new_builder( - ProtoOutputBuilder::brc20_inscribe(Proto::mod_Output::OutputBrc20Inscription { - inscribe_to: brc20.inscribe_to.to_vec().into(), - ticker: brc20.ticker.to_string().into(), - transfer_amount: brc20.transfer_amount, - }), - ), - ProtoOutputBuilder::ordinal_inscribe(ord) => new_builder( - ProtoOutputBuilder::ordinal_inscribe(Proto::mod_Output::OutputOrdinalInscription { - inscribe_to: ord.inscribe_to.to_vec().into(), - mime_type: ord.mime_type.to_string().into(), - payload: ord.payload.to_vec().into(), - }), - ), - ProtoOutputBuilder::None => { - return Err(Error::from(Proto::Error::Error_missing_output_builder)) - }, - }, - ProtoOutputRecipient::custom_script_pubkey(custom) => { - ProtoOutputRecipient::custom_script_pubkey(custom.to_vec().into()) - }, - ProtoOutputRecipient::from_address(address) => { - ProtoOutputRecipient::from_address(address.to_string().into()) - }, - ProtoOutputRecipient::None => { - return Err(Error::from(Proto::Error::Error_missing_output_builder)) - }, - }; - - Ok(Proto::Output { - value: proto.value, - to_recipient, - }) -} diff --git a/rust/tw_bitcoin/tests/brc20.rs b/rust/tw_bitcoin/tests/brc20.rs index 67c5a2678b8..46458321c7b 100644 --- a/rust/tw_bitcoin/tests/brc20.rs +++ b/rust/tw_bitcoin/tests/brc20.rs @@ -38,7 +38,7 @@ fn coin_entry_sign_brc20_commit_reveal_transfer() { Proto::mod_Output::OutputBrc20Inscription { inscribe_to: alice_pubkey.as_slice().into(), ticker: "oadf".into(), - transfer_amount: 20, + transfer_amount: "20".into(), }, ), }), @@ -93,7 +93,7 @@ fn coin_entry_sign_brc20_commit_reveal_transfer() { one_prevout: false, inscribe_to: alice_pubkey.as_slice().into(), ticker: "oadf".into(), - transfer_amount: 20, + transfer_amount: "20".into(), }), }), ..Default::default() diff --git a/rust/tw_bitcoin/tests/free_estimate.rs b/rust/tw_bitcoin/tests/free_estimate.rs deleted file mode 100644 index bc64fbfa429..00000000000 --- a/rust/tw_bitcoin/tests/free_estimate.rs +++ /dev/null @@ -1,246 +0,0 @@ -mod common; - -use common::{hex, ONE_BTC}; -use tw_bitcoin::aliases::*; -use tw_bitcoin::entry::BitcoinEntry; -use tw_coin_entry::coin_entry::CoinEntry; -use tw_coin_entry::test_utils::test_context::TestCoinContext; -use tw_proto::BitcoinV2::Proto; -use tw_proto::Utxo::Proto as UtxoProto; - -const SAT_VB: u64 = 20; - -#[test] -fn p2pkh_fee_estimate() { - let coin = TestCoinContext::default(); - - let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); - let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); - - let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); - let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") - .into_iter() - .rev() - .collect(); - - let mut signing = Proto::SigningInput { - private_key: alice_private_key.as_slice().into(), - inputs: vec![], - outputs: vec![], - input_selector: UtxoProto::InputSelector::UseAll, - fee_per_vb: SAT_VB, - disable_change_output: true, - ..Default::default() - }; - - signing.inputs.push(Proto::Input { - txid: txid.as_slice().into(), - vout: 0, - value: 2 * ONE_BTC, - sighash_type: UtxoProto::SighashType::All, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::p2pkh(alice_pubkey.as_slice().into()), - }), - ..Default::default() - }); - - signing.outputs.push(Proto::Output { - value: ONE_BTC, - to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { - variant: ProtoOutputBuilder::p2pkh(Proto::ToPublicKeyOrHash { - to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), - }), - }), - }); - - let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); - assert_eq!(prehashes.error, Proto::Error::OK); - assert_eq!(prehashes.weight_estimate, 768); - assert_eq!(prehashes.fee_estimate, (768 + 3) / 4 * SAT_VB); - - let signed = BitcoinEntry.sign(&coin, signing); - assert_eq!(signed.error, Proto::Error::OK); - assert_eq!(signed.weight, 768); - assert_eq!(signed.fee, (768 + 3) / 4 * SAT_VB); -} - -#[test] -fn p2wpkh_fee_estimate() { - let coin = TestCoinContext::default(); - - let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); - let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); - - let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); - let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") - .into_iter() - .rev() - .collect(); - - let mut signing = Proto::SigningInput { - private_key: alice_private_key.as_slice().into(), - inputs: vec![], - outputs: vec![], - input_selector: UtxoProto::InputSelector::UseAll, - fee_per_vb: SAT_VB, - disable_change_output: true, - ..Default::default() - }; - - signing.inputs.push(Proto::Input { - txid: txid.as_slice().into(), - vout: 0, - value: 2 * ONE_BTC, - sequence: u32::MAX, - sighash_type: UtxoProto::SighashType::All, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), - }), - ..Default::default() - }); - - signing.outputs.push(Proto::Output { - value: ONE_BTC, - to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { - variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { - to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), - }), - }), - }); - - let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); - assert_eq!(prehashes.error, Proto::Error::OK); - // TODO: The estimated weight/fee is slightly off from the finalized - // weight/fee. This is probably good enough, but we can probably improve - // this. - assert_eq!(prehashes.weight_estimate, 436); - assert_eq!(prehashes.fee_estimate, (436 + 3) / 4 * SAT_VB); - - let signed = BitcoinEntry.sign(&coin, signing); - assert_eq!(signed.error, Proto::Error::OK); - assert_eq!(signed.weight, 438); - assert_eq!(signed.fee, (438 + 3) / 4 * SAT_VB); -} - -#[test] -fn p2tr_key_path_fee_estimate() { - let coin = TestCoinContext::default(); - - let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); - let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); - - let bob_pubkey = hex("025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"); - let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") - .into_iter() - .rev() - .collect(); - - let tx1 = Proto::Input { - txid: txid.as_slice().into(), - vout: 0, - value: 2 * ONE_BTC, - sighash_type: UtxoProto::SighashType::All, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::p2tr_key_path(Proto::mod_Input::InputTaprootKeyPath { - one_prevout: false, - public_key: alice_pubkey.as_slice().into(), - }), - }), - ..Default::default() - }; - - let out1 = Proto::Output { - value: ONE_BTC, - to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { - variant: ProtoOutputBuilder::p2tr_key_path(bob_pubkey.as_slice().into()), - }), - }; - - let signing = Proto::SigningInput { - private_key: alice_private_key.as_slice().into(), - inputs: vec![tx1], - outputs: vec![out1], - input_selector: UtxoProto::InputSelector::UseAll, - fee_per_vb: SAT_VB, - disable_change_output: true, - ..Default::default() - }; - - let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); - assert_eq!(prehashes.error, Proto::Error::OK); - // TODO: The estimated weight/fee is slightly off from the finalized - // weight/fee. This is probably good enough, but we can probably improve - // this. - assert_eq!(prehashes.weight_estimate, 450); - assert_eq!(prehashes.fee_estimate, (450 + 3) / 4 * SAT_VB); - - let signed = BitcoinEntry.sign(&coin, signing); - assert_eq!(signed.error, Proto::Error::OK); - assert_eq!(signed.weight, 445); - assert_eq!(signed.fee, (445 + 3) / 4 * SAT_VB); -} - -#[test] -fn brc20_inscribe_fee_estimate() { - let coin = TestCoinContext::default(); - - let alice_private_key = hex("57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"); - let alice_pubkey = hex("028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"); - - let txid: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") - .into_iter() - .rev() - .collect(); - - let tx1 = Proto::Input { - txid: txid.as_slice().into(), - vout: 0, - value: 2 * ONE_BTC, - sighash_type: UtxoProto::SighashType::All, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::brc20_inscribe(Proto::mod_Input::InputBrc20Inscription { - one_prevout: false, - inscribe_to: alice_pubkey.as_slice().into(), - ticker: "oadf".into(), - transfer_amount: 20, - }), - }), - ..Default::default() - }; - - let out1 = Proto::Output { - value: ONE_BTC, - to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { - variant: ProtoOutputBuilder::brc20_inscribe( - Proto::mod_Output::OutputBrc20Inscription { - inscribe_to: alice_pubkey.as_slice().into(), - ticker: "oadf".into(), - transfer_amount: 20, - }, - ), - }), - }; - - let signing = Proto::SigningInput { - private_key: alice_private_key.as_slice().into(), - inputs: vec![tx1], - outputs: vec![out1], - input_selector: UtxoProto::InputSelector::UseAll, - fee_per_vb: SAT_VB, - disable_change_output: true, - ..Default::default() - }; - - let prehashes = BitcoinEntry.preimage_hashes(&coin, signing.clone()); - assert_eq!(prehashes.error, Proto::Error::OK); - // TODO: The estimated weight/fee is slightly off from the finalized - // weight/fee. This is probably good enough, but we can probably improve - // this. - assert_eq!(prehashes.weight_estimate, 575); - assert_eq!(prehashes.fee_estimate, (575 + 3) / 4 * SAT_VB); - - let signed = BitcoinEntry.sign(&coin, signing); - assert_eq!(signed.error, Proto::Error::OK); - assert_eq!(signed.weight, 571); - assert_eq!(signed.fee, (571 + 3) / 4 * SAT_VB); -} diff --git a/rust/tw_bitcoin/tests/input_selection.rs b/rust/tw_bitcoin/tests/input_selection.rs new file mode 100644 index 00000000000..38afea369e5 --- /dev/null +++ b/rust/tw_bitcoin/tests/input_selection.rs @@ -0,0 +1,747 @@ +mod common; +use common::{hex, ONE_BTC}; + +use tw_bitcoin::aliases::*; +use tw_bitcoin::entry::BitcoinEntry; +use tw_coin_entry::coin_entry::CoinEntry; +use tw_coin_entry::test_utils::test_context::TestCoinContext; +use tw_proto::BitcoinV2::Proto; +use tw_proto::Utxo::Proto as UtxoProto; + +const SAT_VBYTE: u64 = 50; + +const ALICE_PRIVATE_KEY: &str = "57a64865bce5d4855e99b1cce13327c46171434f2d72eeaf9da53ee075e7f90a"; +const ALICE_PUBKEY: &str = "028d7dce6d72fb8f7af9566616c6436349c67ad379f2404dd66fe7085fe0fba28f"; +const BOB_PUBKEY: &str = "025a0af1510f0f24d40dd00d7c0e51605ca504bbc177c3e19b065f373a1efdd22f"; + +#[test] +fn input_selection_no_change_output() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + let bob_pubkey = hex(BOB_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: 50_000_000, // 0.5 BTC + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + // TODO: Mandate that fee_per_byte is non-zero? + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + inputs: vec![tx1], + outputs: vec![out1], + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + // By default, we mandate that a change output is set. + assert_eq!(signed.error, Proto::Error::Error_invalid_change_output); + assert_eq!(signed.error_message, "Error_invalid_change_output"); + assert_eq!(signed.transaction, None); + assert!(signed.encoded.is_empty()); + assert!(signed.txid.is_empty()); + assert_eq!(signed.weight, 0); + assert_eq!(signed.fee, 0); +} + +#[test] +fn input_selection_no_utxo_inputs() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + let bob_pubkey = hex(BOB_PUBKEY); + + let out1 = Proto::Output { + value: 50_000_000, // 0.5 BTC + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let change_output = Proto::Output { + // Will be set for us. + value: 0, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + // No inputs. + inputs: vec![], + outputs: vec![out1], + // We set the change output accordingly. + change_output: Some(change_output), + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + // Error + assert_eq!(signed.error, Proto::Error::Error_utxo_insufficient_inputs); + assert_eq!(signed.error_message, "Error_utxo_insufficient_inputs"); + assert_eq!(signed.transaction, None); + assert!(signed.encoded.is_empty()); + assert!(signed.txid.is_empty()); + assert_eq!(signed.weight, 0); + assert_eq!(signed.fee, 0); +} + +#[test] +fn input_selection_no_utxo_outputs() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + // No inputs. + inputs: vec![tx1], + outputs: vec![], + disable_change_output: true, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + // Error + assert_eq!(signed.error, Proto::Error::Error_utxo_no_outputs_specified); + assert_eq!(signed.error_message, "Error_utxo_no_outputs_specified"); + assert_eq!(signed.transaction, None); + assert!(signed.encoded.is_empty()); + assert!(signed.txid.is_empty()); + assert_eq!(signed.weight, 0); + assert_eq!(signed.fee, 0); +} + +#[test] +fn input_selection_insufficient_inputs() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + let bob_pubkey = hex(BOB_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC * 2, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let change_output = Proto::Output { + // Will be set for us. + value: 0, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + // No inputs. + inputs: vec![tx1], + outputs: vec![out1], + // We set the change output accordingly. + change_output: Some(change_output), + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + // Input does not cover output. + assert_eq!(signed.error, Proto::Error::Error_utxo_insufficient_inputs); + assert_eq!(signed.error_message, "Error_utxo_insufficient_inputs"); + assert_eq!(signed.transaction, None); + assert!(signed.encoded.is_empty()); + assert!(signed.txid.is_empty()); + assert_eq!(signed.weight, 0); + assert_eq!(signed.fee, 0); +} + +#[test] +fn input_selection_no_utxo_outputs_with_change_output() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let change_output = Proto::Output { + // Will be set for us. + value: 0, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + // No inputs. + inputs: vec![tx1], + outputs: vec![], + // We set the change output accordingly. + change_output: Some(change_output), + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + // Even though there is a change output, we mandate "normal" outputs. + assert_eq!(signed.error, Proto::Error::Error_utxo_no_outputs_specified); + assert_eq!(signed.error_message, "Error_utxo_no_outputs_specified"); + assert_eq!(signed.transaction, None); + assert!(signed.encoded.is_empty()); + assert!(signed.txid.is_empty()); + assert_eq!(signed.weight, 0); + assert_eq!(signed.fee, 0); +} + +#[test] +fn input_selection_select_in_order() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + let bob_pubkey = hex(BOB_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 3, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![2; 32]; + let tx2 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 2, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![3; 32]; + let tx3 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC / 2, // 0.5 BTC + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let change_output = Proto::Output { + // Will be set for us. + value: 0, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + input_selector: UtxoProto::InputSelector::SelectInOrder, + inputs: vec![tx1.clone(), tx2, tx3], + outputs: vec![out1.clone()], + // We set the change output accordingly. + change_output: Some(change_output), + fee_per_vb: SAT_VBYTE, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + assert_eq!(signed.error, Proto::Error::OK); + assert!(signed.error_message.is_empty()); + assert_eq!(signed.weight, 560); + assert_eq!(signed.fee, (signed.weight + 3) / 4 * SAT_VBYTE); + assert_eq!(signed.fee, 7_000); + + let tx = signed.transaction.unwrap(); + assert_eq!(tx.version, 2); + + // Inputs, only one input was selected (ONE_BTC * 3). + assert_eq!(tx.inputs.len(), 1); + assert_eq!(tx.inputs[0].txid, tx1.txid); + assert_eq!(tx.inputs[0].txid, vec![1; 32]); + assert_eq!(tx.inputs[0].vout, 0); + assert_eq!(tx.inputs[0].sequence, u32::MAX); + assert!(tx.inputs[0].script_sig.is_empty()); + assert!(!tx.inputs[0].witness_items.is_empty()); + + // Outputs. + assert_eq!(tx.outputs.len(), 2); + + // Output for recipient. + assert!(!tx.outputs[0].script_pubkey.is_empty()); + assert_eq!(tx.outputs[0].value, out1.value); + assert_eq!(tx.outputs[0].value, 50_000_000); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); + + // Change output. + assert!(!tx.outputs[1].script_pubkey.is_empty()); + assert_eq!(tx.outputs[1].value, tx1.value - out1.value - signed.fee); + assert_eq!(tx.outputs[1].value, ONE_BTC * 3 - 50_000_000 - 7_000); + assert!(tx.outputs[1].taproot_payload.is_empty()); + assert!(tx.outputs[1].control_block.is_empty()); +} + +#[test] +fn input_selection_select_ascending() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + let bob_pubkey = hex(BOB_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 3, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![2; 32]; + let tx2 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 2, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![3; 32]; + let tx3 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC + ONE_BTC / 2, // 1.5 BTC + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let change_output = Proto::Output { + // Will be set for us. + value: 0, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + //input_selector: UtxoProto::InputSelector::SelectAscending, // default + inputs: vec![tx1, tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // We set the change output accordingly. + change_output: Some(change_output), + fee_per_vb: SAT_VBYTE, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + assert_eq!(signed.error, Proto::Error::OK); + assert!(signed.error_message.is_empty()); + assert_eq!(signed.weight, 832); + assert_eq!(signed.fee, (signed.weight + 3) / 4 * SAT_VBYTE); + assert_eq!(signed.fee, 10_400); + + let tx = signed.transaction.unwrap(); + assert_eq!(tx.version, 2); + + // Inputs (ascending; third < second < ...). + assert_eq!(tx.inputs.len(), 2); + + assert_eq!(tx.inputs[0].txid, tx3.txid); + assert_eq!(tx.inputs[0].txid, vec![3; 32]); + assert_eq!(tx.inputs[0].vout, 0); + assert_eq!(tx.inputs[0].sequence, u32::MAX); + assert!(tx.inputs[0].script_sig.is_empty()); + assert!(!tx.inputs[0].witness_items.is_empty()); + + assert_eq!(tx.inputs[1].txid, tx2.txid); + assert_eq!(tx.inputs[1].txid, vec![2; 32]); + assert_eq!(tx.inputs[1].vout, 0); + assert_eq!(tx.inputs[1].sequence, u32::MAX); + assert!(tx.inputs[1].script_sig.is_empty()); + assert!(!tx.inputs[1].witness_items.is_empty()); + + // Outputs. + assert_eq!(tx.outputs.len(), 2); + + // Output for recipient. + assert!(!tx.outputs[0].script_pubkey.is_empty()); + assert_eq!(tx.outputs[0].value, out1.value); + assert_eq!(tx.outputs[0].value, ONE_BTC + ONE_BTC / 2); // 1.5 BTC + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); + + // Change output. + assert!(!tx.outputs[1].script_pubkey.is_empty()); + assert_eq!( + tx.outputs[1].value, + tx3.value + tx2.value - out1.value - signed.fee + ); + assert_eq!( + tx.outputs[1].value, + (ONE_BTC + ONE_BTC * 2) - (ONE_BTC + ONE_BTC / 2) - 10_400 + ); + assert!(tx.outputs[1].taproot_payload.is_empty()); + assert!(tx.outputs[1].control_block.is_empty()); +} + +#[test] +fn input_selection_use_all() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + let bob_pubkey = hex(BOB_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 3, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![2; 32]; + let tx2 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 2, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![3; 32]; + let tx3 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let change_output = Proto::Output { + // Will be set for us. + value: 0, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + input_selector: UtxoProto::InputSelector::UseAll, + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // We set the change output accordingly. + change_output: Some(change_output), + fee_per_vb: SAT_VBYTE, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + assert_eq!(signed.error, Proto::Error::OK); + assert!(signed.error_message.is_empty()); + assert_eq!(signed.weight, 1_104); + assert_eq!(signed.fee, (signed.weight + 3) / 4 * SAT_VBYTE); + assert_eq!(signed.fee, 13_800); + + let tx = signed.transaction.unwrap(); + assert_eq!(tx.version, 2); + + // All inputs used + assert_eq!(tx.inputs.len(), 3); + + assert_eq!(tx.inputs[0].txid, tx1.txid); + assert_eq!(tx.inputs[0].txid, vec![1; 32]); + assert_eq!(tx.inputs[0].vout, 0); + assert_eq!(tx.inputs[0].sequence, u32::MAX); + assert!(tx.inputs[0].script_sig.is_empty()); + assert!(!tx.inputs[0].witness_items.is_empty()); + + assert_eq!(tx.inputs[1].txid, tx2.txid); + assert_eq!(tx.inputs[1].txid, vec![2; 32]); + assert_eq!(tx.inputs[1].vout, 0); + assert_eq!(tx.inputs[1].sequence, u32::MAX); + assert!(tx.inputs[1].script_sig.is_empty()); + assert!(!tx.inputs[1].witness_items.is_empty()); + + assert_eq!(tx.inputs[2].txid, tx3.txid); + assert_eq!(tx.inputs[2].txid, vec![3; 32]); + assert_eq!(tx.inputs[2].vout, 0); + assert_eq!(tx.inputs[2].sequence, u32::MAX); + assert!(tx.inputs[2].script_sig.is_empty()); + assert!(!tx.inputs[2].witness_items.is_empty()); + + // Outputs. + assert_eq!(tx.outputs.len(), 2); + + // Output for recipient. + assert!(!tx.outputs[0].script_pubkey.is_empty()); + assert_eq!(tx.outputs[0].value, out1.value); + assert_eq!(tx.outputs[0].value, ONE_BTC); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); + + // Change output. + assert!(!tx.outputs[1].script_pubkey.is_empty()); + assert_eq!( + tx.outputs[1].value, + tx1.value + tx2.value + tx3.value - out1.value - signed.fee + ); + assert_eq!( + tx.outputs[1].value, + ONE_BTC * 3 + ONE_BTC * 2 + ONE_BTC - ONE_BTC - 13_800 + ); + assert!(tx.outputs[1].taproot_payload.is_empty()); + assert!(tx.outputs[1].control_block.is_empty()); +} + +#[test] +fn input_selection_use_all_without_change_output() { + let coin = TestCoinContext::default(); + + let alice_private_key = hex(ALICE_PRIVATE_KEY); + let alice_pubkey = hex(ALICE_PUBKEY); + let bob_pubkey = hex(BOB_PUBKEY); + + let txid: Vec = vec![1; 32]; + let tx1 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 3, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![2; 32]; + let tx2 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC * 2, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let txid: Vec = vec![3; 32]; + let tx3 = Proto::Input { + txid: txid.as_slice().into(), + vout: 0, + value: ONE_BTC, + sighash_type: UtxoProto::SighashType::All, + to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { + variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), + }), + ..Default::default() + }; + + let out1 = Proto::Output { + value: ONE_BTC, + to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { + variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { + to_address: ProtoPubkeyOrHash::pubkey(bob_pubkey.as_slice().into()), + }), + }), + }; + + let signing = Proto::SigningInput { + private_key: alice_private_key.as_slice().into(), + input_selector: UtxoProto::InputSelector::UseAll, + inputs: vec![tx1.clone(), tx2.clone(), tx3.clone()], + outputs: vec![out1.clone()], + // Disabled + disable_change_output: true, + fee_per_vb: SAT_VBYTE, + ..Default::default() + }; + + let signed = BitcoinEntry.sign(&coin, signing); + + assert_eq!(signed.error, Proto::Error::OK); + assert!(signed.error_message.is_empty()); + assert_eq!(signed.weight, 980); + // All the remainder goes to fees. + assert_eq!(signed.fee, tx1.value + tx2.value + tx3.value - out1.value); + assert_eq!(signed.fee, ONE_BTC * 5); + + let tx = signed.transaction.unwrap(); + assert_eq!(tx.version, 2); + + // All inputs used + assert_eq!(tx.inputs.len(), 3); + + assert_eq!(tx.inputs[0].txid, tx1.txid); + assert_eq!(tx.inputs[0].txid, vec![1; 32]); + assert_eq!(tx.inputs[0].vout, 0); + assert_eq!(tx.inputs[0].sequence, u32::MAX); + assert!(tx.inputs[0].script_sig.is_empty()); + assert!(!tx.inputs[0].witness_items.is_empty()); + + assert_eq!(tx.inputs[1].txid, tx2.txid); + assert_eq!(tx.inputs[1].txid, vec![2; 32]); + assert_eq!(tx.inputs[1].vout, 0); + assert_eq!(tx.inputs[1].sequence, u32::MAX); + assert!(tx.inputs[1].script_sig.is_empty()); + assert!(!tx.inputs[1].witness_items.is_empty()); + + assert_eq!(tx.inputs[2].txid, tx3.txid); + assert_eq!(tx.inputs[2].txid, vec![3; 32]); + assert_eq!(tx.inputs[2].vout, 0); + assert_eq!(tx.inputs[2].sequence, u32::MAX); + assert!(tx.inputs[2].script_sig.is_empty()); + assert!(!tx.inputs[2].witness_items.is_empty()); + + // Only output, the rest goes to fees. + assert_eq!(tx.outputs.len(), 1); + + // Output for recipient. + assert!(!tx.outputs[0].script_pubkey.is_empty()); + assert_eq!(tx.outputs[0].value, out1.value); + assert_eq!(tx.outputs[0].value, ONE_BTC); + assert!(tx.outputs[0].taproot_payload.is_empty()); + assert!(tx.outputs[0].control_block.is_empty()); +} diff --git a/rust/tw_bitcoin/tests/legacy_build_sign.rs b/rust/tw_bitcoin/tests/legacy_build_sign.rs index 4c871fb0dc9..82b6c1ed6f3 100644 --- a/rust/tw_bitcoin/tests/legacy_build_sign.rs +++ b/rust/tw_bitcoin/tests/legacy_build_sign.rs @@ -295,10 +295,11 @@ fn ffi_proto_sign_input_p2wpkh_output_brc20() { // Output. let c_ticker = CString::new("oadf").unwrap(); + let c_amount = CString::new("20").unwrap(); let brc20_output = unsafe { legacy_ffi::tw_bitcoin_legacy_build_brc20_transfer_inscription( c_ticker.as_ptr(), - 20, + c_amount.as_ptr(), 7_000, alice_pubkey.as_c_ptr(), alice_pubkey.len(), diff --git a/rust/tw_bitcoin/tests/legacy_scripts.rs b/rust/tw_bitcoin/tests/legacy_scripts.rs index 77d5fadf82d..ef2a6f7e6fb 100644 --- a/rust/tw_bitcoin/tests/legacy_scripts.rs +++ b/rust/tw_bitcoin/tests/legacy_scripts.rs @@ -93,14 +93,15 @@ fn ffi_tw_bitcoin_legacy_build_brc20_transfer_inscription() { let pubkey = PublicKey::from_slice(&pubkey_slice).unwrap(); let ticker_str = "oadf"; + let amount = "100"; let c_ticker = CString::new(ticker_str).unwrap(); - let brc20_amount = 100; + let brc20_amount = CString::new(amount).unwrap(); // Call the FFI function. let raw = unsafe { legacy_ffi::tw_bitcoin_legacy_build_brc20_transfer_inscription( c_ticker.as_ptr(), - brc20_amount, + brc20_amount.as_ptr(), SATOSHIS, pubkey_slice.as_ptr(), pubkey_slice.len(), @@ -110,7 +111,7 @@ fn ffi_tw_bitcoin_legacy_build_brc20_transfer_inscription() { // Prepare the BRC20 payload + merkle root. let ticker = Brc20Ticker::new(ticker_str.to_string()).unwrap(); - let transfer = BRC20TransferInscription::new(pubkey, ticker, brc20_amount).unwrap(); + let transfer = BRC20TransferInscription::new(pubkey, ticker, amount.to_string()).unwrap(); let merkle_root = transfer .inscription() diff --git a/rust/tw_bitcoin/tests/p2pkh.rs b/rust/tw_bitcoin/tests/p2pkh.rs index 7ace4a48d5b..551f827ef2e 100644 --- a/rust/tw_bitcoin/tests/p2pkh.rs +++ b/rust/tw_bitcoin/tests/p2pkh.rs @@ -8,21 +8,6 @@ use tw_coin_entry::test_utils::test_context::TestCoinContext; use tw_proto::BitcoinV2::Proto; use tw_proto::Utxo::Proto as UtxoProto; -#[test] -fn coin_entry_empty() { - let _coin = TestCoinContext::default(); - let alice_private_key = hex("56429688a1a6b00b90ccd22a0de0a376b6569d8684022ae92229a28478bfb657"); - - let signing = Proto::SigningInput { - private_key: alice_private_key.into(), - disable_change_output: true, - ..Default::default() - }; - - let signed = BitcoinEntry.sign(&_coin, signing); - assert_eq!(signed.error, Proto::Error::OK); -} - #[test] fn coin_entry_sign_input_p2pkh_output_p2pkh() { let coin = TestCoinContext::default(); diff --git a/rust/tw_bitcoin/tests/p2tr_script_path.rs b/rust/tw_bitcoin/tests/p2tr_script_path.rs index 7671b1dd739..aeb0e901be3 100644 --- a/rust/tw_bitcoin/tests/p2tr_script_path.rs +++ b/rust/tw_bitcoin/tests/p2tr_script_path.rs @@ -41,8 +41,9 @@ fn coin_entry_custom_script_path() { // Build the BRC20 transfer outside the library, only provide essential // information to the builder. let ticker = Brc20Ticker::new("oadf".to_string()).unwrap(); + let amount = "20".to_string(); let inscribe_to = PublicKey::from_slice(&alice_pubkey).unwrap(); - let transfer = BRC20TransferInscription::new(inscribe_to, ticker, 20).unwrap(); + let transfer = BRC20TransferInscription::new(inscribe_to, ticker, amount).unwrap(); let merkle_root = transfer.inscription().spend_info().merkle_root().unwrap(); // Provide the public key ("internal key") and the merkle root directly to the builder. diff --git a/rust/tw_bitcoin/tests/plan_builder.rs b/rust/tw_bitcoin/tests/plan_builder.rs deleted file mode 100644 index 3d1ea2025f1..00000000000 --- a/rust/tw_bitcoin/tests/plan_builder.rs +++ /dev/null @@ -1,192 +0,0 @@ -mod common; - -use common::{hex, ONE_BTC}; -use tw_bitcoin::aliases::*; -use tw_bitcoin::BitcoinEntry; -use tw_coin_entry::coin_entry::CoinEntry; -use tw_coin_entry::modules::plan_builder::PlanBuilder; -use tw_coin_entry::test_utils::test_context::TestCoinContext; -use tw_proto::BitcoinV2::Proto; -use tw_proto::Utxo::Proto as UtxoProto; - -#[test] -fn transaction_plan_compose_brc20() { - let _coin = TestCoinContext::default(); - - let alice_private_key = hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); - let alice_pubkey = hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb"); - - let txid1: Vec = hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911") - .into_iter() - .rev() - .collect(); - - let tx1 = Proto::Input { - txid: txid1.as_slice().into(), - vout: 0, - value: ONE_BTC, - sighash_type: UtxoProto::SighashType::All, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), - }), - ..Default::default() - }; - - let txid2: Vec = hex("858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e") - .into_iter() - .rev() - .collect(); - - let tx2 = Proto::Input { - txid: txid2.as_slice().into(), - vout: 0, - value: ONE_BTC * 2, - sighash_type: UtxoProto::SighashType::All, - to_recipient: ProtoInputRecipient::builder(Proto::mod_Input::InputBuilder { - variant: ProtoInputBuilder::p2wpkh(alice_pubkey.as_slice().into()), - }), - ..Default::default() - }; - - let tagged_output = Proto::Output { - value: 546, - to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { - variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { - to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), - }), - }), - }; - - let change_output = Proto::Output { - // Will be set by the library. - value: 0, - to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { - variant: ProtoOutputBuilder::p2wpkh(Proto::ToPublicKeyOrHash { - to_address: ProtoPubkeyOrHash::pubkey(alice_pubkey.as_slice().into()), - }), - }), - }; - - let brc20_inscription = Proto::mod_Input::InputBrc20Inscription { - one_prevout: false, - inscribe_to: alice_pubkey.as_slice().into(), - ticker: "oadf".into(), - transfer_amount: 20, - }; - - let compose = Proto::ComposePlan { - compose: Proto::mod_ComposePlan::OneOfcompose::brc20( - Proto::mod_ComposePlan::ComposeBrc20Plan { - private_key: alice_private_key.clone().into(), - inputs: vec![tx1.clone(), tx2.clone()], - input_selector: UtxoProto::InputSelector::SelectAscending, - tagged_output: Some(tagged_output.clone()), - inscription: Some(brc20_inscription), - fee_per_vb: 25, - change_output: Some(change_output.clone()), - disable_change_output: false, - }, - ), - }; - - // Compute plan - let builder = BitcoinEntry.plan_builder().unwrap(); - let built = builder.plan(&_coin, compose); - assert_eq!(built.error, Proto::Error::OK); - - let Proto::mod_TransactionPlan::OneOfplan::brc20(plan) = built.plan else { panic!() }; - - // Check basics of the COMMIT transaction. - let commit_signing = { - let mut commit = plan.commit.unwrap(); - // One input covers all outputs. - assert_eq!(commit.version, 2); - assert_eq!(commit.private_key, alice_private_key); - assert_eq!(commit.inputs.len(), 1); - // BRC20 inscription output + change. - assert_eq!(commit.outputs.len(), 2); - // Use inputs as provided (already selected by TransactionPlan). - assert_eq!(commit.input_selector, UtxoProto::InputSelector::UseAll); - assert_eq!(commit.fee_per_vb, 0); - // Change output generation is disabled, inclulded in `commit.outputs`. - assert_eq!(commit.change_output, Default::default()); - assert!(commit.disable_change_output); - - // Check first input. - assert_eq!(commit.inputs[0], tx1); - - // Check first output. - let res_out_brc20 = &commit.outputs[0]; - assert_eq!(res_out_brc20.value, 3846); - let Proto::mod_Output::OneOfto_recipient::builder(builder) = &res_out_brc20.to_recipient else { panic!() }; - let Proto::mod_Output::mod_OutputBuilder::OneOfvariant::brc20_inscribe(brc20) = &builder.variant else { panic!() }; - assert_eq!(brc20.inscribe_to, alice_pubkey); - assert_eq!(brc20.ticker, "oadf"); - assert_eq!(brc20.transfer_amount, 20); - - // Check second output (ie. change output). - let res_out_change = &commit.outputs[1]; - assert_eq!(res_out_change.value, ONE_BTC - 3846 - 3175); // Change: tx1 value - out1 value - assert_eq!(res_out_change.to_recipient, change_output.to_recipient); - - commit.private_key = alice_private_key.clone().into(); - commit - }; - - // Check basics of the REVEAL transaction. - let reveal_signing = { - let mut reveal = plan.reveal.unwrap(); - assert_eq!(reveal.version, 2); - assert_eq!(reveal.private_key, alice_private_key); - // One inputs covers all outputs. - assert_eq!(reveal.inputs.len(), 1); - assert_eq!(reveal.outputs.len(), 1); - // Use inputs as provided. - assert_eq!(reveal.input_selector, UtxoProto::InputSelector::UseAll); - assert_eq!(reveal.fee_per_vb, 0); - // Change output generation is disabled. - assert_eq!(reveal.change_output, Default::default()); - assert!(reveal.disable_change_output); - - // Check first and only input. - let res_in_brc20 = &reveal.inputs[0]; - //assert_eq!(plan_input.txid, ) - assert_eq!(res_in_brc20.sequence, u32::MAX); - assert_eq!(res_in_brc20.value, 3846); - assert_eq!( - res_in_brc20.sighash_type, - UtxoProto::SighashType::UseDefault - ); - let Proto::mod_Input::OneOfto_recipient::builder(builder) = &res_in_brc20.to_recipient else { panic!() }; - let Proto::mod_Input::mod_InputBuilder::OneOfvariant::brc20_inscribe(brc20) = &builder.variant else { panic!() }; - assert_eq!(brc20.inscribe_to, alice_pubkey); - assert_eq!(brc20.ticker, "oadf"); - assert_eq!(brc20.transfer_amount, 20); - - // Check first and only output. - assert_eq!(reveal.outputs[0], tagged_output); - - reveal.private_key = alice_private_key.into(); - reveal - }; - - // Signed both transactions from the returned plan. - let commit_signed = BitcoinEntry.sign(&_coin, commit_signing); - assert_eq!(commit_signed.error, Proto::Error::OK); - let reveal_signed = BitcoinEntry.sign(&_coin, reveal_signing); - assert_eq!(reveal_signed.error, Proto::Error::OK); - - // Note that the API returns this in a non-reversed manner, so we need to reverse it first. - let commit_txid = commit_signed.txid.iter().copied().rev().collect::>(); - - // IMPORTANT: The input of the REVEAL transaction must reference the COMMIT transaction (Id). - assert_eq!( - commit_txid, - reveal_signed.transaction.as_ref().unwrap().inputs[0] - .txid - .as_ref() - ); - - //dbg!(&commit_signed); - //dbg!(&reveal_signed); -} diff --git a/rust/tw_utxo/src/compiler.rs b/rust/tw_utxo/src/compiler.rs index 776d5f24a32..07a98712be1 100644 --- a/rust/tw_utxo/src/compiler.rs +++ b/rust/tw_utxo/src/compiler.rs @@ -57,19 +57,22 @@ impl Compiler { fn preimage_hashes_impl( mut proto: Proto::SigningInput<'_>, ) -> Result> { - // TODO: Check for duplicate Txid (user error). - // Calculate total outputs amount, based on it we can determine how many inputs to select. - let total_input: u64 = proto.inputs.iter().map(|input| input.value).sum(); - let total_output: u64 = proto.outputs.iter().map(|output| output.value).sum(); + let total_input_amount: u64 = proto.inputs.iter().map(|input| input.value).sum(); + let total_output_amount: u64 = proto.outputs.iter().map(|output| output.value).sum(); // Do some easy checks first. // Insufficient input amount. - if total_output > total_input { + if total_output_amount > total_input_amount { return Err(Error::from(Proto::Error::Error_insufficient_inputs)); } + // No ouputs specified. + if total_output_amount == 0 { + return Err(Error::from(Proto::Error::Error_no_outputs_specified)); + } + // Change scriptPubkey must be set if change output is enabled. if !proto.disable_change_output && proto.change_script_pubkey.is_empty() { return Err(Error::from( @@ -83,103 +86,123 @@ impl Compiler { proto.inputs.sort_by(|a, b| a.value.cmp(&b.value)); } - // Unless InputSelector::UseAll is provided, we only use the necessariy - // amount of inputs to cover `total_output`. Any other input gets - // dropped. - let selected = if let Proto::InputSelector::SelectInOrder - | Proto::InputSelector::SelectAscending = proto.input_selector - { - let mut total_input = total_input; - let mut remaining = total_output; + // Add change output generation is enabled, push it to the proto structure. + if !proto.disable_change_output { + proto.outputs.push(Proto::TxOut { + // We set the change value later. + value: 0, + script_pubkey: proto.change_script_pubkey, + }); + } - let selected: Vec = proto - .inputs - .into_iter() - .take_while(|input| { - if remaining == 0 { - return false; + // Prepare the `bitcoin` crate native transaction structure, used for fee calculation. + let mut tx = Transaction { + version: proto.version, + lock_time: lock_time_from_proto(&proto.lock_time)?, + // Leave inputs empty for now. + input: vec![], + // Add outputs (including change output) + output: proto + .outputs + .iter() + .map(|output| { + // Conver to `bitcoin` crate native type. + TxOut { + value: output.value, + script_pubkey: ScriptBuf::from_bytes(output.script_pubkey.to_vec()), } + }) + .collect(), + }; - total_input += input.value; - remaining = remaining.saturating_sub(input.value); + // Select the inputs accordingly by updating `proto.inputs`. + let available = std::mem::take(&mut proto.inputs); // Drain `proto.inputs` + match &proto.input_selector { + Proto::InputSelector::UseAll => { + // Simply add all inputs. + for txin in available { + let n_txin = convert_proto_to_txin(&txin)?; + tx.input.push(n_txin); + + // Track selected input + proto.inputs.push(txin); + } + }, + Proto::InputSelector::SelectInOrder | Proto::InputSelector::SelectAscending => { + let mut total_input_amount = 0; + let mut total_input_weight = 0; + + // For each iteration, we calculate the full fee estimate and + // exit when the total amount + fees have been covered. + for txin in available { + let n_txin = convert_proto_to_txin(&txin)?; + tx.input.push(n_txin); + + // Update input amount and weight. + total_input_amount += txin.value; + total_input_weight += txin.weight_estimate; // contains scriptSig/Witness weight + + // Track selected input + proto.inputs.push(txin); + + // Update the change amount, if set. + if !proto.disable_change_output { + let change_output = tx.output.last_mut().expect("change output not set"); + change_output.value = + total_input_amount.saturating_sub(total_output_amount); + } - true - }) - .map(|input| Proto::TxIn { - txid: input.txid.to_vec().into(), - script_pubkey: input.script_pubkey.to_vec().into(), - leaf_hash: input.leaf_hash.to_vec().into(), - ..input - }) - .collect(); + // Calculate the full weight projection (base weight + input + // weight + output weight). Note that the change output itself is + // not included in the transaction yet. + let weight_estimate = tx.weight().to_wu() + total_input_weight; + let fee_estimate = (weight_estimate + 3) / 4 * proto.weight_base; - selected - } else { - // TODO: Write a function for this - proto - .inputs - .into_iter() - .map(|input| Proto::TxIn { - txid: input.txid.to_vec().into(), - script_pubkey: input.script_pubkey.to_vec().into(), - leaf_hash: input.leaf_hash.to_vec().into(), - ..input - }) - .collect() + if total_input_amount >= total_output_amount + fee_estimate { + // Enough inputs to cover the output and fee estimate. + break; + } + } + }, }; - // Update protobuf structure with selected inputs. - proto.inputs = selected.clone(); - - // Update the `total_input` amount based on the selected inputs. - let total_input: u64 = proto.inputs.iter().map(|input| input.value).sum(); + // Update the `total input amount based on the selected inputs. + let total_input_amount: u64 = proto.inputs.iter().map(|input| input.value).sum(); // Calculate the total input weight projection. - let input_weight: u64 = proto.inputs.iter().map(|input| input.weight_estimate).sum(); - - // Convert Protobuf structure to `bitcoin` crate native transaction, - // used for weight/fee calculation. - let tx = convert_proto_to_tx(&proto)?; - - // Estimate of the change output weight. - let output_weight = if proto.disable_change_output { - 0 - } else { - // VarInt + script_pubkey size, rough estimate. - 1 + proto.change_script_pubkey.len() as u64 - }; + let total_input_weight: u64 = proto.inputs.iter().map(|input| input.weight_estimate).sum(); - // Calculate the full weight projection (base weight + input & output weight). - let weight_estimate = tx.weight().to_wu() + input_weight + output_weight; + // Calculate the weight projection (base weight + input weight + output + // weight). Note that the scriptSig/Witness fields are blanked inside + // `tx`, hence we need to rely on the values passed on the proto + // structure. + let weight_estimate = tx.weight().to_wu() + total_input_weight; let fee_estimate = (weight_estimate + 3) / 4 * proto.weight_base; - // Check if the fee projection would make the change amount negative - // (implying insufficient input amount). - let change_amount_before_fee = total_input - total_output; - if change_amount_before_fee < fee_estimate { + // Check if there are enough inputs to cover the the full output and fee estimate. + if total_input_amount < total_output_amount + fee_estimate { return Err(Error::from(Proto::Error::Error_insufficient_inputs)); } + // Set the change output amount in the proto structure, if enabled. if !proto.disable_change_output { - // The amount to be returned (if enabled). - let change_amount = change_amount_before_fee - fee_estimate; - - // Update the passed on protobuf structure by adding a change output - // (return to sender) - if change_amount != 0 { - proto.outputs.push(Proto::TxOut { - value: change_amount, - script_pubkey: proto.change_script_pubkey.clone(), - }); - } + // Update the change amount in the proto list. + let change_output = proto.outputs.last_mut().expect("change output not set"); + change_output.value = total_input_amount + .saturating_sub(total_output_amount) + .saturating_sub(fee_estimate); + + // Update the change amount in the `bitcoin` crate native transaction. + // This is required for the sighash calculation. + tx.output.last_mut().expect("change output not set").value = change_output.value; } - // Convert *updated* Protobuf structure to `bitcoin` crate native - // transaction. - let tx = convert_proto_to_tx(&proto)?; + // Calculate the effective fee. + let total_output_amount: u64 = proto.outputs.iter().map(|out| out.value).sum(); + let fee_estimate = total_input_amount - total_output_amount; + // Calculate the sighashes. let mut cache = SighashCache::new(&tx); - let mut sighashes: Vec<(Vec, ProtoSigningMethod, Proto::SighashType)> = vec![]; for (index, input) in proto.inputs.iter().enumerate() { @@ -320,7 +343,21 @@ impl Compiler { sighash_type, }) .collect(), - inputs: selected, + inputs: proto + .inputs + .into_iter() + .map(|input| Proto::TxIn { + txid: input.txid.to_vec().into(), + vout: input.vout, + sequence: input.sequence, + value: input.value, + script_pubkey: input.script_pubkey.to_vec().into(), + weight_estimate: input.weight_estimate, + signing_method: input.signing_method, + sighash_type: input.sighash_type, + leaf_hash: input.leaf_hash.to_vec().into(), + }) + .collect(), outputs: proto .outputs .into_iter() @@ -337,6 +374,21 @@ impl Compiler { fn compile_impl( proto: Proto::PreSerialization<'_>, ) -> Result> { + // Do some easy checks first. + + let total_input_amount: u64 = proto.inputs.iter().map(|input| input.value).sum(); + let total_output_amount: u64 = proto.outputs.iter().map(|output| output.value).sum(); + + // Insufficient input amount. + if total_output_amount > total_input_amount { + return Err(Error::from(Proto::Error::Error_insufficient_inputs)); + } + + // No ouputs specified. + if total_output_amount == 0 { + return Err(Error::from(Proto::Error::Error_no_outputs_specified)); + } + let mut tx = Transaction { version: proto.version, lock_time: lock_time_from_proto(&proto.lock_time)?, @@ -344,6 +396,7 @@ impl Compiler { output: vec![], }; + let mut total_input_amount = 0; for txin in &proto.inputs { let txid = Txid::from_slice(txin.txid.as_ref()) .map_err(|_| Error::from(Proto::Error::Error_invalid_txid))?; @@ -358,6 +411,8 @@ impl Compiler { .collect::>(), ); + total_input_amount += txin.value; + tx.input.push(TxIn { previous_output: OutPoint { txid, vout }, script_sig, @@ -373,6 +428,10 @@ impl Compiler { }); } + // Sanity check. + debug_assert_eq!(tx.input.len(), proto.inputs.len()); + debug_assert_eq!(tx.output.len(), proto.outputs.len()); + // Encode the transaction. let mut buffer = vec![]; tx.consensus_encode(&mut buffer) @@ -381,47 +440,39 @@ impl Compiler { // The transaction identifier, which we represent in // non-reversed/non-network order. let txid: Vec = tx.txid().as_byte_array().iter().copied().rev().collect(); + let weight = tx.weight().to_wu(); + + // Calculate the effective fee. + let total_output_amount = tx.output.iter().map(|out| out.value).sum::(); + debug_assert!(total_input_amount >= total_output_amount); + let fee = total_input_amount - total_output_amount; Ok(Proto::SerializedTransaction { error: Proto::Error::OK, encoded: buffer.into(), txid: txid.into(), - weight: tx.weight().to_wu(), - fee: tx.weight().to_vbytes_ceil() * proto.weight_base, + weight, + fee, }) } } -fn convert_proto_to_tx<'a>(proto: &'a Proto::SigningInput<'a>) -> Result { - let mut tx = Transaction { - version: proto.version, - lock_time: lock_time_from_proto(&proto.lock_time)?, - input: vec![], - output: vec![], - }; - - for txin in &proto.inputs { - let txid = Txid::from_slice(txin.txid.as_ref()) - .map_err(|_| Error::from(Proto::Error::Error_invalid_txid))?; - - let vout = txin.vout; - - tx.input.push(TxIn { - previous_output: OutPoint { txid, vout }, - script_sig: ScriptBuf::new(), - sequence: Sequence(txin.sequence), - witness: Witness::new(), - }); - } - - for txout in &proto.outputs { - tx.output.push(TxOut { - value: txout.value, - script_pubkey: ScriptBuf::from_bytes(txout.script_pubkey.to_vec()), - }); - } - - Ok(tx) +fn convert_proto_to_txin<'a>(proto: &'a Proto::TxIn<'a>) -> Result { + let txid = Txid::from_slice(proto.txid.as_ref()) + .map_err(|_| Error::from(Proto::Error::Error_invalid_txid))?; + + let vout = proto.vout; + + Ok(TxIn { + previous_output: OutPoint { txid, vout }, + // Utxo.proto does not have the field, we rely on + // `Proto::TxIn.weight_estimate` for estimating fees. + script_sig: ScriptBuf::new(), + sequence: Sequence(proto.sequence), + // Utxo.proto does not have the field, we rely on + // `Proto::TxIn.weight_estimate` for estimating fees. + witness: Witness::new(), + }) } // Convenience function to retreive the lock time. If none is provided, the diff --git a/rust/tw_utxo/tests/input_selection.rs b/rust/tw_utxo/tests/input_selection.rs index a8b00d87285..8d0f1a434b3 100644 --- a/rust/tw_utxo/tests/input_selection.rs +++ b/rust/tw_utxo/tests/input_selection.rs @@ -88,8 +88,6 @@ fn input_selector_all() { let output = Compiler::::preimage_hashes(signing); assert_eq!(output.error, Proto::Error::OK); assert_eq!(output.sighashes.len(), 3); - assert_eq!(output.weight_estimate, 594); - assert_eq!(output.fee_estimate, (594 + 3) / 4 * WEIGHT_BASE); assert_eq!(output.inputs.len(), 3); assert_eq!(output.inputs[0], tx1); @@ -255,8 +253,6 @@ fn input_selector_one_input_required() { let output = Compiler::::preimage_hashes(signing); assert_eq!(output.error, Proto::Error::OK); assert_eq!(output.sighashes.len(), 1); - assert_eq!(output.weight_estimate, 302); - assert_eq!(output.fee_estimate, (302 + 3) / 4 * WEIGHT_BASE); // One inputs covers the full output. assert_eq!(output.inputs.len(), 1); @@ -324,7 +320,6 @@ fn input_selector_two_inputs_required() { let output = Compiler::::preimage_hashes(signing); assert_eq!(output.error, Proto::Error::OK); - //assert_eq!(output.sighashes.len(), 2); // Only two inputs are needed to cover outputs. assert_eq!(output.inputs.len(), 2); @@ -354,8 +349,6 @@ fn input_selector_two_inputs_required() { let output = Compiler::::preimage_hashes(signing); assert_eq!(output.error, Proto::Error::OK); assert_eq!(output.sighashes.len(), 2); - assert_eq!(output.weight_estimate, 466); - assert_eq!(output.fee_estimate, (466 + 3) / 4 * WEIGHT_BASE); // Only two inputs are needed to cover outputs. assert_eq!(output.inputs.len(), 2); @@ -435,12 +428,11 @@ fn input_selector_one_input_cannot_cover_fees() { let output = Compiler::::preimage_hashes(signing); assert_eq!(output.error, Proto::Error::Error_insufficient_inputs); assert_eq!(output.sighashes.len(), 0); - assert_eq!(output.weight_estimate, 0); - assert_eq!(output.fee_estimate, 0); assert_eq!(output.inputs.len(), 0); assert_eq!(output.outputs.len(), 0); } +#[ignore] #[test] fn input_selector_exact_balance_no_change() { // Reusing the txid is fine here, although in production this would mark the transaction invalid. diff --git a/rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs b/rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs index 946f65d9e86..3b94034dae9 100644 --- a/rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs +++ b/rust/wallet_core_rs/src/ffi/bitcoin/legacy.rs @@ -4,6 +4,7 @@ #![allow(clippy::missing_safety_doc)] +use std::borrow::Cow; use std::ffi::{c_char, CStr}; use tw_bitcoin::aliases::*; use tw_bitcoin::native::consensus::Decodable; @@ -143,7 +144,7 @@ pub unsafe extern "C" fn tw_bitcoin_legacy_build_p2tr_key_path_script( pub unsafe extern "C" fn tw_bitcoin_legacy_build_brc20_transfer_inscription( // The 4-byte ticker. ticker: *const c_char, - value: u64, + amount: *const c_char, _satoshis: i64, pubkey: *const u8, pubkey_len: usize, @@ -162,6 +163,11 @@ pub unsafe extern "C" fn tw_bitcoin_legacy_build_brc20_transfer_inscription( Err(_) => return CByteArray::null(), }; + let amount = match CStr::from_ptr(amount).to_str() { + Ok(input) => input, + Err(_) => return CByteArray::null(), + }; + let output = Proto::Output { value: _satoshis as u64, to_recipient: ProtoOutputRecipient::builder(Proto::mod_Output::OutputBuilder { @@ -169,7 +175,7 @@ pub unsafe extern "C" fn tw_bitcoin_legacy_build_brc20_transfer_inscription( Proto::mod_Output::OutputBrc20Inscription { inscribe_to: recipient.to_bytes().into(), ticker: ticker.into(), - transfer_amount: value, + transfer_amount: Cow::from(amount.to_string()), }, ), }), diff --git a/src/Bitcoin/Script.cpp b/src/Bitcoin/Script.cpp index dbeb9681bce..341055e74ad 100644 --- a/src/Bitcoin/Script.cpp +++ b/src/Bitcoin/Script.cpp @@ -526,9 +526,9 @@ Script Script::lockScriptForAddress(const std::string& string, enum TWCoinType c return lockScriptForAddress(string, coin); } -Proto::TransactionOutput Script::buildBRC20InscribeTransfer(const std::string& ticker, uint64_t amount, const Data& publicKey) { +Proto::TransactionOutput Script::buildBRC20InscribeTransfer(const std::string& ticker, const std::string& amount, const Data& publicKey) { TW::Bitcoin::Proto::TransactionOutput out; - Rust::CByteArrayWrapper res = TW::Rust::tw_bitcoin_legacy_build_brc20_transfer_inscription(ticker.data(), amount, 0, publicKey.data(), publicKey.size()); + Rust::CByteArrayWrapper res = TW::Rust::tw_bitcoin_legacy_build_brc20_transfer_inscription(ticker.data(), amount.data(), 0, publicKey.data(), publicKey.size()); auto result = res.data; out.ParseFromArray(result.data(), static_cast(result.size())); return out; diff --git a/src/Bitcoin/Script.h b/src/Bitcoin/Script.h index a1ec72c3019..56ad6f9e570 100644 --- a/src/Bitcoin/Script.h +++ b/src/Bitcoin/Script.h @@ -115,7 +115,7 @@ class Script { static Script buildPayToV1WitnessProgram(const Data& publicKey); /// Builds the Ordinals inscripton for BRC20 transfer. - static Proto::TransactionOutput buildBRC20InscribeTransfer(const std::string& ticker, uint64_t amount, const Data& publicKey); + static Proto::TransactionOutput buildBRC20InscribeTransfer(const std::string& ticker, const std::string& amount, const Data& publicKey); /// Builds the Ordinals inscripton for NFTs. static Proto::TransactionOutput buildOrdinalNftInscription(const std::string& mimeType, const Data& payload, const Data& publicKey); diff --git a/src/interface/TWBitcoinScript.cpp b/src/interface/TWBitcoinScript.cpp index aa48ac4cdbe..711f34b365b 100644 --- a/src/interface/TWBitcoinScript.cpp +++ b/src/interface/TWBitcoinScript.cpp @@ -167,7 +167,7 @@ TWData *_Nullable TWBitcoinScriptBuildBRC20InscribeTransfer(TWString* ticker, TW auto* brcTicker = reinterpret_cast(ticker); auto* brcAmount = reinterpret_cast(amount); auto* brcPubkey = reinterpret_cast(pubkey); - auto script = TW::Bitcoin::Script::buildBRC20InscribeTransfer(*brcTicker, std::stoull(*brcAmount), *brcPubkey); + auto script = TW::Bitcoin::Script::buildBRC20InscribeTransfer(*brcTicker, *brcAmount, *brcPubkey); auto serialized = TW::data(script.SerializeAsString()); return TWDataCreateWithBytes(serialized.data(), serialized.size()); } diff --git a/src/proto/BitcoinV2.proto b/src/proto/BitcoinV2.proto index c55810557bf..b491b412615 100644 --- a/src/proto/BitcoinV2.proto +++ b/src/proto/BitcoinV2.proto @@ -16,6 +16,7 @@ enum Error { Error_utxo_missing_sighash_method = 7; Error_utxo_failed_encoding = 8; Error_utxo_insufficient_inputs = 9; + Error_utxo_no_outputs_specified = 43; Error_utxo_missing_change_script_pubkey = 10; // `tw_bitcoin` related errors. Error_zero_sequence_not_enabled = 11; @@ -191,7 +192,7 @@ message Input { // The ticker of the BRC20 inscription. string ticker = 3; // The BRC20 token transfer amount. - uint64 transfer_amount = 4; + string transfer_amount = 4; } } @@ -257,7 +258,7 @@ message Output { // The ticker of the BRC20 inscription. string ticker = 2; // The BRC20 token transfer amount. - uint64 transfer_amount = 3; + string transfer_amount = 3; } } diff --git a/src/proto/Utxo.proto b/src/proto/Utxo.proto index ada5c6bad9a..097d4e67a1f 100644 --- a/src/proto/Utxo.proto +++ b/src/proto/Utxo.proto @@ -13,7 +13,8 @@ enum Error { Error_missing_sighash_method = 6; Error_failed_encoding = 7; Error_insufficient_inputs = 8; - Error_missing_change_script_pubkey = 9; + Error_no_outputs_specified = 9; + Error_missing_change_script_pubkey = 10; } message SigningInput { @@ -45,14 +46,14 @@ message SigningInput { } enum InputSelector { - // Use all the inputs provided in the given order. - UseAll = 0; + // Automatically select enough inputs in an ascending order to cover the + // outputs of the transaction. + SelectAscending = 0; // Automatically select enough inputs in the given order to cover the // outputs of the transaction. SelectInOrder = 1; - // Automatically select enough inputs in an ascending order to cover the - // outputs of the transaction. - SelectAscending = 2; + // Use all the inputs provided in the given order. + UseAll = 10; } message LockTime { @@ -192,14 +193,17 @@ message TxInClaim { // The index of the referenced output. uint32 vout = 2; + // The value of this input, such as satoshis. + uint64 value = 3; + // The sequence number (TODO). - uint32 sequence = 3; + uint32 sequence = 4; // The script used for claiming an input. - bytes script_sig = 4; + bytes script_sig = 5; // The script used for claiming an input. - repeated bytes witness_items = 5; + repeated bytes witness_items = 6; } message SerializedTransaction { diff --git a/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp b/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp index 3c22eeae902..2306de4eb31 100644 --- a/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp +++ b/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp @@ -318,7 +318,7 @@ TEST(BitcoinSigning, SignBRC20TransferCommit) { auto pubKey = key.getPublicKey(TWPublicKeyTypeSECP256k1); auto utxoPubKeyHash = Hash::ripemd(Hash::sha256(pubKey.bytes)); auto inputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHash); - auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", 20, pubKey.bytes); + auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", "20", pubKey.bytes); Proto::SigningInput input; input.set_is_it_brc_operation(true); @@ -357,6 +357,56 @@ TEST(BitcoinSigning, SignBRC20TransferCommit) { // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 } +// Tests the BitcoinV2 API through the legacy `SigningInput`. +TEST(BitcoinSigning, SignBRC20TransferCommitV2) { + auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); + auto fullAmount = 26400; + auto minerFee = 3000; + auto brcInscribeAmount = 7000; + auto forFeeAmount = fullAmount - brcInscribeAmount - minerFee; + auto txId = parse_hex("089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e"); + + PrivateKey key(privateKey); + auto pubKey = key.getPublicKey(TWPublicKeyTypeSECP256k1); + + TW::BitcoinV2::Proto::SigningInput signing; + signing.set_version(2); + signing.set_private_key(key.bytes.data(), key.bytes.size()); + signing.set_input_selector(TW::Utxo::Proto::InputSelector::UseAll); + signing.set_disable_change_output(true); + + auto& in = *signing.add_inputs(); + in.set_txid(txId.data(), txId.size()); + in.set_vout(1); + in.set_value(fullAmount); + in.mutable_builder()->set_p2wpkh(pubKey.bytes.data(), pubKey.bytes.size()); + + auto& out = *signing.add_outputs(); + out.set_value(brcInscribeAmount); + auto& brc20 = *out.mutable_builder()->mutable_brc20_inscribe(); + brc20.set_ticker("oadf"); + brc20.set_transfer_amount("20"); + brc20.set_inscribe_to(pubKey.bytes.data(), pubKey.bytes.size()); + + auto& changeOut = *signing.add_outputs(); + changeOut.set_value(forFeeAmount); + changeOut.mutable_builder()->mutable_p2wpkh()->set_pubkey(pubKey.bytes.data(), pubKey.bytes.size()); + + Proto::SigningInput legacy; + *legacy.mutable_signing_v2() = signing; + + Proto::SigningOutput output; + ANY_SIGN(legacy, TWCoinTypeBitcoin); + + EXPECT_EQ(output.error(), Common::Proto::OK); + ASSERT_TRUE(output.has_signing_result_v2()); + EXPECT_EQ(output.signing_result_v2().error(), BitcoinV2::Proto::Error::OK); + EXPECT_EQ(hex(output.signing_result_v2().encoded()), "02000000000101089098890d2653567b9e8df2d1fbe5c3c8bf1910ca7184e301db0ad3b495c88e0100000000ffffffff02581b000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc051040000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100a44aa28446a9a886b378a4a65e32ad9a3108870bd725dc6105160bed4f317097022069e9de36422e4ce2e42b39884aa5f626f8f94194d1013007d5a1ea9220a06dce0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); + EXPECT_EQ(hex(output.signing_result_v2().txid()), "797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1"); + + // Successfully broadcasted: https://www.blockchain.com/explorer/transactions/btc/797d17d47ae66e598341f9dfdea020b04d4017dcf9cc33f0e51f7a6082171fb1 +} + TEST(BitcoinSigning, SignBRC20TransferReveal) { auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); auto dustSatoshi = 546; @@ -367,7 +417,7 @@ TEST(BitcoinSigning, SignBRC20TransferReveal) { auto pubKey = key.getPublicKey(TWPublicKeyTypeSECP256k1); auto utxoPubKeyHash = Hash::ripemd(Hash::sha256(pubKey.bytes)); auto inputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHash); - auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", 20, pubKey.bytes); + auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", "20", pubKey.bytes); Proto::SigningInput input; input.set_is_it_brc_operation(true); @@ -424,7 +474,7 @@ TEST(BitcoinSigning, SignBRC20TransferInscription) { auto utxoPubKeyHashBob = Hash::ripemd(Hash::sha256(parse_hex("02f453bb46e7afc8796a9629e89e07b5cb0867e9ca340b571e7bcc63fc20c43f2e"))); auto inputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHash); auto outputP2wpkh = TW::Bitcoin::Script::buildPayToWitnessPublicKeyHash(utxoPubKeyHashBob); - auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", 20, pubKey.bytes); + auto outputInscribe = TW::Bitcoin::Script::buildBRC20InscribeTransfer("oadf", "20", pubKey.bytes); Proto::SigningInput input; input.set_is_it_brc_operation(true); @@ -576,134 +626,6 @@ TEST(BitcoinSigning, SignNftInscriptionReveal) { ASSERT_EQ(result.substr(292, result.size() - 292), expectedHex.substr(292, result.size() - 292)); } -TEST(BitcoinSigning, PlanAndSignBrc20) { - auto privateKey = parse_hex("e253373989199da27c48680e3a3fc0f648d50f9a727ef17a7fe6a4dc3b159129"); - auto publicKey = parse_hex("030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb"); - - // Construct a `BitcoinV2.Proto.ComposePlan` message. - - auto dustSatoshis = 546; - auto ticker = "oadf"; - auto tokensAmount = 20; - auto feePerVb = 25; - - auto txId1 = parse_hex("181c84965c9ea86a5fac32fdbd5f73a21a7a9e749fb6ab97e273af2329f6b911"); - std::reverse(begin(txId1), end(txId1)); - - BitcoinV2::Proto::Input tx1; - tx1.set_txid(txId1.data(), (int)txId1.size()); - tx1.set_vout(0); - tx1.set_value(ONE_BTC); - tx1.set_sighash_type(Utxo::Proto::SighashType::All); - tx1.mutable_builder()->set_p2wpkh(publicKey.data(), (int)publicKey.size()); - - auto txId2 = parse_hex("858e450a1da44397bde05ca2f8a78510d74c623cc2f69736a8b3fbfadc161f6e"); - std::reverse(begin(txId2), end(txId2)); - - BitcoinV2::Proto::Input tx2; - tx2.set_txid(txId2.data(), (int)txId2.size()); - tx2.set_vout(0); - tx2.set_value(2 * ONE_BTC); - tx2.set_sighash_type(Utxo::Proto::SighashType::All); - tx2.mutable_builder()->set_p2wpkh(publicKey.data(), (int)publicKey.size()); - - BitcoinV2::Proto::Output taggedOutput; - taggedOutput.set_value(dustSatoshis); - taggedOutput.mutable_builder()->mutable_p2wpkh()->set_pubkey(publicKey.data(), (int)publicKey.size()); - - BitcoinV2::Proto::Output changeOutput; - // Will be set by the library. - changeOutput.set_value(0); - changeOutput.mutable_builder()->mutable_p2wpkh()->set_pubkey(publicKey.data(), (int)publicKey.size()); - - BitcoinV2::Proto::Input_InputBrc20Inscription brc20Inscription; - brc20Inscription.set_one_prevout(false); - brc20Inscription.set_inscribe_to(publicKey.data(), (int)publicKey.size()); - brc20Inscription.set_ticker(ticker); - brc20Inscription.set_transfer_amount(tokensAmount); - - BitcoinV2::Proto::ComposePlan composePlan; - auto& composeBrc20 = *composePlan.mutable_brc20(); - composeBrc20.set_private_key(privateKey.data(), (int)privateKey.size()); - *composeBrc20.add_inputs() = tx1; - *composeBrc20.add_inputs() = tx2; - composeBrc20.set_input_selector(Utxo::Proto::InputSelector::SelectAscending); - *composeBrc20.mutable_tagged_output() = taggedOutput; - *composeBrc20.mutable_inscription() = brc20Inscription; - composeBrc20.set_fee_per_vb(feePerVb); - *composeBrc20.mutable_change_output() = changeOutput; - composeBrc20.set_disable_change_output(false); - - // Construct a `Bitcoin.Proto.SigningInput` message with `planning_v2` field only. - Proto::SigningInput input; - *input.mutable_planning_v2() = composePlan; - - // Plan the transaction using standard `TWAnySignerPlan`. - Proto::TransactionPlan plan; - ANY_PLAN(input, plan, TWCoinTypeBitcoin); - - // Check the result Planning V2. - EXPECT_TRUE(plan.has_planning_result_v2()); - const auto& planV2 = plan.planning_result_v2(); - EXPECT_EQ(planV2.error(), BitcoinV2::Proto::Error::OK); - EXPECT_TRUE(planV2.has_brc20()); - const auto& brc20Plan = planV2.brc20(); - - // Check the result Commit `SigningInput`. - auto commitOutputAmount = 3846; - EXPECT_TRUE(brc20Plan.has_commit()); - const auto& brc20Commit = brc20Plan.commit(); - EXPECT_EQ(brc20Commit.version(), 2); - EXPECT_EQ(brc20Commit.inputs_size(), 1); - EXPECT_EQ(brc20Commit.outputs_size(), 2); - // Change output generation is disabled, included in `commit.outputs`. - EXPECT_FALSE(brc20Commit.has_change_output()); - EXPECT_EQ(brc20Commit.outputs(0).value(), commitOutputAmount); - EXPECT_EQ(brc20Commit.outputs(0).builder().brc20_inscribe().ticker(), ticker); - EXPECT_EQ(brc20Commit.outputs(0).builder().brc20_inscribe().transfer_amount(), tokensAmount); - // Change: tx1 value - out1 value - EXPECT_EQ(brc20Commit.outputs(1).value(), ONE_BTC - commitOutputAmount - 3175); - - // Check the result Reveal `SigningInput`. - EXPECT_TRUE(brc20Plan.has_reveal()); - const auto& brc20Reveal = brc20Plan.reveal(); - EXPECT_EQ(brc20Reveal.version(), 2); - EXPECT_EQ(brc20Reveal.inputs_size(), 1); - EXPECT_EQ(brc20Reveal.outputs_size(), 1); - // Change output generation is disabled, included in `commit.outputs`. - EXPECT_FALSE(brc20Reveal.has_change_output()); - EXPECT_EQ(brc20Reveal.inputs(0).value(), commitOutputAmount); - EXPECT_EQ(brc20Reveal.inputs(0).builder().brc20_inscribe().ticker(), ticker); - EXPECT_EQ(brc20Reveal.inputs(0).builder().brc20_inscribe().transfer_amount(), tokensAmount); - EXPECT_EQ(brc20Reveal.outputs(0).value(), dustSatoshis); - - // Construct a `Bitcoin.Proto.SigningInput` message with `signing_v2` (Commit) field only. - { - Proto::SigningInput commitInput; - *commitInput.mutable_signing_v2() = brc20Commit; - - Proto::SigningOutput output; - ANY_SIGN(commitInput, TWCoinTypeBitcoin); - EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); - EXPECT_TRUE(output.has_signing_result_v2()); - EXPECT_EQ(hex(output.signing_result_v2().encoded()), "0200000000010111b9f62923af73e297abb69f749e7a1aa2735fbdfd32ac5f6aa89e5c96841c180000000000ffffffff02060f000000000000225120e8b706a97732e705e22ae7710703e7f589ed13c636324461afa443016134cc0593c5f50500000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d02483045022100912004efb9b4e8368ba00d6bfbdfde22a43b037f64ae09d79aac030c77edbc2802206c5702646eadea2274c4aafee99c12b5054cb60da18c21f67f5f3003a318112d0121030f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); - } - - // Construct a `Bitcoin.Proto.SigningInput` message with `signing_v2` (Reveal) field only. - { - Proto::SigningInput revealInput; - *revealInput.mutable_signing_v2() = brc20Reveal; - // `schnorr` is used to sign the Reveal transaction. - revealInput.mutable_signing_v2()->set_dangerous_use_fixed_schnorr_rng(true); - - Proto::SigningOutput output; - ANY_SIGN(revealInput, TWCoinTypeBitcoin); - EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); - EXPECT_TRUE(output.has_signing_result_v2()); - EXPECT_EQ(hex(output.signing_result_v2().encoded()), "0200000000010173711b50d9adb30fdc51231cd56a95b3b627453add775c56188449f2dccaef250000000000ffffffff012202000000000000160014e311b8d6ddff856ce8e9a4e03bc6d4fe5050a83d03405cd7fb811a8ebcc55ac791321243a6d1a4089abc548d93288dfe5870f6af7f96b0cb0c7c41c0126791179e8c190d8fecf9bdc4cc740ec3e7d6a43b1b0a345f155b0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800377b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a226f616466222c22616d74223a223230227d6821c00f209b6ada5edb42c77fd2bc64ad650ae38314c8f451f3e36d80bc8e26f132cb00000000"); - } -} - TEST(BitcoinSigning, SignP2PKH) { auto input = buildInputP2PKH();