Skip to content

Commit

Permalink
Ensure that a change output is generated
Browse files Browse the repository at this point in the history
  • Loading branch information
Tibo-lg committed Mar 28, 2024
1 parent a332060 commit 97af684
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 144 deletions.
16 changes: 12 additions & 4 deletions bitcoin-rpc-provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use bitcoin::secp256k1::{PublicKey, SecretKey};
use bitcoin::{
consensus::Decodable, network::constants::Network, Amount, PrivateKey, Transaction, Txid,
};
use bitcoin::{Address, OutPoint, ScriptBuf, TxOut};
use bitcoin::{Address, OutPoint, Script, ScriptBuf, TxOut};
use bitcoincore_rpc::jsonrpc::serde_json;
use bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoincore_rpc::{json, Auth, Client, RpcApi};
Expand Down Expand Up @@ -285,6 +285,7 @@ impl Wallet for BitcoinCoreProvider {
amount: u64,
_fee_rate: u64,
lock_utxos: bool,
_change_script: &Script,
) -> Result<Vec<Utxo>, ManagerError> {
let client = self.client.lock().unwrap();
let utxo_res = client
Expand Down Expand Up @@ -391,11 +392,18 @@ impl Wallet for BitcoinCoreProvider {
}

fn unreserve_utxos(&self, outpoints: &[OutPoint]) -> Result<(), ManagerError> {
match self.client.lock().unwrap().unlock_unspent(outpoints).map_err(rpc_err_to_manager_err)? {
match self
.client
.lock()
.unwrap()
.unlock_unspent(outpoints)
.map_err(rpc_err_to_manager_err)?
{
true => Ok(()),
false => Err(ManagerError::StorageError(format!("Failed to unlock utxos: {outpoints:?}")))
false => Err(ManagerError::StorageError(format!(
"Failed to unlock utxos: {outpoints:?}"
))),
}

}
}

Expand Down
52 changes: 33 additions & 19 deletions dlc-manager/src/contract_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,38 +756,52 @@ where

#[cfg(test)]
mod tests {
use std::rc::Rc;

use mocks::dlc_manager::contract::offered_contract::OfferedContract;
use dlc_messages::OfferDlc;
use mocks::{
dlc_manager::contract::offered_contract::OfferedContract, mock_wallet::MockWallet,
};
use secp256k1_zkp::PublicKey;

#[test]
fn accept_contract_test() {
let offer_dlc =
serde_json::from_str(include_str!("../test_inputs/offer_contract.json")).unwrap();
fn fee_computation_test_common(offer_dlc: OfferDlc, utxo_values: &[u64]) -> MockWallet {
let dummy_pubkey: PublicKey =
"02e6642fd69bd211f93f7f1f36ca51a26a5290eb2dd1b0d8279a87bb0d480c8443"
.parse()
.unwrap();
let offered_contract =
OfferedContract::try_from_offer_dlc(&offer_dlc, dummy_pubkey, [0; 32]).unwrap();
let blockchain = Rc::new(mocks::mock_blockchain::MockBlockchain::new());
let fee_rate: u64 = offered_contract.fee_rate_per_vb;
let utxo_value: u64 = offered_contract.total_collateral
- offered_contract.offer_params.collateral
+ crate::utils::get_half_common_fee(fee_rate).unwrap();
let wallet = Rc::new(mocks::mock_wallet::MockWallet::new(
&blockchain,
&[utxo_value, 10000],
));
let blockchain = mocks::mock_blockchain::MockBlockchain::new();
let wallet = MockWallet::new(&blockchain, utxo_values);

mocks::dlc_manager::contract_updater::accept_contract(
secp256k1_zkp::SECP256K1,
&offered_contract,
&wallet,
&wallet,
&blockchain,
&&wallet,
&&wallet,
&&blockchain,
)
.expect("Not to fail");
wallet
}

#[test]
fn with_exact_value_utxo_doesnt_fail() {
let offer_dlc: OfferDlc =
serde_json::from_str(include_str!("../test_inputs/offer_contract.json")).unwrap();
let fee_rate: u64 = offer_dlc.fee_rate_per_vb;
let utxo_value: u64 = offer_dlc.contract_info.get_total_collateral()
- offer_dlc.offer_collateral
+ crate::utils::get_half_common_fee(fee_rate).unwrap();
fee_computation_test_common(offer_dlc, &[utxo_value, 10000]);
}

#[test]
fn with_no_change_utxo_enforce_change_output() {
let offer_dlc: OfferDlc =
serde_json::from_str(include_str!("../test_inputs/offer_contract2.json")).unwrap();
let wallet = fee_computation_test_common(offer_dlc, &[136015, 40000]);
let utxos = wallet.utxos.lock().unwrap();
for utxo in utxos.iter() {
assert!(utxo.reserved);
}
}
}
8 changes: 6 additions & 2 deletions dlc-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub mod payout_curve;
mod utils;

use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::{Address, Block, OutPoint, ScriptBuf, Transaction, TxOut, Txid};
use bitcoin::{Address, Block, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
use chain_monitor::ChainMonitor;
use channel::offered_channel::OfferedChannel;
use channel::signed_channel::{SignedChannel, SignedChannelStateType};
Expand Down Expand Up @@ -152,12 +152,16 @@ pub trait Wallet {
fn get_new_address(&self) -> Result<Address, Error>;
/// Returns a new (unused) change address.
fn get_new_change_address(&self) -> Result<Address, Error>;
/// Get a set of UTXOs to fund the given amount.
/// Get a set of UTXOs to fund the given amount. The implementation is expected to take into
/// account the cost of the inputs that are selected. For the protocol to be secure, it is
/// required that each party has a change output on the funding transaction to be able to bump
/// the fee in case of network congestion.
fn get_utxos_for_amount(
&self,
amount: u64,
fee_rate: u64,
lock_utxos: bool,
change_script: &Script,
) -> Result<Vec<Utxo>, Error>;
/// Import the provided address.
fn import_address(&self, address: &Address) -> Result<(), Error>;
Expand Down
35 changes: 30 additions & 5 deletions dlc-manager/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
use std::ops::Deref;

use bitcoin::{consensus::Encodable, Txid};
use dlc::{PartyParams, TxInputInfo};
use dlc::{
util::{get_change_weight, get_inputs_weight, weight_to_fee},
PartyParams, TxInputInfo,
};
use dlc_messages::{
oracle_msgs::{OracleAnnouncement, OracleAttestation},
FundingInput,
Expand Down Expand Up @@ -80,10 +83,32 @@ where
let change_spk = change_addr.script_pubkey();
let change_serial_id = get_new_serial_id();

// Add base cost of fund tx + CET / 2 and a CET output to the collateral.
let appr_required_amount =
own_collateral + get_half_common_fee(fee_rate)? + dlc::util::weight_to_fee(124, fee_rate)?;
let utxos = wallet.get_utxos_for_amount(appr_required_amount, fee_rate, true)?;
// Add base cost of fund tx + CET / 2 and a CET output to the collateral + minimum amount to
// have a change output.
let min_change_value = change_addr.script_pubkey().dust_value().to_sat();
let change_weight = get_change_weight(&change_spk)?;
let change_fee = weight_to_fee(change_weight, fee_rate)?;
let appr_required_amount = own_collateral
+ get_half_common_fee(fee_rate)?
+ dlc::util::weight_to_fee(124, fee_rate)?
+ min_change_value
+ change_fee;
let utxos = wallet.get_utxos_for_amount(appr_required_amount, fee_rate, true, &change_spk)?;
let total_value: u64 = utxos.iter().map(|x| x.tx_out.value).sum();
let inputs_weight = get_inputs_weight(
&utxos
.iter()
.map(|x| (x.tx_out.script_pubkey.as_ref(), 107))
.collect::<Vec<_>>(),
)?;
let inputs_fee = weight_to_fee(inputs_weight, fee_rate)?;

if total_value < appr_required_amount + inputs_fee {
return Err(Error::InvalidParameters(format!(
"Coin selection didn't return a high enough value. Requested: {appr_required_amount} Got: {total_value}."
)
));
}

let mut funding_inputs: Vec<FundingInput> = Vec::new();
let mut funding_tx_info: Vec<TxInputInfo> = Vec::new();
Expand Down
120 changes: 120 additions & 0 deletions dlc-manager/test_inputs/offer_contract2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{
"protocolVersion":1,
"contractFlags":0,
"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
"temporaryContractId":"f6a1b2841c93db06e94200b227bb4bdea83068efa557d68e14775237cbaab56a",
"contractInfo":{
"singleContractInfo":{
"totalCollateral":120000,
"contractInfo":{
"contractDescriptor":{
"numericOutcomeContractDescriptor":{
"numDigits":10,
"payoutFunction":{
"payoutFunctionPieces":[
{
"endPoint":{
"eventOutcome":0,
"outcomePayout":0,
"extraPrecision":0
},
"payoutCurvePiece":{
"polynomialPayoutCurvePiece":{
"payoutPoints":[
{
"eventOutcome":3,
"outcomePayout":70000,
"extraPrecision":0
}
]
}
}
},
{
"endPoint":{
"eventOutcome":5,
"outcomePayout":120000,
"extraPrecision":0
},
"payoutCurvePiece":{
"polynomialPayoutCurvePiece":{
"payoutPoints":[

]
}
}
}
],
"lastEndpoint":{
"eventOutcome":1023,
"outcomePayout":120000,
"extraPrecision":0
}
},
"roundingIntervals":{
"intervals":[
{
"beginInterval":0,
"roundingMod":1
}
]
}
}
},
"oracleInfo":{
"single":{
"oracleAnnouncement":{
"announcementSignature":"18e18de8b3547e210addd32589db9520286f55c0c18510c67bb6f8ea66b05154b84c6ec0075e3623f886b7e2bc623b7df25e1bc25d1cc87c622b28f0ae526664",
"oraclePublicKey":"1d524d2753a36ebe340af67370f78219b4dbb6f56d2f96b3b21eaabec6f4a114",
"oracleEvent":{
"oracleNonces":[
"bc927a2c8bf43c9d208e679848ffaf95d178fdbd2e29d1c66668f21dd75149e8",
"9ed74e19c1d532f5127829b7d9f183e0738ad084485428b53a7fe0c50f2efe5e",
"f44733d1129d0cd9253124749f8cff2c7e7eecd79888a4a015d3e3ad153ef282",
"f4f39e5733bfc5ca18530eb444419b31d9dc0ec938502615c33f2b0b7c05ac71",
"930991374fbf6b9a49e5e16fa3c5c39638af58f5a4c55682a93b2b940502e7bf",
"e3af3b59907c349d627e3f4f20125bdc1e979cac41ee82ef0a184000c79e904b",
"0b95d4335713752329a1791b963d526c0a49873bbbfcad9e1c03881508b2a801",
"48776cc1e3b8f3ff7fd6226ea2df5607787913468a1c0faad4ff315b7cf3b41d",
"0b39b0e1a14f5f50cb05f0a6d8e7c082f75e9fe386006727af933ce4d273a76f",
"479a38e13c1622bfd53299ee67680d7a0edd3fed92223e3a878c8d010fcc1a2d"
],
"eventMaturityEpoch":1623133104,
"eventDescriptor":{
"digitDecompositionEvent":{
"base":2,
"isSigned":false,
"unit":"sats/sec",
"precision":0,
"nbDigits":10
}
},
"eventId":"Test"
}
}
}
}
}
}
},
"fundingPubkey":"02556021f6abda2ae7a74d38a4e4a3b00c1dd648db96397dcd8642c3d0b0b139d1",
"payoutSpk":"0014430af74f2f9dc88729fd02eaeb946fc161e2be1e",
"payoutSerialId":8165863461276958928,
"offerCollateral":60000,
"fundingInputs":[
{
"inputSerialId":11632658032743242199,
"prevTx":"02000000000101e79f7a30bb35206060eb09a99b6956bcdc7a1767b310c8dfde3595c69246a60e0000000000feffffff0200c2eb0b000000001600142a416c1e5f5e78bc6c518294fd1dd86b40eed2d77caf953e000000001600148e56705661334df89b2c1c7c4e41da9cef9eb38e0247304402201491f05ebe196b333420cbab3e7e7f3e431bfe91a42730cef9c6e64b0e8ff62302202c5fc79abbdb0a1c8ad422dbb97a54693feedc580f0cb7a62bdadaecbfc4f9430121035f57172a38f35f29f4357dcc2d24ea8e72638cf43190e4fdcb3f0ace215cfd5602020000",
"prevTxVout":0,
"sequence":4294967295,
"maxWitnessLen":107,
"redeemScript":""
}
],
"changeSpk":"001441ca183be469eab996f34ed31197a96b57f6050e",
"changeSerialId":16919534260907952016,
"fundOutputSerialId":5054305248376932341,
"feeRatePerVb":400,
"cetLocktime":1623133103,
"refundLocktime":1623737904
}
1 change: 1 addition & 0 deletions dlc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[package]
authors = ["Crypto Garage"]
description = "Creation, signing and verification of Discreet Log Contracts (DLC) transactions."
edition = "2018"
homepage = "https://github.com/p2pderivatives/rust-dlc"
license-file = "../LICENSE"
name = "dlc"
Expand Down
29 changes: 11 additions & 18 deletions dlc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use secp256k1_zkp::{
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::fmt;
use util::{get_change_weight, get_inputs_weight};

pub mod channel;
pub mod secp_utils;
Expand Down Expand Up @@ -80,6 +81,8 @@ macro_rules! checked_add {
};
}

pub(crate) use checked_add;

/// Represents the payouts for a unique contract outcome. Offer party represents
/// the initiator of the contract while accept party represents the party
/// accepting the contract.
Expand Down Expand Up @@ -282,25 +285,15 @@ impl PartyParams {
fee_rate_per_vb: u64,
extra_fee: u64,
) -> Result<(TxOut, u64, u64), Error> {
let mut inputs_weight: usize = 0;

for w in &self.inputs {
let script_weight = util::redeem_script_to_script_sig(&w.redeem_script)
.len()
.checked_mul(4)
.ok_or(Error::InvalidArgument)?;
inputs_weight = checked_add!(
inputs_weight,
TX_INPUT_BASE_WEIGHT,
script_weight,
w.max_witness_len
)?;
}
let inputs_weight = get_inputs_weight(
&self
.inputs
.iter()
.map(|x| (x.redeem_script.as_ref(), x.max_witness_len))
.collect::<Vec<_>>(),
)?;

// Value size + script length var_int + ouput script pubkey size
let change_size = self.change_script_pubkey.len();
// Change size is scaled by 4 from vBytes to weight units
let change_weight = change_size.checked_mul(4).ok_or(Error::InvalidArgument)?;
let change_weight = get_change_weight(&self.change_script_pubkey)?;

// Base weight (nLocktime, nVersion, ...) is distributed among parties
// independently of inputs contributed
Expand Down
Loading

0 comments on commit 97af684

Please sign in to comment.