Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 86 additions & 19 deletions src/cfddlc_transactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,14 @@ TransactionController DlcManager::CreateFundTransaction(
remote_serial_id};

outputs_info.push_back(fund_output_info);
outputs_info.push_back(local_output_info);
outputs_info.push_back(remote_output_info);

// Single-funded DLC: exclude zero-value change outputs (dust filtering)
if (!IsDustOutputInfo(local_output_info)) {
outputs_info.push_back(local_output_info);
}
if (!IsDustOutputInfo(remote_output_info)) {
outputs_info.push_back(remote_output_info);
}

std::sort(outputs_info.begin(), outputs_info.end(), CompareOutputSerialId);

Expand Down Expand Up @@ -258,8 +264,13 @@ TransactionController DlcManager::CreateBatchFundTransaction(
remote_change_output.GetLockingScript(), remote_change_output.GetValue(),
remote_serial_id};

outputs_info.push_back(local_output_info);
outputs_info.push_back(remote_output_info);
// Single-funded DLC: exclude zero-value change outputs (dust filtering)
if (!IsDustOutputInfo(local_output_info)) {
outputs_info.push_back(local_output_info);
}
if (!IsDustOutputInfo(remote_output_info)) {
outputs_info.push_back(remote_output_info);
}

std::sort(outputs_info.begin(), outputs_info.end(), CompareOutputSerialId);

