Skip to content

feat: single funded contracts#3

Draft
bennyhodl wants to merge 3 commits into
masterfrom
single-funded-dlc
Draft

feat: single funded contracts#3
bennyhodl wants to merge 3 commits into
masterfrom
single-funded-dlc

Conversation

@bennyhodl
Copy link
Copy Markdown
Owner

Single Funded DLC Implementation - Rust DLC

Overview

This document explains the implementation of single-funded Discreet Log Contracts (DLCs) in Rust DLC, where only one party provides collateral and covers all transaction fees, while the other party (acceptor) contributes no inputs or fees.

Problem Statement

In the existing Rust DLC implementation, both parties in a DLC split the fees for:

  • Funding transaction fees
  • Contract Execution Transaction (CET) fees
  • Refund transaction fees

This dual-funding model requires both parties to provide inputs and pay their portion of fees. However, there are use cases where only one party (the offeror) should provide all collateral and cover all fees, while the acceptor provides no inputs.

Solution Design

The solution implements a fee distribution model based on collateral contribution:

Fee Distribution Logic

  1. Zero Collateral (Unfunded Party): If own_collateral == 0, the party pays no fees and provides no inputs
  2. Full Collateral (Single-Funded): If own_collateral == total_collateral, the party pays all fees
  3. Partial Collateral (Dual-Funded): If 0 < own_collateral < total_collateral, fees are split between parties (existing behavior)

Key Changes

1. DLC Manager Changes (dlc-manager/src/)

Files Modified:

  • channel_updater.rs
  • contract_updater.rs
  • utils.rs

Core Changes:

  • Added total_collateral parameter to get_party_params() function calls
  • Implemented get_appr_required_amount() function with conditional fee logic
  • Updated fee calculation based on collateral contribution ratio
fn get_appr_required_amount(
    total_collateral: Amount,
    own_collateral: Amount,
    fee_rate: u64,
) -> Result<Amount, Error> {
    let appr_required_amount = if own_collateral == Amount::ZERO {
        // No collateral = no fees
        Amount::ZERO
    } else if own_collateral == total_collateral {
        // Full collateral = full fees
        own_collateral + get_common_fee(fee_rate)? + dlc::util::weight_to_fee(124, fee_rate)?
    } else {
        // Partial collateral = split fees (existing behavior)
        own_collateral + get_half_common_fee(fee_rate)? + dlc::util::weight_to_fee(124, fee_rate)?
    };
    Ok(appr_required_amount)
}

2. DLC Core Changes (dlc/src/lib.rs)

Core Changes:

  • Modified PartyParams::get_change_output_and_fees() to accept total_collateral parameter
  • Updated base weight calculation for funding and CET transactions
  • Added early return for zero-collateral parties

Key Logic:

// Early return for unfunded parties
if self.collateral == Amount::ZERO {
    let change_output = TxOut {
        value: Amount::ZERO,
        script_pubkey: self.change_script_pubkey.clone(),
    };
    return Ok((change_output, Amount::ZERO, Amount::ZERO));
}

// Dynamic base weight calculation
let this_party_fund_base_weight = if self.collateral == total_collateral {
    FUND_TX_BASE_WEIGHT        // Full weight for single-funded
} else {
    FUND_TX_BASE_WEIGHT / 2    // Split weight for dual-funded
};

Implementation Details

Fee Calculation Changes

Funding Transaction Fees

  • Single-funded: Offeror pays FUND_TX_BASE_WEIGHT
  • Dual-funded: Each party pays FUND_TX_BASE_WEIGHT / 2
  • Unfunded: Acceptor pays 0

Contract Execution Transaction (CET) Fees

  • Single-funded: Offeror pays CET_BASE_WEIGHT
  • Dual-funded: Each party pays CET_BASE_WEIGHT / 2
  • Unfunded: Acceptor pays 0

Input Requirements

The get_appr_required_amount() function determines the total amount each party needs to provide:

Party Type Required Amount
Unfunded (acceptor) 0
Single-funded (offeror) collateral + full_fees
Dual-funded collateral + half_fees

Change Output Handling

For unfunded parties, a zero-value change output is created to maintain transaction structure while indicating no funding requirement.

Testing

The implementation includes comprehensive tests covering:

  1. Single-funded DLC scenarios
  2. Unfunded party scenarios
  3. Dual-funded comparison tests
  4. Fee calculation verification

