diff --git a/include/cfddlc/cfddlc_transactions.h b/include/cfddlc/cfddlc_transactions.h index b6d95c3..5dfeb11 100644 --- a/include/cfddlc/cfddlc_transactions.h +++ b/include/cfddlc/cfddlc_transactions.h @@ -98,6 +98,43 @@ struct CFD_DLC_EXPORT DlcOutcome { Amount remote_payout; }; +/** + * @brief Information about an existing DLC funding output to be used as input. + * + * This enables DLC splicing - using the funding output of an existing DLC + * as input to a new DLC, allowing for dynamic fund management. + */ +struct DlcInputInfo { + /** + * @brief The transaction ID of the existing DLC funding transaction. + */ + Txid fund_txid; + /** + * @brief The vout index of the DLC funding output. + */ + uint32_t fund_vout; + /** + * @brief The amount locked in the DLC funding output. + */ + Amount fund_amount; + /** + * @brief The local party's funding public key from the existing DLC. + */ + Pubkey local_fund_pubkey; + /** + * @brief The remote party's funding public key from the existing DLC. + */ + Pubkey remote_fund_pubkey; + /** + * @brief The maximum witness length for spending this DLC output. + */ + uint32_t max_witness_length; + /** + * @brief Randomly chosen number used to sort inputs. + */ + uint64_t input_serial_id; +}; + /** * @brief Contain a transaction input together with the maximum witness length * for this input. @@ -169,6 +206,12 @@ struct PartyParams { * */ std::vector inputs_info; + /** + * @brief A list of existing DLC funding outputs to be used as inputs (for + * splicing) + * + */ + std::vector dlc_inputs_info; /** * @brief The total value of the provided inputs * @@ -777,6 +820,86 @@ class CFD_DLC_EXPORT DlcManager { const std::vector &fund_output_serial_ids = std::vector()); + /** + * @brief Create a set of DLC transactions that splice existing DLC funding + * outputs. This enables dynamic DLC management by using existing DLC outputs + * as inputs. + * + * @param outcomes the possible outcome values. + * @param local_params the parameters for the local party (may include DLC + * inputs). + * @param remote_params the parameters for the remote party (may include DLC + * inputs). + * @param refund_locktime the unix time or block number after which the + * refund transaction can be used. + * @param fee_rate the fee rate to compute the fees. + * @param option_dest (optional) destination address for the payment of the + * option premium + * @param option_premium (optional) value for the option premium + * @param fund_lock_time the lock time to use for the fund transaction + * (optional) + * @param cet_lock_time the lock time to use for the cet transactions + * (optional) + * @param fund_output_serial_id serial ID for the fund output sorting + * (optional) + * @return DlcTransactions a struct containing the necessary transaction + * to establish a DLC with spliced inputs. + */ + static DlcTransactions CreateSplicedDlcTransactions( + const std::vector &outcomes, + const PartyParams &local_params, + const PartyParams &remote_params, + uint64_t refund_locktime, + uint32_t fee_rate, + const Address &option_dest = Address(), + const Amount &option_premium = Amount::CreateBySatoshiAmount(0), + const uint64_t fund_lock_time = 0, + const uint64_t cet_lock_time = 0, + const uint64_t fund_output_serial_id = 0); + + /** + * @brief Sign a DLC input in a funding transaction. + * + * @param fund_transaction the funding transaction to sign. + * @param dlc_input the DLC input information. + * @param local_privkey the local party's private key. + * @param remote_signature the remote party's signature for this input. + */ + static void SignDlcFundingInput( + TransactionController *fund_transaction, + const DlcInputInfo &dlc_input, + const Privkey &local_privkey, + const ByteData &remote_signature); + + /** + * @brief Get a raw signature for a DLC input in a funding transaction. + * + * @param fund_transaction the funding transaction to sign. + * @param dlc_input the DLC input information. + * @param privkey the private key to sign with. + * @return ByteData the raw signature. + */ + static ByteData GetRawDlcFundingInputSignature( + const TransactionController &fund_transaction, + const DlcInputInfo &dlc_input, + const Privkey &privkey); + + /** + * @brief Verify a signature for a DLC input in a funding transaction. + * + * @param fund_transaction the funding transaction. + * @param dlc_input the DLC input information. + * @param signature the signature to verify. + * @param pubkey the public key to verify against. + * @return true if the signature is valid. + * @return false if the signature is invalid. + */ + static bool VerifyDlcFundingInputSignature( + const TransactionController &fund_transaction, + const DlcInputInfo &dlc_input, + const ByteData &signature, + const Pubkey &pubkey); + private: /** * @brief Create a Fund Transaction object @@ -907,6 +1030,43 @@ class CFD_DLC_EXPORT DlcManager { static std::tuple GetBatchChangeOutputAndFees( const BatchPartyParams ¶ms, uint64_t fee_rate); + /** + * @brief Convert DLC inputs to regular transaction inputs. + * + * @param dlc_inputs_info the DLC inputs to convert. + * @return std::vector the converted transaction inputs. + */ + static std::vector + ConvertDlcInputsToTxInputs(const std::vector &dlc_inputs_info); + + /** + * @brief Get the total weight for DLC inputs. + * + * @param dlc_inputs_info the DLC inputs to calculate weight for. + * @return uint32_t the total weight. + */ + static uint32_t + GetDlcInputsWeight(const std::vector &dlc_inputs_info); + + /** + * @brief Calculate the total input amount from regular and DLC inputs. + * + * @param regular_inputs the regular UTXO inputs. + * @param dlc_inputs the DLC funding inputs. + * @return Amount the total input amount. + */ + static Amount CalculateTotalInputAmount( + const std::vector ®ular_inputs, + const std::vector &dlc_inputs); + + /** + * @brief Create the funding script for a DLC input. + * + * @param dlc_input the DLC input information. + * @return Script the funding script. + */ + static Script CreateDlcInputFundingScript(const DlcInputInfo &dlc_input); + /** * @brief Computes an adaptor point from a set of messages, r_values and a * public key. diff --git a/src/cfddlc_transactions.cpp b/src/cfddlc_transactions.cpp index 963ea4a..4fb864a 100644 --- a/src/cfddlc_transactions.cpp +++ b/src/cfddlc_transactions.cpp @@ -954,9 +954,14 @@ std::tuple DlcManager::GetChangeOutputAndFees( Amount option_premium, Address option_dest) { auto inputs_size = GetInputsWeight(params.inputs_info); + + // Add weight for DLC inputs + auto dlc_inputs_size = GetDlcInputsWeight(params.dlc_inputs_info); + auto change_size = params.change_script_pubkey.GetData().GetDataSize(); double fund_weight = - (FUND_TX_BASE_WEIGHT / 2 + inputs_size + change_size * 4 + 36); + (FUND_TX_BASE_WEIGHT / 2 + inputs_size + dlc_inputs_size + change_size * 4 + + 36); if (option_premium.GetSatoshiValue() > 0) { if (option_dest.GetAddress() == "") { throw CfdException( @@ -974,8 +979,13 @@ std::tuple DlcManager::GetChangeOutputAndFees( auto cet_fee = ceil(cet_weight / 4) * fee_rate; auto fund_out = params.collateral + fund_fee + cet_fee; + // Calculate total input amount including DLC inputs + Amount total_input_amount = + params.input_amount + CalculateTotalInputAmount( + std::vector(), params.dlc_inputs_info); + // Single-funded DLC: party with no inputs contributes zero fees - if (params.input_amount == 0) { + if (total_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( @@ -983,15 +993,20 @@ std::tuple DlcManager::GetChangeOutputAndFees( return std::make_tuple(change_output, 0, 0); } - if (params.input_amount < (fund_out + option_premium)) { + if (total_input_amount < (fund_out + option_premium)) { throw CfdException( CfdError::kCfdIllegalArgumentError, "Input amount smaller than required for collateral, " "fees and option premium."); } + if (total_input_amount < Amount::CreateBySatoshiAmount(546)) { // dust limit + throw CfdException( + CfdError::kCfdIllegalArgumentError, "DLC input amounts too small"); + } + TxOut change_output( - params.input_amount - fund_out - option_premium, + total_input_amount - fund_out - option_premium, params.change_script_pubkey); return std::make_tuple(change_output, fund_fee, cet_fee); @@ -1000,6 +1015,10 @@ std::tuple DlcManager::GetChangeOutputAndFees( std::tuple DlcManager::GetBatchChangeOutputAndFees( const BatchPartyParams ¶ms, uint64_t fee_rate) { auto inputs_size = GetInputsWeight(params.inputs_info); + + // Note: BatchPartyParams doesn't have dlc_inputs_info field yet + // This would need to be added if batch DLC splicing is required + auto change_size = params.change_script_pubkey.GetData().GetDataSize(); double fund_weight = ((BATCH_FUND_TX_BASE_WEIGHT + @@ -1057,5 +1076,178 @@ Pubkey DlcManager::ComputeAdaptorPoint( return SchnorrUtil::ComputeSigPointBatch(msgs, r_values, pubkey); } + +// DLC Splicing Implementation + +std::vector DlcManager::ConvertDlcInputsToTxInputs( + const std::vector &dlc_inputs_info) { + std::vector tx_inputs; + tx_inputs.reserve(dlc_inputs_info.size()); + + for (const auto &dlc_input : dlc_inputs_info) { + TxIn tx_in(dlc_input.fund_txid, dlc_input.fund_vout, 0, Script()); + TxInputInfo tx_input_info = { + tx_in, dlc_input.max_witness_length, dlc_input.input_serial_id}; + tx_inputs.push_back(tx_input_info); + } + + return tx_inputs; +} + +uint32_t DlcManager::GetDlcInputsWeight( + const std::vector &dlc_inputs_info) { + uint32_t total_weight = 0; + + for (const auto &dlc_input : dlc_inputs_info) { + // DLC inputs are P2WSH multisig inputs + // Base input: 36 bytes (txid + vout + sequence) + // Script sig: empty (1 byte for empty script) + // Witness: 1 byte (witness stack size) + signatures + redeem script + // Signatures: 2 * 72 bytes (max signature size) + 2 * 1 byte (length) + // Redeem script: ~69 bytes (2-of-2 multisig) + 1 byte (length) + // Total witness: ~220 bytes + total_weight += 36 * 4 + 1 * 4 + dlc_input.max_witness_length; + } + + return total_weight; +} + +Amount DlcManager::CalculateTotalInputAmount( + const std::vector ®ular_inputs, + const std::vector &dlc_inputs) { + Amount total = Amount::CreateBySatoshiAmount(0); + + // Note: regular_inputs don't contain amount info - that's in + // PartyParams.input_amount DLC inputs contain their amounts directly + for (const auto &dlc_input : dlc_inputs) { + total += dlc_input.fund_amount; + } + + return total; +} + +Script DlcManager::CreateDlcInputFundingScript(const DlcInputInfo &dlc_input) { + return CreateFundTxLockingScript( + dlc_input.local_fund_pubkey, dlc_input.remote_fund_pubkey); +} + +DlcTransactions DlcManager::CreateSplicedDlcTransactions( + const std::vector &outcomes, + const PartyParams &local_params, + const PartyParams &remote_params, + uint64_t refund_locktime, + uint32_t fee_rate, + const Address &option_dest, + const Amount &option_premium, + uint64_t fund_lock_time, + uint64_t cet_lock_time, + uint64_t fund_output_serial_id) { + // Create enhanced party parameters that include DLC inputs as regular inputs + PartyParams enhanced_local_params = local_params; + PartyParams enhanced_remote_params = remote_params; + + // Convert DLC inputs to regular transaction inputs and add to inputs_info + auto local_dlc_tx_inputs = + ConvertDlcInputsToTxInputs(local_params.dlc_inputs_info); + auto remote_dlc_tx_inputs = + ConvertDlcInputsToTxInputs(remote_params.dlc_inputs_info); + + enhanced_local_params.inputs_info.insert( + enhanced_local_params.inputs_info.end(), local_dlc_tx_inputs.begin(), + local_dlc_tx_inputs.end()); + + enhanced_remote_params.inputs_info.insert( + enhanced_remote_params.inputs_info.end(), remote_dlc_tx_inputs.begin(), + remote_dlc_tx_inputs.end()); + + // Calculate total input amounts including DLC inputs + Amount local_dlc_amount = CalculateTotalInputAmount( + std::vector(), local_params.dlc_inputs_info); + Amount remote_dlc_amount = CalculateTotalInputAmount( + std::vector(), remote_params.dlc_inputs_info); + + enhanced_local_params.input_amount += local_dlc_amount; + enhanced_remote_params.input_amount += remote_dlc_amount; + + // Clear DLC inputs from enhanced params since they're now regular inputs + enhanced_local_params.dlc_inputs_info.clear(); + enhanced_remote_params.dlc_inputs_info.clear(); + + // Validate DLC inputs from both parties + auto validate_dlc_inputs = [](const std::vector& inputs, + const std::string& party_name) { + for (const auto& dlc_input : inputs) { + if (dlc_input.fund_amount.GetSatoshiValue() == 0) { + throw CfdException( + CfdError::kCfdIllegalArgumentError, + party_name + " DLC input amount cannot be zero"); + } + // Validate that local_fund_pubkey != remote_fund_pubkey + if (dlc_input.local_fund_pubkey.GetHex() == + dlc_input.remote_fund_pubkey.GetHex()) { + throw CfdException( + CfdError::kCfdIllegalArgumentError, + party_name + + " DLC input local and remote pubkeys cannot be identical"); + } + } + }; + + validate_dlc_inputs(local_params.dlc_inputs_info, "Local"); + validate_dlc_inputs(remote_params.dlc_inputs_info, "Remote"); + + // Use the regular DLC transaction creation with enhanced parameters + return CreateDlcTransactions( + outcomes, enhanced_local_params, enhanced_remote_params, refund_locktime, + fee_rate, option_dest, option_premium, fund_lock_time, cet_lock_time, + fund_output_serial_id); +} + +ByteData DlcManager::GetRawDlcFundingInputSignature( + const TransactionController &fund_transaction, + const DlcInputInfo &dlc_input, + const Privkey &privkey) { + auto funding_script = CreateDlcInputFundingScript(dlc_input); + + return GetRawTxWitSigAllSignature( + fund_transaction, privkey, dlc_input.fund_txid, dlc_input.fund_vout, + funding_script, dlc_input.fund_amount); +} + +void DlcManager::SignDlcFundingInput( + TransactionController *fund_transaction, + const DlcInputInfo &dlc_input, + const Privkey &local_privkey, + const ByteData &remote_signature) { + auto funding_script = CreateDlcInputFundingScript(dlc_input); + auto local_signature = + GetRawDlcFundingInputSignature(*fund_transaction, dlc_input, local_privkey); + + // Determine signature order based on public key order + auto local_pubkey = local_privkey.GetPubkey(); + std::vector signatures; + + if (local_pubkey.GetHex() < dlc_input.local_fund_pubkey.GetHex()) { + signatures = {local_signature, remote_signature}; + } else { + signatures = {remote_signature, local_signature}; + } + + AddSignaturesForMultiSigInput( + fund_transaction, dlc_input.fund_txid, dlc_input.fund_vout, funding_script, + signatures); +} + +bool DlcManager::VerifyDlcFundingInputSignature( + const TransactionController &fund_transaction, + const DlcInputInfo &dlc_input, + const ByteData &signature, + const Pubkey &pubkey) { + auto funding_script = CreateDlcInputFundingScript(dlc_input); + + return fund_transaction.VerifyInputSignature( + signature, pubkey, dlc_input.fund_txid, dlc_input.fund_vout, funding_script, + SigHashType(), dlc_input.fund_amount, WitnessVersion::kVersion0); +} } // namespace dlc } // namespace cfd diff --git a/test/test_cfddlc_transactions.cpp b/test/test_cfddlc_transactions.cpp index 9d41d58..3b08034 100644 --- a/test/test_cfddlc_transactions.cpp +++ b/test/test_cfddlc_transactions.cpp @@ -24,6 +24,7 @@ using cfd::core::Pubkey; using cfd::core::SchnorrPubkey; using cfd::core::SchnorrSignature; using cfd::core::SchnorrUtil; +using cfd::core::Script; using cfd::core::ScriptUtil; using cfd::core::SigHashAlgorithm; using cfd::core::SigHashType; @@ -33,6 +34,7 @@ using cfd::core::TxOut; using cfd::core::WitnessVersion; using cfd::dlc::BatchPartyParams; +using cfd::dlc::DlcInputInfo; using cfd::dlc::DlcManager; using cfd::dlc::DlcOutcome; using cfd::dlc::PartyParams; @@ -150,11 +152,23 @@ const Address REMOTE_FINAL_ADDRESS2( Privkey("0000000000000000000000000000000000000000000000000000000000000010") .GeneratePubkey()); +const Script LOCAL_CHANGE_SCRIPT_PUBKEY = + LOCAL_CHANGE_ADDRESS.GetLockingScript(); +const Script LOCAL_FINAL_SCRIPT_PUBKEY = LOCAL_FINAL_ADDRESS.GetLockingScript(); +const Script REMOTE_CHANGE_SCRIPT_PUBKEY = + REMOTE_CHANGE_ADDRESS.GetLockingScript(); +const Script REMOTE_FINAL_SCRIPT_PUBKEY = + REMOTE_FINAL_ADDRESS.GetLockingScript(); + +const std::vector OUTCOMES = { + {WIN_AMOUNT, LOSE_AMOUNT}, {LOSE_AMOUNT, WIN_AMOUNT}}; + const PartyParams LOCAL_PARAMS = { LOCAL_FUND_PUBKEY, LOCAL_CHANGE_ADDRESS.GetLockingScript(), LOCAL_FINAL_ADDRESS.GetLockingScript(), LOCAL_INPUTS_INFO, + {}, // dlc_inputs_info LOCAL_INPUT_AMOUNT, LOCAL_COLLATERAL_AMOUNT, 0, @@ -165,6 +179,7 @@ const PartyParams REMOTE_PARAMS = { REMOTE_CHANGE_ADDRESS.GetLockingScript(), REMOTE_FINAL_ADDRESS.GetLockingScript(), REMOTE_INPUTS_INFO, + {}, // dlc_inputs_info REMOTE_INPUT_AMOUNT, REMOTE_COLLATERAL_AMOUNT, 0, @@ -175,6 +190,7 @@ const PartyParams LOCAL_PARAMS_SERIAL_ID = { LOCAL_CHANGE_ADDRESS.GetLockingScript(), LOCAL_FINAL_ADDRESS.GetLockingScript(), LOCAL_INPUTS_INFO_SERIAL_ID, + {}, // dlc_inputs_info LOCAL_INPUT_AMOUNT, LOCAL_COLLATERAL_AMOUNT, 4593, @@ -185,6 +201,7 @@ const PartyParams REMOTE_PARAMS_SERIAL_ID = { REMOTE_CHANGE_ADDRESS.GetLockingScript(), REMOTE_FINAL_ADDRESS.GetLockingScript(), REMOTE_INPUTS_INFO_SERIAL_ID, + {}, // dlc_inputs_info REMOTE_INPUT_AMOUNT, REMOTE_COLLATERAL_AMOUNT, 2332, @@ -1331,3 +1348,677 @@ TEST(DlcManager, CreateSingleFundedDlcTransactionsNoChange) { LOSE_AMOUNT.GetSatoshiValue(), cets[0].GetTransaction().GetTxOut(1).GetValue().GetSatoshiValue()); } + +// Test for CreateSplicedDlcTransactions +TEST(DlcManagerTest, CreateSplicedDlcTransactions) { + // Create initial DLC parameters for the existing DLC + Pubkey existing_local_pubkey = Pubkey( + "03f942716865bb9b62678d99aa34de4632249d066d99de2b5a2e542e54908450d6"); + Pubkey existing_remote_pubkey = Pubkey( + "03f942716865bb9b62678d99aa34de4632249d066d99de2b5a2e542e54908450d7"); + + // Create DLC input info representing an existing DLC to be spliced + DlcInputInfo existing_dlc_input; + existing_dlc_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + existing_dlc_input.fund_vout = 0; + existing_dlc_input.fund_amount = + Amount::CreateBySatoshiAmount(12000000); // Increased for fees + existing_dlc_input.local_fund_pubkey = existing_local_pubkey; + existing_dlc_input.remote_fund_pubkey = existing_remote_pubkey; + existing_dlc_input.max_witness_length = 220; + existing_dlc_input.input_serial_id = 1; + + // Create parameters for the local party (splicing in additional funds) + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; // No additional regular inputs for this test + local_params.dlc_inputs_info = {existing_dlc_input}; // Existing DLC input + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = Amount::CreateBySatoshiAmount(6000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + // Create parameters for the remote party (no additional funds, just + // participating) + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; // No additional inputs + remote_params.dlc_inputs_info = {}; // No DLC inputs + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(4000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + // Create outcomes that match the total collateral (10000000) + std::vector splice_outcomes = { + {Amount::CreateBySatoshiAmount(7000000), + Amount::CreateBySatoshiAmount(3000000)}, + {Amount::CreateBySatoshiAmount(3000000), + Amount::CreateBySatoshiAmount(7000000)}}; + + // Create new DLC with spliced inputs + EXPECT_NO_THROW( + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + splice_outcomes, local_params, remote_params, REFUND_LOCKTIME, 2)); + + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + splice_outcomes, local_params, remote_params, REFUND_LOCKTIME, 2); + + // Verify the fund transaction has the expected inputs + auto fund_tx = dlc_txs.fund_transaction; + EXPECT_EQ( + 1, fund_tx.GetTransaction() + .GetTxInCount()); // 1 DLC input (no regular inputs in this test) + + // Verify the DLC input is present + bool dlc_input_found = false; + for (uint32_t i = 0; i < fund_tx.GetTransaction().GetTxInCount(); i++) { + auto tx_in = fund_tx.GetTransaction().GetTxIn(i); + if ( + tx_in.GetTxid().GetHex() == existing_dlc_input.fund_txid.GetHex() && + tx_in.GetVout() == existing_dlc_input.fund_vout) { + dlc_input_found = true; + break; + } + } + EXPECT_TRUE(dlc_input_found); + + // Verify the total collateral is correct (input amount larger to cover fees) + EXPECT_EQ( + 10000000, local_params.collateral.GetSatoshiValue() + + remote_params.collateral.GetSatoshiValue()); + + // Verify DLC input provides sufficient funds (12M input for 10M collateral + + // fees) + EXPECT_GT(existing_dlc_input.fund_amount.GetSatoshiValue(), 10000000); + + // Verify CETs are created + EXPECT_EQ(splice_outcomes.size(), dlc_txs.cets.size()); + + // Verify refund transaction is created + EXPECT_EQ(1, dlc_txs.refund_transaction.GetTransaction().GetTxInCount()); +} + +// Test for DLC input signature functions +TEST(DlcManagerTest, DlcInputSigningFunctions) { + // Create a simple funding transaction + auto fund_tx = cfd::TransactionController(2, 0); + + // Create DLC input info + DlcInputInfo dlc_input; + dlc_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + dlc_input.fund_vout = 0; + dlc_input.fund_amount = Amount::CreateBySatoshiAmount(10000000); + dlc_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + dlc_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + dlc_input.max_witness_length = 220; + dlc_input.input_serial_id = 1; + + // Add the DLC input to the funding transaction + fund_tx.AddTxIn(dlc_input.fund_txid, dlc_input.fund_vout); + + // Add a dummy output to make it a valid transaction + fund_tx.AddTxOut( + Script("0014a7b2b1da77ffa99d565b00063e4c777bb8a4d669"), + Amount::CreateBySatoshiAmount(9999000)); + + // Create private keys for signing + Privkey local_privkey = Privkey::GenerageRandomKey(); + Privkey remote_privkey = Privkey::GenerageRandomKey(); + + // Test getting raw signature + ByteData local_signature; + EXPECT_NO_THROW( + local_signature = DlcManager::GetRawDlcFundingInputSignature( + fund_tx, dlc_input, local_privkey)); + EXPECT_FALSE(local_signature.IsEmpty()); + + // Test signature verification + EXPECT_TRUE(DlcManager::VerifyDlcFundingInputSignature( + fund_tx, dlc_input, local_signature, local_privkey.GetPubkey())); + + // Test with wrong signature + ByteData wrong_signature = ByteData( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"); + EXPECT_FALSE(DlcManager::VerifyDlcFundingInputSignature( + fund_tx, dlc_input, wrong_signature, local_privkey.GetPubkey())); +} + +// Test for splice-out scenario (removing funds from DLC) +TEST(DlcManagerTest, CreateSplicedDlcTransactionsSpliceOut) { + // Create DLC input with large amount + DlcInputInfo large_dlc_input; + large_dlc_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + large_dlc_input.fund_vout = 0; + large_dlc_input.fund_amount = + Amount::CreateBySatoshiAmount(20000000); // 0.2 BTC + large_dlc_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + large_dlc_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + large_dlc_input.max_witness_length = 220; + large_dlc_input.input_serial_id = 1; + + // Local party wants to reduce their commitment + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; // No additional inputs + local_params.dlc_inputs_info = {large_dlc_input}; // Using existing DLC + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = + Amount::CreateBySatoshiAmount(5000000); // Reduced collateral + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + // Remote party also reduces commitment + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {}; + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = + Amount::CreateBySatoshiAmount(3000000); // Reduced collateral + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + // Create outcomes that match the reduced total collateral (8000000) + std::vector reduced_outcomes = { + {Amount::CreateBySatoshiAmount(6000000), + Amount::CreateBySatoshiAmount(2000000)}, + {Amount::CreateBySatoshiAmount(2000000), + Amount::CreateBySatoshiAmount(6000000)}}; + + // Create spliced DLC with reduced amounts + EXPECT_NO_THROW( + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + reduced_outcomes, local_params, remote_params, REFUND_LOCKTIME, 2)); + + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + reduced_outcomes, local_params, remote_params, REFUND_LOCKTIME, 2); + + // Should have significant change output due to splice-out + auto fund_tx = dlc_txs.fund_transaction; + + // Find the change output (should be substantial due to splice-out) + bool substantial_change_found = false; + for (uint32_t i = 0; i < fund_tx.GetTransaction().GetTxOutCount(); i++) { + auto tx_out = fund_tx.GetTransaction().GetTxOut(i); + if (tx_out.GetValue().GetSatoshiValue() > 10000000) { // Expect large + // change + substantial_change_found = true; + break; + } + } + EXPECT_TRUE(substantial_change_found); +} + +// Test for DLC transition (changing oracle or terms) +TEST(DlcManagerTest, CreateSplicedDlcTransactionsDlcTransition) { + // Create DLC input representing an existing DLC + DlcInputInfo existing_dlc_input; + existing_dlc_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + existing_dlc_input.fund_vout = 0; + existing_dlc_input.fund_amount = + Amount::CreateBySatoshiAmount(22000000); // Increased for fees + existing_dlc_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + existing_dlc_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + existing_dlc_input.max_witness_length = 220; + existing_dlc_input.input_serial_id = 1; + + // Both parties contribute the existing DLC (50/50 split) + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; + local_params.dlc_inputs_info = {existing_dlc_input}; + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = Amount::CreateBySatoshiAmount(10000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {}; + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(10000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + // Create new DLC with different outcomes (transition) + std::vector new_outcomes = { + {Amount::CreateBySatoshiAmount(15000000), + Amount::CreateBySatoshiAmount(5000000)}, + {Amount::CreateBySatoshiAmount(8000000), + Amount::CreateBySatoshiAmount(12000000)}, + {Amount::CreateBySatoshiAmount(0), + Amount::CreateBySatoshiAmount(20000000)}}; + + EXPECT_NO_THROW( + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + new_outcomes, local_params, remote_params, REFUND_LOCKTIME, 2)); + + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + new_outcomes, local_params, remote_params, REFUND_LOCKTIME, 2); + + // Verify the transition created the expected number of CETs + EXPECT_EQ(new_outcomes.size(), dlc_txs.cets.size()); + + // Verify the funding transaction uses the existing DLC input + auto fund_tx = dlc_txs.fund_transaction; + EXPECT_EQ(1, fund_tx.GetTransaction().GetTxInCount()); + + auto tx_in = fund_tx.GetTransaction().GetTxIn(0); + EXPECT_EQ(existing_dlc_input.fund_txid.GetHex(), tx_in.GetTxid().GetHex()); + EXPECT_EQ(existing_dlc_input.fund_vout, tx_in.GetVout()); +} + +// Test for DLC input validation - zero amount +TEST(DlcManagerTest, CreateSplicedDlcTransactionsZeroAmountFails) { + // Create DLC input with zero amount + DlcInputInfo zero_amount_input; + zero_amount_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + zero_amount_input.fund_vout = 0; + zero_amount_input.fund_amount = Amount::CreateBySatoshiAmount(0); // Zero amount + zero_amount_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + zero_amount_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + zero_amount_input.max_witness_length = 220; + zero_amount_input.input_serial_id = 1; + + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; + local_params.dlc_inputs_info = {zero_amount_input}; // Zero amount DLC input + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = Amount::CreateBySatoshiAmount(5000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {}; + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(5000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + std::vector outcomes = { + {Amount::CreateBySatoshiAmount(7000000), + Amount::CreateBySatoshiAmount(3000000)}, + {Amount::CreateBySatoshiAmount(3000000), + Amount::CreateBySatoshiAmount(7000000)}}; + + // Should throw exception for zero amount DLC input + EXPECT_THROW( + DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2), + CfdException); +} + +// Test for DLC input validation - identical pubkeys +TEST(DlcManagerTest, CreateSplicedDlcTransactionsIdenticalPubkeysFails) { + // Create DLC input with identical local and remote pubkeys + DlcInputInfo identical_pubkeys_input; + identical_pubkeys_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + identical_pubkeys_input.fund_vout = 0; + identical_pubkeys_input.fund_amount = Amount::CreateBySatoshiAmount(10000000); + identical_pubkeys_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + identical_pubkeys_input.remote_fund_pubkey = LOCAL_FUND_PUBKEY; // Same as local! + identical_pubkeys_input.max_witness_length = 220; + identical_pubkeys_input.input_serial_id = 1; + + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; + local_params.dlc_inputs_info = {identical_pubkeys_input}; // Identical pubkeys + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = Amount::CreateBySatoshiAmount(5000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {}; + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(5000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + std::vector outcomes = { + {Amount::CreateBySatoshiAmount(7000000), + Amount::CreateBySatoshiAmount(3000000)}, + {Amount::CreateBySatoshiAmount(3000000), + Amount::CreateBySatoshiAmount(7000000)}}; + + // Should throw exception for identical pubkeys + EXPECT_THROW( + DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2), + CfdException); +} + +// Test for dust limit validation +TEST(DlcManagerTest, CreateSplicedDlcTransactionsDustLimitFails) { + // Create DLC input with very small amount (below dust limit) + DlcInputInfo dust_input; + dust_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + dust_input.fund_vout = 0; + dust_input.fund_amount = Amount::CreateBySatoshiAmount(500); // Below 546 sat dust limit + dust_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + dust_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + dust_input.max_witness_length = 220; + dust_input.input_serial_id = 1; + + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; + local_params.dlc_inputs_info = {dust_input}; // Dust amount input + local_params.input_amount = Amount::CreateBySatoshiAmount(0); // No regular inputs + local_params.collateral = Amount::CreateBySatoshiAmount(200); // Very small collateral + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {}; + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(200); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + std::vector outcomes = { + {Amount::CreateBySatoshiAmount(300), + Amount::CreateBySatoshiAmount(100)}, + {Amount::CreateBySatoshiAmount(100), + Amount::CreateBySatoshiAmount(300)}}; + + // Should throw exception for dust amount + EXPECT_THROW( + DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2), + CfdException); +} + +// Test for remote party DLC input validation +TEST(DlcManagerTest, CreateSplicedDlcTransactionsRemoteValidationFails) { + // Create valid local DLC input + DlcInputInfo valid_local_input; + valid_local_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + valid_local_input.fund_vout = 0; + valid_local_input.fund_amount = Amount::CreateBySatoshiAmount(10000000); + valid_local_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + valid_local_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + valid_local_input.max_witness_length = 220; + valid_local_input.input_serial_id = 1; + + // Create invalid remote DLC input (zero amount) + DlcInputInfo invalid_remote_input; + invalid_remote_input.fund_txid = + Txid("7fc895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19c"); + invalid_remote_input.fund_vout = 1; + invalid_remote_input.fund_amount = Amount::CreateBySatoshiAmount(0); // Zero amount + invalid_remote_input.local_fund_pubkey = LOCAL_FUND_PUBKEY2; + invalid_remote_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY2; + invalid_remote_input.max_witness_length = 220; + invalid_remote_input.input_serial_id = 2; + + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; + local_params.dlc_inputs_info = {valid_local_input}; // Valid local input + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = Amount::CreateBySatoshiAmount(5000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {invalid_remote_input}; // Invalid remote input + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(5000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + std::vector outcomes = { + {Amount::CreateBySatoshiAmount(7000000), + Amount::CreateBySatoshiAmount(3000000)}, + {Amount::CreateBySatoshiAmount(3000000), + Amount::CreateBySatoshiAmount(7000000)}}; + + // Should throw exception for zero amount in remote DLC input + EXPECT_THROW( + DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2), + CfdException); +} + +// Test for multiple DLC inputs validation +TEST(DlcManagerTest, CreateSplicedDlcTransactionsMultipleInputsValidation) { + // Create multiple DLC inputs, one valid and one invalid + DlcInputInfo valid_input; + valid_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + valid_input.fund_vout = 0; + valid_input.fund_amount = Amount::CreateBySatoshiAmount(10000000); + valid_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + valid_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + valid_input.max_witness_length = 220; + valid_input.input_serial_id = 1; + + DlcInputInfo invalid_input; + invalid_input.fund_txid = + Txid("7fc895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19c"); + invalid_input.fund_vout = 1; + invalid_input.fund_amount = Amount::CreateBySatoshiAmount(5000000); + invalid_input.local_fund_pubkey = LOCAL_FUND_PUBKEY2; + invalid_input.remote_fund_pubkey = LOCAL_FUND_PUBKEY2; // Same as local - invalid! + invalid_input.max_witness_length = 220; + invalid_input.input_serial_id = 2; + + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; + local_params.dlc_inputs_info = {valid_input, invalid_input}; // Mixed valid/invalid + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = Amount::CreateBySatoshiAmount(7000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {}; + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(3000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + std::vector outcomes = { + {Amount::CreateBySatoshiAmount(8000000), + Amount::CreateBySatoshiAmount(2000000)}, + {Amount::CreateBySatoshiAmount(2000000), + Amount::CreateBySatoshiAmount(8000000)}}; + + // Should throw exception for identical pubkeys in one of the inputs + EXPECT_THROW( + DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2), + CfdException); +} + +// Test for validation passing with properly formatted inputs +TEST(DlcManagerTest, CreateSplicedDlcTransactionsValidationPasses) { + // Create valid DLC inputs for both parties + DlcInputInfo valid_local_input; + valid_local_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + valid_local_input.fund_vout = 0; + valid_local_input.fund_amount = Amount::CreateBySatoshiAmount(10000000); + valid_local_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + valid_local_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + valid_local_input.max_witness_length = 220; + valid_local_input.input_serial_id = 1; + + DlcInputInfo valid_remote_input; + valid_remote_input.fund_txid = + Txid("7fc895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19c"); + valid_remote_input.fund_vout = 1; + valid_remote_input.fund_amount = Amount::CreateBySatoshiAmount(8000000); + valid_remote_input.local_fund_pubkey = LOCAL_FUND_PUBKEY2; + valid_remote_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY2; // Different from local + valid_remote_input.max_witness_length = 220; + valid_remote_input.input_serial_id = 2; + + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = {}; + local_params.dlc_inputs_info = {valid_local_input}; // Valid local input + local_params.input_amount = Amount::CreateBySatoshiAmount(0); + local_params.collateral = Amount::CreateBySatoshiAmount(5000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = {}; + remote_params.dlc_inputs_info = {valid_remote_input}; // Valid remote input + remote_params.input_amount = Amount::CreateBySatoshiAmount(0); + remote_params.collateral = Amount::CreateBySatoshiAmount(5000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + std::vector outcomes = { + {Amount::CreateBySatoshiAmount(7000000), + Amount::CreateBySatoshiAmount(3000000)}, + {Amount::CreateBySatoshiAmount(3000000), + Amount::CreateBySatoshiAmount(7000000)}}; + + // Should pass validation and create valid DLC transactions + EXPECT_NO_THROW( + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2)); + + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2); + + // Verify the transaction was created successfully + EXPECT_EQ(2, dlc_txs.fund_transaction.GetTransaction().GetTxInCount()); // Two DLC inputs + EXPECT_EQ(outcomes.size(), dlc_txs.cets.size()); + + // Verify both DLC inputs are present + bool local_input_found = false, remote_input_found = false; + for (uint32_t i = 0; i < dlc_txs.fund_transaction.GetTransaction().GetTxInCount(); i++) { + auto tx_in = dlc_txs.fund_transaction.GetTransaction().GetTxIn(i); + if (tx_in.GetTxid().GetHex() == valid_local_input.fund_txid.GetHex() && + tx_in.GetVout() == valid_local_input.fund_vout) { + local_input_found = true; + } + if (tx_in.GetTxid().GetHex() == valid_remote_input.fund_txid.GetHex() && + tx_in.GetVout() == valid_remote_input.fund_vout) { + remote_input_found = true; + } + } + EXPECT_TRUE(local_input_found); + EXPECT_TRUE(remote_input_found); +} + +// Test for mixed regular and DLC inputs validation +TEST(DlcManagerTest, CreateSplicedDlcTransactionsMixedInputs) { + // Create a valid DLC input + DlcInputInfo valid_dlc_input; + valid_dlc_input.fund_txid = + Txid("8ec895b4d30adb01e38471ca1019bfc8c3e5fbc98b5216e84d68f0cd9b8de19b"); + valid_dlc_input.fund_vout = 0; + valid_dlc_input.fund_amount = Amount::CreateBySatoshiAmount(8000000); + valid_dlc_input.local_fund_pubkey = LOCAL_FUND_PUBKEY; + valid_dlc_input.remote_fund_pubkey = REMOTE_FUND_PUBKEY; + valid_dlc_input.max_witness_length = 220; + valid_dlc_input.input_serial_id = 1; + + // Create params with both regular and DLC inputs + PartyParams local_params; + local_params.fund_pubkey = LOCAL_FUND_PUBKEY; + local_params.change_script_pubkey = LOCAL_CHANGE_SCRIPT_PUBKEY; + local_params.final_script_pubkey = LOCAL_FINAL_SCRIPT_PUBKEY; + local_params.inputs_info = LOCAL_INPUTS_INFO; // Regular input + local_params.dlc_inputs_info = {valid_dlc_input}; // DLC input + local_params.input_amount = LOCAL_INPUT_AMOUNT; // Regular input amount + local_params.collateral = Amount::CreateBySatoshiAmount(5000000); + local_params.payout_serial_id = 1; + local_params.change_serial_id = 1; + + PartyParams remote_params; + remote_params.fund_pubkey = REMOTE_FUND_PUBKEY; + remote_params.change_script_pubkey = REMOTE_CHANGE_SCRIPT_PUBKEY; + remote_params.final_script_pubkey = REMOTE_FINAL_SCRIPT_PUBKEY; + remote_params.inputs_info = REMOTE_INPUTS_INFO; // Regular input + remote_params.dlc_inputs_info = {}; // No DLC inputs + remote_params.input_amount = REMOTE_INPUT_AMOUNT; // Regular input amount + remote_params.collateral = Amount::CreateBySatoshiAmount(5000000); + remote_params.payout_serial_id = 2; + remote_params.change_serial_id = 2; + + std::vector outcomes = { + {Amount::CreateBySatoshiAmount(7000000), + Amount::CreateBySatoshiAmount(3000000)}, + {Amount::CreateBySatoshiAmount(3000000), + Amount::CreateBySatoshiAmount(7000000)}}; + + // Should work with mixed input types + EXPECT_NO_THROW( + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2)); + + auto dlc_txs = DlcManager::CreateSplicedDlcTransactions( + outcomes, local_params, remote_params, REFUND_LOCKTIME, 2); + + // Verify the transaction has both regular and DLC inputs + // 1 regular + 1 DLC from local, 1 regular from remote = 3 total + EXPECT_EQ(3, dlc_txs.fund_transaction.GetTransaction().GetTxInCount()); + EXPECT_EQ(outcomes.size(), dlc_txs.cets.size()); +}