Expand Down Expand Up @@ -645,6 +656,31 @@ DlcTransactions DlcManager::CreateDlcTransactions(
std::tie(remote_change_output, remote_fund_fee, remote_cet_fee) =
GetChangeOutputAndFees(remote_params, fee_rate);

// Single-funded DLC: when one party has no inputs, the funding party pays all
// fees
if (local_params.input_amount == 0) {
// Local party has no inputs, so remote party should pay for both CET fees
remote_cet_fee += local_cet_fee;
local_cet_fee = 0;
// Recalculate remote change: fund both collaterals and all fees
auto remote_total_cost = local_params.collateral +
remote_params.collateral + remote_fund_fee +
remote_cet_fee;
remote_change_output = TxOut(
remote_params.input_amount - remote_total_cost - option_premium,
remote_params.change_script_pubkey);
} else if (remote_params.input_amount == 0) {
// Remote party has no inputs, so local party should pay for both CET fees
local_cet_fee += remote_cet_fee;
remote_cet_fee = 0;
// Recalculate local change: fund both collaterals and all fees
auto local_total_cost = local_params.collateral + remote_params.collateral +
local_fund_fee + local_cet_fee;
local_change_output = TxOut(
local_params.input_amount - local_total_cost - option_premium,
local_params.change_script_pubkey);
}

auto fund_output_value = local_params.input_amount +
remote_params.input_amount -
local_change_output.GetValue().GetSatoshiValue() -
Expand Down Expand Up @@ -679,16 +715,22 @@ DlcTransactions DlcManager::CreateDlcTransactions(
// the given lock time.
auto fund_tx_id = fund_tx.GetTransaction().GetTxid();

std::vector<uint64_t> change_serial_ids = {
fund_output_serial_id, local_params.change_serial_id,
remote_params.change_serial_id};
// Build list of actual serial IDs that were included (excluding dust outputs)
std::vector<uint64_t> actual_serial_ids = {fund_output_serial_id};

std::sort(change_serial_ids.begin(), change_serial_ids.end());
if (local_change_output.GetValue() >= DUST_LIMIT) {
actual_serial_ids.push_back(local_params.change_serial_id);
}
if (remote_change_output.GetValue() >= DUST_LIMIT) {
actual_serial_ids.push_back(remote_params.change_serial_id);
}

std::sort(actual_serial_ids.begin(), actual_serial_ids.end());

uint32_t fund_vout = 0;

for (size_t i = 0; i < change_serial_ids.size(); i++) {
if (change_serial_ids[i] == fund_output_serial_id) {
for (size_t i = 0; i < actual_serial_ids.size(); i++) {
if (actual_serial_ids[i] == fund_output_serial_id) {
fund_vout = static_cast<uint32_t>(i);
break;
}
Expand Down Expand Up @@ -824,21 +866,26 @@ BatchDlcTransactions DlcManager::CreateBatchDlcTransactions(
fund_vouts.push_back(i);
}
} else {
// set change_serial_ids to fund_output_serial_ids and change_serial_ids and
// sort
std::vector<uint64_t> change_serial_ids = fund_output_serial_ids;
change_serial_ids.push_back(local_params.change_serial_id);
change_serial_ids.push_back(remote_params.change_serial_id);
std::sort(change_serial_ids.begin(), change_serial_ids.end());
// Build list of actual serial IDs that were included (excluding dust
// outputs)
std::vector<uint64_t> actual_serial_ids = fund_output_serial_ids;

if (local_change_output.GetValue() >= DUST_LIMIT) {
actual_serial_ids.push_back(local_params.change_serial_id);
}
if (remote_change_output.GetValue() >= DUST_LIMIT) {
actual_serial_ids.push_back(remote_params.change_serial_id);
}
std::sort(actual_serial_ids.begin(), actual_serial_ids.end());

// set fund_vouts to empty array
fund_vouts.resize(fund_output_serial_ids.size());

// set fund_vouts to the index of fund_output_serial_ids in
// change_serial_ids
// actual_serial_ids
for (size_t i = 0; i < fund_output_serial_ids.size(); i++) {
for (size_t j = 0; j < change_serial_ids.size(); j++) {
if (fund_output_serial_ids[i] == change_serial_ids[j]) {
for (size_t j = 0; j < actual_serial_ids.size(); j++) {
if (fund_output_serial_ids[i] == actual_serial_ids[j]) {
fund_vouts[i] = j;
break;
}
Expand Down Expand Up @@ -926,6 +973,16 @@ std::tuple<TxOut, uint64_t, uint64_t> DlcManager::GetChangeOutputAndFees(
params.final_script_pubkey.GetData().GetDataSize() * 4);
auto cet_fee = ceil(cet_weight / 4) * fee_rate;
auto fund_out = params.collateral + fund_fee + cet_fee;

// Single-funded DLC: party with no inputs contributes zero fees
if (params.input_amount == 0) {
// Party with no inputs: return zero change output and zero fees
// The funding party pays for all fees including this party's CET fees
TxOut change_output(
Amount::CreateBySatoshiAmount(0), params.change_script_pubkey);
return std::make_tuple(change_output, 0, 0);
}

if (params.input_amount < (fund_out + option_premium)) {
throw CfdException(
CfdError::kCfdIllegalArgumentError,
Expand Down Expand Up @@ -961,6 +1018,16 @@ std::tuple<TxOut, uint64_t, uint64_t> DlcManager::GetBatchChangeOutputAndFees(
params.collaterals.begin(), params.collaterals.end(), Amount(0));

auto fund_out = collateral + fund_fee + cet_fee;

// Single-funded DLC: party with no inputs contributes zero fees
if (params.input_amount == 0) {
// Party with no inputs: return zero change output and zero fees
// The funding party pays for all fees including this party's CET fees
TxOut change_output(
Amount::CreateBySatoshiAmount(0), params.change_script_pubkey);
return std::make_tuple(change_output, 0, 0);
}

if (params.input_amount < fund_out) {
throw CfdException(
CfdError::kCfdIllegalArgumentError,
Expand Down
187 changes: 184 additions & 3 deletions test/test_cfddlc_transactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -800,19 +800,131 @@ TEST(DlcManager, CreateCetTransactionNotEnoughInputTest) {

auto local_params_short = LOCAL_PARAMS;
local_params_short.input_amount = Amount::CreateBySatoshiAmount(1000);
auto remote_params_short = LOCAL_PARAMS;
auto remote_params_short = REMOTE_PARAMS;
remote_params_short.input_amount = Amount::CreateBySatoshiAmount(1000);
// Act/Assert

// Act/Assert - Still should throw for insufficient funds (not zero)
ASSERT_THROW(
auto dlc_transactions = DlcManager::CreateDlcTransactions(
outcomes, local_params_short, REMOTE_PARAMS, REFUND_LOCKTIME, 1),
CfdException);
ASSERT_THROW(
auto dlc_transactions = DlcManager::CreateDlcTransactions(
outcomes, remote_params_short, REMOTE_PARAMS, REFUND_LOCKTIME, 1),
outcomes, local_params_short, remote_params_short, REFUND_LOCKTIME, 1),
CfdException);
}

TEST(DlcManager, CreateSingleFundedDlcTransactions) {
// Arrange
std::vector<DlcOutcome> outcomes = {
{WIN_AMOUNT, LOSE_AMOUNT}, {LOSE_AMOUNT, WIN_AMOUNT}};

// Create params for single-funded DLC where acceptor has zero inputs
auto local_params_funded = LOCAL_PARAMS;
local_params_funded.input_amount = Amount::CreateByCoinAmount(
100); // Double the input for both parties' collateral

auto remote_params_unfunded = REMOTE_PARAMS;
remote_params_unfunded.input_amount =
Amount::CreateBySatoshiAmount(0); // Zero inputs for acceptor
remote_params_unfunded.inputs_info.clear(); // No inputs

// Act
auto dlc_transactions = DlcManager::CreateDlcTransactions(
outcomes, local_params_funded, remote_params_unfunded, REFUND_LOCKTIME, 1);

auto fund_tx = dlc_transactions.fund_transaction;
auto refund_tx = dlc_transactions.refund_transaction;
auto cets = dlc_transactions.cets;

// Assert
EXPECT_EQ(dlc_transactions.cets.size(), outcomes.size());

// Fund transaction should have only 2 outputs (fund + local change), remote
// change should be filtered out as dust
EXPECT_EQ(2, fund_tx.GetTransaction().GetTxOutCount());

// Verify fund output value accounts for both parties' collateral
auto fund_output_amount = fund_tx.GetTransaction().GetTxOut(0).GetValue();
EXPECT_GT(
fund_output_amount.GetSatoshiValue(),
LOCAL_COLLATERAL_AMOUNT.GetSatoshiValue() +
REMOTE_COLLATERAL_AMOUNT.GetSatoshiValue());

// Verify refund transaction correctly distributes collateral
EXPECT_EQ(
LOCAL_COLLATERAL_AMOUNT.GetSatoshiValue(),
refund_tx.GetTransaction().GetTxOut(0).GetValue().GetSatoshiValue());
EXPECT_EQ(
REMOTE_COLLATERAL_AMOUNT.GetSatoshiValue(),
refund_tx.GetTransaction().GetTxOut(1).GetValue().GetSatoshiValue());

// Verify CETs have correct payouts
EXPECT_EQ(
WIN_AMOUNT.GetSatoshiValue(),
cets[0].GetTransaction().GetTxOut(0).GetValue().GetSatoshiValue());
EXPECT_EQ(
LOSE_AMOUNT.GetSatoshiValue(),
cets[0].GetTransaction().GetTxOut(1).GetValue().GetSatoshiValue());
}

TEST(DlcManager, CreateBatchSingleFundedDlcTransactions) {
// Arrange
std::vector<DlcOutcome> outcomes = {
{WIN_AMOUNT, LOSE_AMOUNT}, {LOSE_AMOUNT, WIN_AMOUNT}};
std::vector<std::vector<DlcOutcome>> outcomes_batch = {outcomes, outcomes};
std::vector<uint64_t> refund_locktimes = {REFUND_LOCKTIME, REFUND_LOCKTIME};

// Create params for single-funded batch DLC where acceptor has zero inputs
auto local_batch_params_funded = LOCAL_BATCH_PARAMS;
local_batch_params_funded.input_amount =
Amount::CreateByCoinAmount(200); // Enough for both DLCs

auto remote_batch_params_unfunded = REMOTE_BATCH_PARAMS;
remote_batch_params_unfunded.input_amount =
Amount::CreateBySatoshiAmount(0); // Zero inputs for acceptor
remote_batch_params_unfunded.inputs_info.clear(); // No inputs

// Act
auto dlc_transactions = DlcManager::CreateBatchDlcTransactions(
outcomes_batch, local_batch_params_funded, remote_batch_params_unfunded,
refund_locktimes, 1);

auto fund_tx = dlc_transactions.fund_transaction;
auto refund_txs = dlc_transactions.refund_transactions;
auto cets_list = dlc_transactions.cets_list;

// Assert
EXPECT_EQ(cets_list.size(), outcomes_batch.size());
EXPECT_EQ(refund_txs.size(), outcomes_batch.size());

// Fund transaction should have only 3 outputs (2 fund outputs + local
// change), remote change should be filtered out as dust
EXPECT_EQ(3, fund_tx.GetTransaction().GetTxOutCount());

// Verify each refund transaction correctly distributes collateral
for (size_t i = 0; i < refund_txs.size(); i++) {
EXPECT_EQ(
LOCAL_COLLATERAL_AMOUNT.GetSatoshiValue(),
refund_txs[i].GetTransaction().GetTxOut(0).GetValue().GetSatoshiValue());
EXPECT_EQ(
REMOTE_COLLATERAL_AMOUNT.GetSatoshiValue(),
refund_txs[i].GetTransaction().GetTxOut(1).GetValue().GetSatoshiValue());
}

// Verify each CET list has correct payouts
for (size_t i = 0; i < cets_list.size(); i++) {
auto cets = cets_list[i];
EXPECT_EQ(cets.size(), outcomes.size());
EXPECT_EQ(
WIN_AMOUNT.GetSatoshiValue(),
cets[0].GetTransaction().GetTxOut(0).GetValue().GetSatoshiValue());
EXPECT_EQ(
LOSE_AMOUNT.GetSatoshiValue(),
cets[0].GetTransaction().GetTxOut(1).GetValue().GetSatoshiValue());
}
}

TEST(DlcManager, FundTransactionWithPremiumTest) {
// Arrange
auto local_change = Amount::CreateBySatoshiAmount(4899899758);
Expand Down Expand Up @@ -1150,3 +1262,72 @@ TEST(DlcManager, CetTestSerialId) {
REMOTE_FUND_PRIVKEY, fund_script, FUND_TX_SERIAL_ID, 0, FUND_OUTPUT);
EXPECT_EQ(cet.GetHex(), CET_SERIAL_ID_HEX_SIGNED.GetHex());
}

TEST(DlcManager, CreateSingleFundedDlcTransactionsNoChange) {
// Arrange
std::vector<DlcOutcome> outcomes = {
{WIN_AMOUNT, LOSE_AMOUNT}, {LOSE_AMOUNT, WIN_AMOUNT}};

// First, run with large amount to see what the change is
auto local_params_large = LOCAL_PARAMS;
local_params_large.input_amount =
Amount::CreateByCoinAmount(100); // Large amount

auto remote_params_unfunded = REMOTE_PARAMS;
remote_params_unfunded.input_amount = Amount::CreateBySatoshiAmount(0);
remote_params_unfunded.inputs_info.clear();

// Get the change amount from a large input
auto dlc_transactions_large = DlcManager::CreateDlcTransactions(
outcomes, local_params_large, remote_params_unfunded, REFUND_LOCKTIME, 1);
auto fund_tx_large = dlc_transactions_large.fund_transaction;

// Calculate exact amount needed: large_input - change_amount
auto large_input = local_params_large.input_amount;
auto change_amount = fund_tx_large.GetTransaction()
.GetTxOut(1)
.GetValue(); // Change is second output
auto exact_amount_needed = large_input - change_amount;

// Now create params with exact amount
auto local_params_exact = LOCAL_PARAMS;
local_params_exact.input_amount = exact_amount_needed;

// Act
auto dlc_transactions = DlcManager::CreateDlcTransactions(
outcomes, local_params_exact, remote_params_unfunded, REFUND_LOCKTIME, 1);

auto fund_tx = dlc_transactions.fund_transaction;
auto refund_tx = dlc_transactions.refund_transaction;
auto cets = dlc_transactions.cets;

// Assert
EXPECT_EQ(dlc_transactions.cets.size(), outcomes.size());

// Fund transaction should have exactly 1 output (just the funding output, no
// change)
EXPECT_EQ(1, fund_tx.GetTransaction().GetTxOutCount());

// The single output should be the funding output
auto fund_output_amount = fund_tx.GetTransaction().GetTxOut(0).GetValue();
EXPECT_GT(
fund_output_amount.GetSatoshiValue(),
LOCAL_COLLATERAL_AMOUNT.GetSatoshiValue() +
REMOTE_COLLATERAL_AMOUNT.GetSatoshiValue());

// Verify refund transaction correctly distributes collateral
EXPECT_EQ(
LOCAL_COLLATERAL_AMOUNT.GetSatoshiValue(),
refund_tx.GetTransaction().GetTxOut(0).GetValue().GetSatoshiValue());
EXPECT_EQ(
REMOTE_COLLATERAL_AMOUNT.GetSatoshiValue(),
refund_tx.GetTransaction().GetTxOut(1).GetValue().GetSatoshiValue());

// Verify CETs have correct payouts
EXPECT_EQ(
WIN_AMOUNT.GetSatoshiValue(),
cets[0].GetTransaction().GetTxOut(0).GetValue().GetSatoshiValue());
EXPECT_EQ(
LOSE_AMOUNT.GetSatoshiValue(),
cets[0].GetTransaction().GetTxOut(1).GetValue().GetSatoshiValue());
}