Test Cases Added

#[test]
fn get_appr_required_amount_single_funded_dlc()
fn get_appr_required_amount_unfunded_dlc() 
fn get_appr_required_amount_dual_funded_dlc()
fn get_change_output_and_fees_no_inputs_no_funding()
fn get_change_output_and_fees_single_funded_vs_dual_funded()

Migration Path

Step 1: Rust DLC Integration

  • Merge changes into core Rust DLC library
  • Maintain backward compatibility with existing dual-funded DLCs
  • Update API to accept total_collateral parameter

Step 2: DLC DevKit (DDK) Integration

  • Update DDK to use modified Rust DLC version
  • Implement corresponding changes in DDK manager
  • Update DDK API to support single-funded DLC creation

Backward Compatibility

The implementation maintains full backward compatibility:

  • Existing dual-funded DLCs continue to work unchanged
  • API changes are additive (new parameter with sensible defaults)
  • No breaking changes to existing contracts

Usage Examples

Creating a Single-Funded DLC

let total_collateral = offer_collateral + accept_collateral; // accept_collateral = 0
let (party_params, funding_inputs) = get_party_params(
    secp,
    own_collateral,
    total_collateral,  // New parameter
    fee_rate,
    wallet,
    signer,
    input_amount
)?;

Fee Verification

// For single-funded DLC
assert_eq!(acceptor_fees, Amount::ZERO);
assert_eq!(offeror_fees, full_transaction_fees);

// For dual-funded DLC  
assert_eq!(acceptor_fees + offeror_fees, full_transaction_fees);

Benefits

  1. Flexibility: Supports both single-funded and dual-funded DLCs
  2. Cost Distribution: Allows one party to cover all costs when appropriate
  3. Simplified Onboarding: Reduces barriers for parties who don't want to provide collateral
  4. Backward Compatible: Existing implementations continue to work

Testing and Reproduction

Reproducing Single-Funded DLC with Rust DLC Sample
To test and demonstrate the single-funded DLC functionality, follow these steps using the Rust DLC sample:

  1. Setup Environment
    Follow the standard Rust DLC sample setup:
docker compose --profile oracle up -d
  1. Modify Wallet Creation Script
    Before creating wallets, modify the wallet creation script to prevent Bob from receiving any Bitcoin:
    In scripts/create_wallets.sh:
# Comment out Bob's funding lines
bitcoincli "${opts[@]}" -rpcwallet=bob getnewaddress bech32
bitcoincli "${opts[@]}" generatetoaddress 10 ${newaddress} &> /dev/null

This ensures Bob starts with zero UTXOs.
3. Create Modified Contract Input
Create or modify the contract JSON to set up a single-funded DLC:
In examples/contracts/single_funded_contract.json:

{
  "offer_collateral": 200000000,  // 2 BTC (Alice provides all collateral)
  "accept_collateral": 0,         // 0 BTC (Bob provides no collateral)
  "fee_rate": 2,
  "contract_info": [
    {
      "oracles": {
        "oracle_params": [{
          "oracle_public_key": "...",
          "oracle_nonces": ["..."]
        }]
      },
      "contract_descriptor": {
        "outcome_payouts": [
          {"outcome": "outcome1", "offer_payout": 200000000, "accept_payout": 0},
          {"outcome": "outcome2", "offer_payout": 100000000, "accept_payout": 100000000}
        ]
      }
    }
  ]
}
  1. Modify Block Generation Script
    Update the block generation script to use Bob's wallet (since Bob won't have funds):
    In scripts/generate_blocks.sh:
# Change from alice to bob for address generation
newaddress=$($bitcoincli "${opts[@]}" -rpcwallet=bob getnewaddress bech32)
bitcoincli "${opts[@]}" generatetoaddress 10 ${newaddress} &> /dev/null
  1. Run the Test
    Execute the standard setup:
# Create wallets (Bob will have no funds)
docker compose exec bitcoind /scripts/create_wallets.sh

# Start Alice's instance
cargo run ./examples/configurations/alice.yml

In a different terminal:

# Start Bob's instance
cargo run ./examples/configurations/bob.yml
  1. Create Single-Funded DLC
    From Bob's instance (despite having no funds):
