Skip to content

Commit 7b3d4eb

Browse files
committed
Ensure that a change output is generated
1 parent af8d33f commit 7b3d4eb

File tree

10 files changed

+338
-142
lines changed

10 files changed

+338
-142
lines changed

bitcoin-rpc-provider/src/lib.rs

+12-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use bitcoin::secp256k1::{PublicKey, SecretKey};
1414
use bitcoin::{
1515
consensus::Decodable, network::constants::Network, Amount, PrivateKey, Transaction, Txid,
1616
};
17-
use bitcoin::{Address, OutPoint, ScriptBuf, TxOut};
17+
use bitcoin::{Address, OutPoint, Script, ScriptBuf, TxOut};
1818
use bitcoincore_rpc::jsonrpc::serde_json;
1919
use bitcoincore_rpc::jsonrpc::serde_json::Value;
2020
use bitcoincore_rpc::{json, Auth, Client, RpcApi};
@@ -285,6 +285,7 @@ impl Wallet for BitcoinCoreProvider {
285285
amount: u64,
286286
_fee_rate: u64,
287287
lock_utxos: bool,
288+
_change_script: &Script,
288289
) -> Result<Vec<Utxo>, ManagerError> {
289290
let client = self.client.lock().unwrap();
290291
let utxo_res = client
@@ -391,11 +392,18 @@ impl Wallet for BitcoinCoreProvider {
391392
}
392393

393394
fn unreserve_utxos(&self, outpoints: &[OutPoint]) -> Result<(), ManagerError> {
394-
match self.client.lock().unwrap().unlock_unspent(outpoints).map_err(rpc_err_to_manager_err)? {
395+
match self
396+
.client
397+
.lock()
398+
.unwrap()
399+
.unlock_unspent(outpoints)
400+
.map_err(rpc_err_to_manager_err)?
401+
{
395402
true => Ok(()),
396-
false => Err(ManagerError::StorageError(format!("Failed to unlock utxos: {outpoints:?}")))
403+
false => Err(ManagerError::StorageError(format!(
404+
"Failed to unlock utxos: {outpoints:?}"
405+
))),
397406
}
398-
399407
}
400408
}
401409

dlc-manager/src/contract_updater.rs

+33-19
Original file line numberDiff line numberDiff line change
@@ -756,38 +756,52 @@ where
756756

757757
#[cfg(test)]
758758
mod tests {
759-
use std::rc::Rc;
760-
761-
use mocks::dlc_manager::contract::offered_contract::OfferedContract;
759+
use dlc_messages::OfferDlc;
760+
use mocks::{
761+
dlc_manager::contract::offered_contract::OfferedContract, mock_wallet::MockWallet,
762+
};
762763
use secp256k1_zkp::PublicKey;
763764

764-
#[test]
765-
fn accept_contract_test() {
766-
let offer_dlc =
767-
serde_json::from_str(include_str!("../test_inputs/offer_contract.json")).unwrap();
765+
fn fee_computation_test_common(offer_dlc: OfferDlc, utxo_values: &[u64]) -> MockWallet {
768766
let dummy_pubkey: PublicKey =
769767
"02e6642fd69bd211f93f7f1f36ca51a26a5290eb2dd1b0d8279a87bb0d480c8443"
770768
.parse()
771769
.unwrap();
772770
let offered_contract =
773771
OfferedContract::try_from_offer_dlc(&offer_dlc, dummy_pubkey, [0; 32]).unwrap();
774-
let blockchain = Rc::new(mocks::mock_blockchain::MockBlockchain::new());
775-
let fee_rate: u64 = offered_contract.fee_rate_per_vb;
776-
let utxo_value: u64 = offered_contract.total_collateral
777-
- offered_contract.offer_params.collateral
778-
+ crate::utils::get_half_common_fee(fee_rate).unwrap();
779-
let wallet = Rc::new(mocks::mock_wallet::MockWallet::new(
780-
&blockchain,
781-
&[utxo_value, 10000],
782-
));
772+
let blockchain = mocks::mock_blockchain::MockBlockchain::new();
773+
let wallet = MockWallet::new(&blockchain, utxo_values);
783774

784775
mocks::dlc_manager::contract_updater::accept_contract(
785776
secp256k1_zkp::SECP256K1,
786777
&offered_contract,
787-
&wallet,
788-
&wallet,
789-
&blockchain,
778+
&&wallet,
779+
&&wallet,
780+
&&blockchain,
790781
)
791782
.expect("Not to fail");
783+
wallet
784+
}
785+
786+
#[test]
787+
fn with_exact_value_utxo_doesnt_fail() {
788+
let offer_dlc: OfferDlc =
789+
serde_json::from_str(include_str!("../test_inputs/offer_contract.json")).unwrap();
790+
let fee_rate: u64 = offer_dlc.fee_rate_per_vb;
791+
let utxo_value: u64 = offer_dlc.contract_info.get_total_collateral()
792+
- offer_dlc.offer_collateral
793+
+ crate::utils::get_half_common_fee(fee_rate).unwrap();
794+
fee_computation_test_common(offer_dlc, &[utxo_value, 10000]);
795+
}
796+
797+
#[test]
798+
fn with_no_change_utxo_enforce_change_output() {
799+
let offer_dlc: OfferDlc =
800+
serde_json::from_str(include_str!("../test_inputs/offer_contract2.json")).unwrap();
801+
let wallet = fee_computation_test_common(offer_dlc, &[136015, 40000]);
802+
let utxos = wallet.utxos.lock().unwrap();
803+
for utxo in utxos.iter() {
804+
assert!(utxo.reserved);
805+
}
792806
}
793807
}

dlc-manager/src/lib.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub mod payout_curve;
3737
mod utils;
3838

3939
use bitcoin::psbt::PartiallySignedTransaction;
40-
use bitcoin::{Address, Block, OutPoint, ScriptBuf, Transaction, TxOut, Txid};
40+
use bitcoin::{Address, Block, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
4141
use chain_monitor::ChainMonitor;
4242
use channel::offered_channel::OfferedChannel;
4343
use channel::signed_channel::{SignedChannel, SignedChannelStateType};
@@ -152,12 +152,18 @@ pub trait Wallet {
152152
fn get_new_address(&self) -> Result<Address, Error>;
153153
/// Returns a new (unused) change address.
154154
fn get_new_change_address(&self) -> Result<Address, Error>;
155-
/// Get a set of UTXOs to fund the given amount.
155+
/// Get a set of UTXOs to fund the given amount. The implementation is expected to take into
156+
/// account the cost of the inputs that are selected. For the protocol to be secure, it is
157+
/// required that each party has a change output on the funding transaction to be able to bump
158+
/// the fee in case of network congestion. If the total value of the returned UTXO is to small
159+
/// to have a change output, the library will call the method again with a higer `amount` to
160+
/// ensure having a change output.
156161
fn get_utxos_for_amount(
157162
&self,
158163
amount: u64,
159164
fee_rate: u64,
160165
lock_utxos: bool,
166+
change_script: &Script,
161167
) -> Result<Vec<Utxo>, Error>;
162168
/// Import the provided address.
163169
fn import_address(&self, address: &Address) -> Result<(), Error>;

dlc-manager/src/utils.rs

+28-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
use std::ops::Deref;
33

44
use bitcoin::{consensus::Encodable, Txid};
5-
use dlc::{PartyParams, TxInputInfo};
5+
use dlc::{
6+
util::{get_inputs_and_change_weight, weight_to_fee},
7+
PartyParams, TxInputInfo,
8+
};
69
use dlc_messages::{
710
oracle_msgs::{OracleAnnouncement, OracleAttestation},
811
FundingInput,
@@ -83,7 +86,30 @@ where
8386
// Add base cost of fund tx + CET / 2 and a CET output to the collateral.
8487
let appr_required_amount =
8588
own_collateral + get_half_common_fee(fee_rate)? + dlc::util::weight_to_fee(124, fee_rate)?;
86-
let utxos = wallet.get_utxos_for_amount(appr_required_amount, fee_rate, true)?;
89+
let mut utxos =
90+
wallet.get_utxos_for_amount(appr_required_amount, fee_rate, true, &change_spk)?;
91+
let total_value: u64 = utxos.iter().map(|x| x.tx_out.value).sum();
92+
let min_change_value = change_addr.script_pubkey().dust_value().to_sat();
93+
let (inputs_weight, change_weight) = get_inputs_and_change_weight(
94+
&utxos
95+
.iter()
96+
.map(|x| (x.tx_out.script_pubkey.as_ref(), 107))
97+
.collect::<Vec<_>>(),
98+
&change_spk,
99+
)?;
100+
let inputs_fee = weight_to_fee(inputs_weight, fee_rate)?;
101+
let change_fee = weight_to_fee(change_weight, fee_rate)?;
102+
// We need to have a change output, if we didn't on first try, we request an amount which
103+
// includes minimum value for the change output as well as the fee for it.
104+
if total_value < appr_required_amount + min_change_value + inputs_fee + change_fee {
105+
wallet.unreserve_utxos(&utxos.iter().map(|x| x.outpoint).collect::<Vec<_>>())?;
106+
utxos = wallet.get_utxos_for_amount(
107+
appr_required_amount + min_change_value + change_fee,
108+
fee_rate,
109+
true,
110+
&change_spk,
111+
)?;
112+
}
87113

88114
let mut funding_inputs: Vec<FundingInput> = Vec::new();
89115
let mut funding_tx_info: Vec<TxInputInfo> = Vec::new();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
{
2+
"protocolVersion":1,
3+
"contractFlags":0,
4+
"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
5+
"temporaryContractId":"f6a1b2841c93db06e94200b227bb4bdea83068efa557d68e14775237cbaab56a",
6+
"contractInfo":{
7+
"singleContractInfo":{
8+
"totalCollateral":120000,
9+
"contractInfo":{
10+
"contractDescriptor":{
11+
"numericOutcomeContractDescriptor":{
12+
"numDigits":10,
13+
"payoutFunction":{
14+
"payoutFunctionPieces":[
15+
{
16+
"endPoint":{
17+
"eventOutcome":0,
18+
"outcomePayout":0,
19+
"extraPrecision":0
20+
},
21+
"payoutCurvePiece":{
22+
"polynomialPayoutCurvePiece":{
23+
"payoutPoints":[
24+
{
25+
"eventOutcome":3,
26+
"outcomePayout":70000,
27+
"extraPrecision":0
28+
}
29+
]
30+
}
31+
}
32+
},
33+
{
34+
"endPoint":{
35+
"eventOutcome":5,
36+
"outcomePayout":120000,
37+
"extraPrecision":0
38+
},
39+
"payoutCurvePiece":{
40+
"polynomialPayoutCurvePiece":{
41+
"payoutPoints":[
42+
43+
]
44+
}
45+
}
46+
}
47+
],
48+
"lastEndpoint":{
49+
"eventOutcome":1023,
50+
"outcomePayout":120000,
51+
"extraPrecision":0
52+
}
53+
},
54+
"roundingIntervals":{
55+
"intervals":[
56+
{
57+
"beginInterval":0,
58+
"roundingMod":1
59+
}
60+
]
61+
}
62+
}
63+
},
64+
"oracleInfo":{
65+
"single":{
66+
"oracleAnnouncement":{
67+
"announcementSignature":"18e18de8b3547e210addd32589db9520286f55c0c18510c67bb6f8ea66b05154b84c6ec0075e3623f886b7e2bc623b7df25e1bc25d1cc87c622b28f0ae526664",
68+
"oraclePublicKey":"1d524d2753a36ebe340af67370f78219b4dbb6f56d2f96b3b21eaabec6f4a114",
69+
"oracleEvent":{
70+
"oracleNonces":[
71+
"bc927a2c8bf43c9d208e679848ffaf95d178fdbd2e29d1c66668f21dd75149e8",
72+
"9ed74e19c1d532f5127829b7d9f183e0738ad084485428b53a7fe0c50f2efe5e",
73+
"f44733d1129d0cd9253124749f8cff2c7e7eecd79888a4a015d3e3ad153ef282",
74+
"f4f39e5733bfc5ca18530eb444419b31d9dc0ec938502615c33f2b0b7c05ac71",
75+
"930991374fbf6b9a49e5e16fa3c5c39638af58f5a4c55682a93b2b940502e7bf",
76+
"e3af3b59907c349d627e3f4f20125bdc1e979cac41ee82ef0a184000c79e904b",
77+
"0b95d4335713752329a1791b963d526c0a49873bbbfcad9e1c03881508b2a801",
78+
"48776cc1e3b8f3ff7fd6226ea2df5607787913468a1c0faad4ff315b7cf3b41d",
79+
"0b39b0e1a14f5f50cb05f0a6d8e7c082f75e9fe386006727af933ce4d273a76f",
80+
"479a38e13c1622bfd53299ee67680d7a0edd3fed92223e3a878c8d010fcc1a2d"
81+
],
82+
"eventMaturityEpoch":1623133104,
83+
"eventDescriptor":{
84+
"digitDecompositionEvent":{
85+
"base":2,
86+
"isSigned":false,
87+
"unit":"sats/sec",
88+
"precision":0,
89+
"nbDigits":10
90+
}
91+
},
92+
"eventId":"Test"
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
},
100+
"fundingPubkey":"02556021f6abda2ae7a74d38a4e4a3b00c1dd648db96397dcd8642c3d0b0b139d1",
101+
"payoutSpk":"0014430af74f2f9dc88729fd02eaeb946fc161e2be1e",
102+
"payoutSerialId":8165863461276958928,
103+
"offerCollateral":60000,
104+
"fundingInputs":[
105+
{
106+
"inputSerialId":11632658032743242199,
107+
"prevTx":"02000000000101e79f7a30bb35206060eb09a99b6956bcdc7a1767b310c8dfde3595c69246a60e0000000000feffffff0200c2eb0b000000001600142a416c1e5f5e78bc6c518294fd1dd86b40eed2d77caf953e000000001600148e56705661334df89b2c1c7c4e41da9cef9eb38e0247304402201491f05ebe196b333420cbab3e7e7f3e431bfe91a42730cef9c6e64b0e8ff62302202c5fc79abbdb0a1c8ad422dbb97a54693feedc580f0cb7a62bdadaecbfc4f9430121035f57172a38f35f29f4357dcc2d24ea8e72638cf43190e4fdcb3f0ace215cfd5602020000",
108+
"prevTxVout":0,
109+
"sequence":4294967295,
110+
"maxWitnessLen":107,
111+
"redeemScript":""
112+
}
113+
],
114+
"changeSpk":"001441ca183be469eab996f34ed31197a96b57f6050e",
115+
"changeSerialId":16919534260907952016,
116+
"fundOutputSerialId":5054305248376932341,
117+
"feeRatePerVb":400,
118+
"cetLocktime":1623133103,
119+
"refundLocktime":1623737904
120+
}

dlc/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[package]
22
authors = ["Crypto Garage"]
33
description = "Creation, signing and verification of Discreet Log Contracts (DLC) transactions."
4+
edition = "2018"
45
homepage = "https://github.com/p2pderivatives/rust-dlc"
56
license-file = "../LICENSE"
67
name = "dlc"

dlc/src/lib.rs

+12-19
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ macro_rules! checked_add {
8080
};
8181
}
8282

83+
pub(crate) use checked_add;
84+
85+
use crate::util::get_inputs_and_change_weight;
86+
8387
/// Represents the payouts for a unique contract outcome. Offer party represents
8488
/// the initiator of the contract while accept party represents the party
8589
/// accepting the contract.
@@ -282,25 +286,14 @@ impl PartyParams {
282286
fee_rate_per_vb: u64,
283287
extra_fee: u64,
284288
) -> Result<(TxOut, u64, u64), Error> {
285-
let mut inputs_weight: usize = 0;
286-
287-
for w in &self.inputs {
288-
let script_weight = util::redeem_script_to_script_sig(&w.redeem_script)
289-
.len()
290-
.checked_mul(4)
291-
.ok_or(Error::InvalidArgument)?;
292-
inputs_weight = checked_add!(
293-
inputs_weight,
294-
TX_INPUT_BASE_WEIGHT,
295-
script_weight,
296-
w.max_witness_len
297-
)?;
298-
}
299-
300-
// Value size + script length var_int + ouput script pubkey size
301-
let change_size = self.change_script_pubkey.len();
302-
// Change size is scaled by 4 from vBytes to weight units
303-
let change_weight = change_size.checked_mul(4).ok_or(Error::InvalidArgument)?;
289+
let (inputs_weight, change_weight) = get_inputs_and_change_weight(
290+
&self
291+
.inputs
292+
.iter()
293+
.map(|x| (x.redeem_script.as_ref(), x.max_witness_len))
294+
.collect::<Vec<_>>(),
295+
&self.change_script_pubkey,
296+
)?;
304297

305298
// Base weight (nLocktime, nVersion, ...) is distributed among parties
306299
// independently of inputs contributed

dlc/src/util.rs

+29-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use bitcoin::{
99
use bitcoin::{ScriptBuf, Sequence, Witness};
1010
use secp256k1_zkp::{ecdsa::Signature, Message, PublicKey, Secp256k1, SecretKey, Signing};
1111

12-
use crate::Error;
12+
use crate::{checked_add, Error};
1313

1414
// Setting the nSequence for every input of a transaction to this value disables
1515
// both RBF and nLockTime usage.
@@ -256,3 +256,31 @@ pub fn validate_fee_rate(fee_rate_per_vb: u64) -> Result<(), Error> {
256256

257257
Ok(())
258258
}
259+
260+
/// Computes the total weight of the given inputs and change.
261+
pub fn get_inputs_and_change_weight(
262+
inputs: &[(&Script, usize)],
263+
change_spk: &Script,
264+
) -> Result<(usize, usize), Error> {
265+
let mut inputs_weight: usize = 0;
266+
267+
for w in inputs {
268+
let script_weight = redeem_script_to_script_sig(w.0)
269+
.len()
270+
.checked_mul(4)
271+
.ok_or(Error::InvalidArgument)?;
272+
inputs_weight = checked_add!(
273+
inputs_weight,
274+
crate::TX_INPUT_BASE_WEIGHT,
275+
script_weight,
276+
w.1
277+
)?;
278+
}
279+
280+
// Value size + script length var_int + ouput script pubkey size
281+
let change_size = change_spk.len();
282+
// Change size is scaled by 4 from vBytes to weight units
283+
let change_weight = change_size.checked_mul(4).ok_or(Error::InvalidArgument)?;
284+
285+
Ok((inputs_weight, change_weight))
286+
}

0 commit comments

Comments
 (0)