From 24f24c118506881a1e3bfdc3655013db26fe38ac Mon Sep 17 00:00:00 2001 From: Fabio Lama <42901763+lamafab@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:05:48 +0100 Subject: [PATCH] [BitcoinV2] Implement InputSelector::SelectDescending (#3672) --- rust/tw_bitcoin/tests/input_selection.rs | 127 +++++++++++++++++++++++ rust/tw_utxo/src/compiler.rs | 9 +- src/proto/Utxo.proto | 3 + 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/rust/tw_bitcoin/tests/input_selection.rs b/rust/tw_bitcoin/tests/input_selection.rs index 38afea369e5..3105370c378 100644 --- a/rust/tw_bitcoin/tests/input_selection.rs +++ b/rust/tw_bitcoin/tests/input_selection.rs @@ -502,6 +502,133 @@ fn input_selection_select_ascending() { assert!(tx.outputs[1].control_block.is_empty()); } +#[test] +fn input_selection_select_descending() { + 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 txid: Vec = vec![2; 32]; + let tx2 = 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![3; 32]; + let tx3 = 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 out1 = Proto::Output { + value: ONE_BTC * 4, + 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(), + // Select descending. + input_selector: UtxoProto::InputSelector::SelectDescending, + 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 (descending; second > third). + assert_eq!(tx.inputs.len(), 2); + + assert_eq!(tx.inputs[0].txid, tx2.txid); + assert_eq!(tx.inputs[0].txid, vec![2; 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, tx3.txid); + assert_eq!(tx.inputs[1].txid, vec![3; 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 * 4); + 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, + tx2.value + tx3.value - out1.value - signed.fee + ); + assert_eq!( + tx.outputs[1].value, + (ONE_BTC * 3) + (ONE_BTC * 2) - (ONE_BTC * 4) - 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(); diff --git a/rust/tw_utxo/src/compiler.rs b/rust/tw_utxo/src/compiler.rs index 07a98712be1..822cd8a3542 100644 --- a/rust/tw_utxo/src/compiler.rs +++ b/rust/tw_utxo/src/compiler.rs @@ -80,10 +80,11 @@ impl Compiler { )); } - // If the input selector is InputSelector::SelectAscending, we sort the - // input first. + // If enabled, sort the order of the UTXOs. if let Proto::InputSelector::SelectAscending = proto.input_selector { proto.inputs.sort_by(|a, b| a.value.cmp(&b.value)); + } else if let Proto::InputSelector::SelectDescending = proto.input_selector { + proto.inputs.sort_by(|a, b| b.value.cmp(&a.value)); } // Add change output generation is enabled, push it to the proto structure. @@ -128,7 +129,9 @@ impl Compiler { proto.inputs.push(txin); } }, - Proto::InputSelector::SelectInOrder | Proto::InputSelector::SelectAscending => { + Proto::InputSelector::SelectInOrder + | Proto::InputSelector::SelectAscending + | Proto::InputSelector::SelectDescending => { let mut total_input_amount = 0; let mut total_input_weight = 0; diff --git a/src/proto/Utxo.proto b/src/proto/Utxo.proto index 097d4e67a1f..b5b2bbe40e8 100644 --- a/src/proto/Utxo.proto +++ b/src/proto/Utxo.proto @@ -52,6 +52,9 @@ enum InputSelector { // Automatically select enough inputs in the given order to cover the // outputs of the transaction. SelectInOrder = 1; + // Automatically select enough inputs in an descending order to cover the + // outputs of the transaction. + SelectDescending = 2; // Use all the inputs provided in the given order. UseAll = 10; }