Skip to content

Commit

Permalink
[BitcoinV2] Implement InputSelector::SelectDescending (#3672)
Browse files Browse the repository at this point in the history
  • Loading branch information
lamafab authored Jan 25, 2024
1 parent ca1a2d2 commit 24f24c1
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 3 deletions.
127 changes: 127 additions & 0 deletions rust/tw_bitcoin/tests/input_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> = 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<u8> = 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<u8> = 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();
Expand Down
9 changes: 6 additions & 3 deletions rust/tw_utxo/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ impl Compiler<StandardBitcoinContext> {
));
}

// 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.
Expand Down Expand Up @@ -128,7 +129,9 @@ impl Compiler<StandardBitcoinContext> {
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;

Expand Down
3 changes: 3 additions & 0 deletions src/proto/Utxo.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 24f24c1

Please sign in to comment.