offercontract 02c84f8e151590e718d22e528c55f14c0042c66e68c3f793d7b3b8bf5ae630c648@127.0.0.1:9000 ./examples/contracts/single_funded_contract.json

From Alice's instance:

listoffers
acceptoffer xxxxx

Generate blocks to confirm:

bashdocker compose exec bitcoind /scripts/generate_blocks.sh
  1. Expected Behavior
    With Single-Funded Implementation:
  • ✅ Bob can offer a contract despite having zero UTXOs
  • ✅ Bob contributes no inputs to the funding transaction
  • ✅ Alice pays all fees (approximately 420 sats instead of split 214 sats each)
  • ✅ Contract executes successfully

Without Single-Funded Implementation (Previous Behavior):

  • ❌ Bob's offer would fail due to insufficient funds for fees
  • ❌ Even with zero collateral, Bob would need ~214 sats for fee contribution
  • ❌ Contract creation would be rejected

Key Observations

Fee Distribution: Alice now pays the full transaction fee (~420 sats) instead of the previous split model (~214 sats each)
Input Requirements: Bob provides zero inputs while Alice provides sufficient inputs to cover:

Her collateral (2 BTC)
Full funding transaction fees
Full CET/refund transaction fees

Zero-Value Outputs: Bob's change output will have zero value, indicating no funding contribution
Backward Compatibility: Dual-funded DLCs continue to work with the existing fee-splitting model

Integration and Release Strategy

Phase 1: Rust DLC Core Integration

Immediate Priority: This PR must be merged into Rust DLC first due to dependency architecture and compatibility requirements.

Why Rust DLC First:

  • DLC DevKit (DDK) uses Rust DLC as a dependency
  • Core changes in dlc/src/lib.rs (specifically PartyParams::get_change_output_and_fees()) must be available in the published crate
  • DDK cannot reference unpublished Rust DLC changes in production

Changes Included:

  • Core DLC library modifications (dlc/src/lib.rs)
  • DLC Manager changes (dlc-manager/src/)
  • Full single-funded DLC functionality
  • Comprehensive test suite

Phase 2: Rust DLC Release (v0.8)

Critical Requirement: A new Rust DLC release (e.g., v0.8) must be published after merging.

Why New Release is Required:

  • Current crates.io version of Rust DLC uses legacy types
  • DDK was forked before Rust DLC adopted bitcoin::Amount throughout the codebase
  • DDK still uses older type systems that are incompatible with current Rust DLC master
  • New release will include:
    • bitcoin::Amount standardization
    • Single-funded DLC functionality
    • Updated get_change_output_and_fees() API

Phase 3: DLC DevKit Integration

Post-Release Integration: Only after Rust DLC v0.8 is published can DDK be updated.

DDK Integration Requirements:

  • Update DDK dependency to use Rust DLC v0.8 from crates.io
  • Implement corresponding DLC Manager changes in DDK
  • Update DDK APIs to support single-funded DLC creation
  • Migrate from legacy types to bitcoin::Amount throughout DDK

Current Workaround for Development:

  • DDK single-funded DLC PR branch can use git dependency to Rust DLC
  • This enables development and testing but prevents crates.io publication
  • Production release requires published Rust DLC dependency
# Development (DDK PR branch)
[dependencies]
dlc = { git = "https://github.com/p2pderivatives/rust-dlc.git", branch = "single-funded" }

# Production (after Rust DLC v0.8 release)
[dependencies]
dlc = "0.8"

Timeline and Dependencies

1. Rust DLC PR Review & Merge
   ↓
2. Rust DLC v0.8 Release (includes bitcoin::Amount + single-funded)
   ↓  
3. DDK Update Dependencies & Integration
   ↓
4. DDK Master Merge & Release

Blocking Factors

Cannot proceed with DDK production integration until:

  • Rust DLC single-funded changes are merged
  • Rust DLC v0.8 is released on crates.io
  • DDK can reference stable, published version

Current Development Status:

  • ✅ Rust DLC implementation complete (this PR)
  • ✅ DDK branch development possible (using git dependency)
  • ❌ DDK production release blocked on Rust DLC release
  • ❌ Crates.io publication blocked on stable dependencies

@bennyhodl bennyhodl force-pushed the single-funded-dlc branch 5 times, most recently from ef7181e to e389637 Compare July 9, 2025 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant