Skip to content
Open
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
163 changes: 119 additions & 44 deletions ddk-manager/src/contract/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ use std::fmt;
use std::ops::Range;

use bitcoin::Amount;
use ddk_dlc::{DlcTransactions, FeeRule, PartyParams};
use ddk_dlc::{DlcTransactions, FeeRule, FundingRequirements, PartyParams};
use ddk_messages::{OfferDlc, TlvStream};

use super::cet_records::{cet_records, CetContext};
use super::cet_records::{cet_records, CetContext, CetRecord};
use super::contract_info::ContractInfo;
use super::offered_contract::OfferedContract;
use crate::error::Error;
Expand Down Expand Up @@ -65,6 +65,61 @@ impl From<&OfferDlc> for ContractTerms {
}
}

/// How a contract's transactions are built: how the CET fee is split, and how
/// a splicing party's inputs are laid out.
///
/// A new contract is built under [`BuildRule::CURRENT`]. A signed contract is
/// rebuilt under whichever rule in [`BuildRule::SIGNED`] reproduces the
/// funding transaction both parties signed.
#[derive(Clone, Copy, Debug)]
pub struct BuildRule {
/// How the CET fee is split between the parties.
pub fee_rule: FeeRule,
/// How a party's inputs are laid out when the contract spends a splice
/// input: [`PartyParams::spliced`], or [`PartyParams::legacy_spliced`] for
/// a contract built by ddk-dlc 2.0.0-rc.8 and earlier.
pub splice_layout: fn(&PartyParams) -> PartyParams,
}

impl BuildRule {
/// The rule new contracts are built under.
pub const CURRENT: BuildRule = BuildRule {
fee_rule: FeeRule::CounterpartyPayout,
splice_layout: PartyParams::spliced,
};

/// Every rule a signed contract can have been built under, the current
/// rule first. [`PartyParams::legacy_spliced`] differs from the current
/// layout only for a splice with an ordinary input whose witness is larger
/// than a P2WPKH witness, so both fee rules are tried with the current
/// layout before either is tried with the legacy one.
pub const SIGNED: [BuildRule; 4] = [
BuildRule::CURRENT,
BuildRule {
fee_rule: FeeRule::OwnPayoutOnly,
splice_layout: PartyParams::spliced,
},
BuildRule {
fee_rule: FeeRule::CounterpartyPayout,
splice_layout: PartyParams::legacy_spliced,
},
BuildRule {
fee_rule: FeeRule::OwnPayoutOnly,
splice_layout: PartyParams::legacy_spliced,
},
];

/// `params` with their inputs laid out as this rule funds them, in a
/// contract that spends a splice input if `splicing`.
fn lay_out(&self, params: &PartyParams, splicing: bool) -> PartyParams {
if splicing {
(self.splice_layout)(params)
} else {
params.clone()
}
}
}

/// A contract's transactions, with the CETs of each contract info located.
#[derive(Clone)]
pub struct ContractTransactions {
Expand Down Expand Up @@ -144,16 +199,16 @@ impl From<BuildError> for Error {
///
/// The first contract info's payouts give the funding, CET and refund
/// transactions; every further contract info adds its CETs after them. The
/// fee rule is the one the contract was funded under: [`FeeRule::default`]
/// for a new contract, and whichever rule reproduces a signed contract's
/// funding transaction for an existing one.
/// rule is the one the contract was built under: [`BuildRule::CURRENT`] for a
/// new contract, and whichever rule in [`BuildRule::SIGNED`] reproduces a
/// signed contract's funding transaction for an existing one.
pub fn build_contract_transactions(
offer: &PartyParams,
accept: &PartyParams,
contract_infos: &[ContractInfo],
offer_tlvs: &TlvStream,
terms: &ContractTerms,
fee_rule: FeeRule,
rule: BuildRule,
) -> Result<ContractTransactions, BuildError> {
if contract_infos.is_empty() {
return Err(BuildError::Offer(
Expand All @@ -172,47 +227,27 @@ pub fn build_contract_transactions(
record.validate(contract_infos, terms.total_collateral)?;
}

// A record may make a CET pay the accepting party's amount to a longer
// script than its own; the CET fee is reserved for the longest.
let accept_cet_script_len = records
.iter()
.map(|record| record.accept_payout_script_len())
.fold(accept.payout_script_pubkey.len(), usize::max);
let accept_cet_script_len = accept_cet_script_len(&records, accept.payout_script_pubkey.len());

let payouts = contract_infos[0].get_payouts(terms.total_collateral)?;
// A splice input carries a `dlc_input`; when present the funding
// transaction spends the previous contract's 2-of-2 output and must be
// built through the spliced constructor.
let has_dlc_inputs = !offer.dlc_inputs.is_empty() || !accept.dlc_inputs.is_empty();
let mut transactions = if has_dlc_inputs {
ddk_dlc::create_spliced_dlc_transactions_with_fee_rule(
offer,
accept,
&payouts,
terms.refund_locktime,
terms.fee_rate_per_vb,
0,
terms.cet_locktime,
terms.fund_output_serial_id,
terms.contract_flags,
fee_rule,
accept_cet_script_len,
)?
} else {
ddk_dlc::create_dlc_transactions_with_fee_rule(
offer,
accept,
&payouts,
terms.refund_locktime,
terms.fee_rate_per_vb,
0,
terms.cet_locktime,
terms.fund_output_serial_id,
terms.contract_flags,
fee_rule,
accept_cet_script_len,
)?
};
// transaction spends the previous contract's 2-of-2 output, so both
// parties' inputs are laid out as the rule's spliced constructor lays them
// out.
let splicing = !offer.dlc_inputs.is_empty() || !accept.dlc_inputs.is_empty();
let mut transactions = ddk_dlc::create_dlc_transactions_with_fee_rule(
&rule.lay_out(offer, splicing),
&rule.lay_out(accept, splicing),
&payouts,
terms.refund_locktime,
terms.fee_rate_per_vb,
0,
terms.cet_locktime,
terms.fund_output_serial_id,
terms.contract_flags,
rule.fee_rule,
accept_cet_script_len,
)?;

let mut cet_ranges = Vec::with_capacity(contract_infos.len());
cet_ranges.push(0..transactions.cets.len());
Expand Down Expand Up @@ -257,3 +292,43 @@ pub fn build_contract_transactions(
cet_ranges,
})
}

/// What the offering party's inputs must hold to fund `offer`'s side of a
/// contract with an accepting party paid at a script of
/// `accept_payout_script_len` bytes, and the fees that side pays.
///
/// This prices the offering party as [`build_contract_transactions`] funds it
/// for a new contract, so a coin selector can be exact before any accept
/// exists: the CET records on the offer can make a CET pay the accepting
/// party a longer script, and a splice input changes the inputs the funding
/// transaction spends. An offer that funds the whole contract pays for both.
pub fn offer_funding_requirements(
offer: &PartyParams,
accept_payout_script_len: usize,
offer_tlvs: &TlvStream,
terms: &ContractTerms,
) -> Result<FundingRequirements, BuildError> {
let records = cet_records(offer_tlvs)?;
// Only the offering party may contribute a splice input, so the offer's
// own inputs say whether the contract splices.
let rule = BuildRule::CURRENT;
let offer = rule.lay_out(offer, !offer.dlc_inputs.is_empty());
Ok(offer.funding_requirements(
terms.total_collateral,
terms.fee_rate_per_vb,
Amount::ZERO,
offer.payout_script_pubkey.len(),
accept_cet_script_len(&records, accept_payout_script_len),
rule.fee_rule,
)?)
}

/// The longest script a CET pays the accepting party: its payout script of
/// `accept_payout_script_len` bytes, or a longer one a record makes a CET pay
/// in its place. The CET fee is reserved for it.
fn accept_cet_script_len(records: &[Box<dyn CetRecord>], accept_payout_script_len: usize) -> usize {
records
.iter()
.map(|record| record.accept_payout_script_len())
.fold(accept_payout_script_len, usize::max)
}
8 changes: 4 additions & 4 deletions ddk-manager/src/contract_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bitcoin::script::PushBytesBuf;
use bitcoin::Amount;
use bitcoin::{consensus::Decodable, Script, ScriptBuf, Transaction, Witness};
use ddk_dlc::dlc_input::DlcInputInfo;
use ddk_dlc::{DlcTransactions, FeeRule, PartyParams};
use ddk_dlc::{DlcTransactions, PartyParams};
use ddk_messages::{
oracle_msgs::{OracleAnnouncement, OracleAttestation},
AcceptDlc, FundingSignature, FundingSignatures, OfferDlc, SignDlc, WitnessElement,
Expand All @@ -29,7 +29,7 @@ use crate::{
contract_input::ContractInput,
offered_contract::OfferedContract,
signed_contract::SignedContract,
transactions::{build_contract_transactions, ContractTransactions},
transactions::{build_contract_transactions, BuildRule, ContractTransactions},
AdaptorInfo,
},
conversion_utils::get_tx_input_infos,
Expand Down Expand Up @@ -198,7 +198,7 @@ where
&offered_contract.contract_info,
&offered_contract.tlvs,
&offered_contract.into(),
FeeRule::default(),
BuildRule::CURRENT,
)?;
log_created_transactions(
logger,
Expand Down Expand Up @@ -355,7 +355,7 @@ where
&offered_contract.contract_info,
&offered_contract.tlvs,
&offered_contract.into(),
FeeRule::default(),
BuildRule::CURRENT,
)?;
log_created_transactions(
logger,
Expand Down
9 changes: 6 additions & 3 deletions ddk/src/contract/accept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@ pub fn create_dlc_transactions(
}

/// Rebuilds a signed contract's transactions under the [`ddk_dlc::FeeRule`]
/// whose funding transaction matches `sign`'s contract id.
/// and splice input layout whose funding transaction matches `sign`'s
/// contract id.
///
/// Tries [`ddk_dlc::FeeRule::CounterpartyPayout`] first, then
/// [`ddk_dlc::FeeRule::OwnPayoutOnly`]. Fails when neither rule matches.
/// Tries the current rules first: [`ddk_dlc::FeeRule::CounterpartyPayout`]
/// and [`ddk_dlc::PartyParams::spliced`]. Then
/// [`ddk_dlc::FeeRule::OwnPayoutOnly`], and both fee rules again with
/// [`ddk_dlc::PartyParams::legacy_spliced`]. Fails when no rule matches.
pub fn create_signed_dlc_transactions(
offer: &OfferDlc,
accept: &AcceptDlc,
Expand Down
56 changes: 32 additions & 24 deletions ddk/src/contract/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use bitcoin::consensus::Decodable;
use bitcoin::{Amount, ScriptBuf, Transaction, Witness};
use ddk_dlc::secp256k1_zkp::{All, EcdsaAdaptorSignature, PublicKey, Secp256k1, SecretKey};
use ddk_dlc::{DlcTransactions, FeeRule, PartyParams as DlcPartyParams, TxInputInfo};
use ddk_dlc::{DlcTransactions, PartyParams as DlcPartyParams, TxInputInfo};
use ddk_manager::contract::contract_info::ContractInfo as ExecutionContractInfo;
use ddk_manager::contract::transactions::{build_contract_transactions, ContractTransactions};
use ddk_manager::contract::transactions::{
build_contract_transactions, BuildRule, ContractTransactions,
};
use ddk_messages::{
AcceptDlc, CetAdaptorSignatures, FundingInput, FundingSignatures, OfferDlc, SignDlc,
};
Expand All @@ -30,19 +32,20 @@ pub(crate) fn context_from_messages(
offer: &OfferDlc,
accept: &AcceptDlc,
) -> Result<ContractContext, ContractError> {
context_from_messages_with_fee_rule(offer, accept, FeeRule::default())
context_from_messages_with_rule(offer, accept, BuildRule::CURRENT)
}

/// Rebuilds a signed contract under the [`FeeRule`] whose funding transaction
/// matches the sign message's contract id.
/// Rebuilds a signed contract under the [`BuildRule`] whose funding
/// transaction matches the sign message's contract id.
///
/// The two rules give different funding transactions when one party funds the
/// whole contract. The contract id comes from the funding transaction that
/// both parties signed, so only the rule the contract was built with matches
/// it. [`FeeRule::CounterpartyPayout`] is tried first. If it cannot build the
/// transactions, or its contract id does not match, [`FeeRule::OwnPayoutOnly`]
/// is tried. If neither rule matches, the error from the first rule is
/// returned.
/// The [`ddk_dlc::FeeRule`]s give different funding transactions when one
/// party funds the whole contract, and the splice layouts do when a splice has
/// an ordinary input with a witness larger than a P2WPKH witness. The contract
/// id comes from the funding transaction that both parties signed, so only the
/// rule the contract was built with matches it. The rules in
/// [`BuildRule::SIGNED`] are tried in order: when one cannot build the
/// transactions, or its contract id does not match, the next is tried. If none
/// matches, the error from the current rule is returned.
///
/// Use this only for a contract that is already signed. New contracts use
/// [`context_from_messages`].
Expand All @@ -52,20 +55,22 @@ pub(crate) fn signed_context(
sign: &SignDlc,
) -> Result<ContractContext, ContractError> {
let rebuild = |rule| {
let context = context_from_messages_with_fee_rule(offer, accept, rule)?;
let context = context_from_messages_with_rule(offer, accept, rule)?;
ensure_sign_message(offer, sign, &context)?;
Ok(context)
};
// The inputs of an OwnPayoutOnly contract can be too small for the
// CounterpartyPayout fee, so a build error also moves to the next rule.
rebuild(FeeRule::default())
.or_else(|current_error| rebuild(FeeRule::OwnPayoutOnly).map_err(|_| current_error))
let [current, older @ ..] = BuildRule::SIGNED;
older.into_iter().fold(rebuild(current), |result, rule| {
result.or_else(|current_error| rebuild(rule).map_err(|_| current_error))
})
}

fn context_from_messages_with_fee_rule(
fn context_from_messages_with_rule(
offer: &OfferDlc,
accept: &AcceptDlc,
fee_rule: FeeRule,
rule: BuildRule,
) -> Result<ContractContext, ContractError> {
ensure_protocol_version(offer.protocol_version, ContractError::InvalidOffer)?;
ensure_protocol_version(accept.protocol_version, ContractError::InvalidAccept)?;
Expand Down Expand Up @@ -97,7 +102,7 @@ fn context_from_messages_with_fee_rule(
accept.accept_collateral,
&accept.funding_inputs,
)?;
build_context_with_fee_rule(offer, &accept_params, fee_rule)
build_context_with_rule(offer, &accept_params, rule)
}

/// Rebuilds the contract transactions from an offer and the accepting party's
Expand All @@ -107,13 +112,13 @@ pub(crate) fn build_context(
offer: &OfferDlc,
accept_params: &DlcPartyParams,
) -> Result<ContractContext, ContractError> {
build_context_with_fee_rule(offer, accept_params, FeeRule::default())
build_context_with_rule(offer, accept_params, BuildRule::CURRENT)
}

fn build_context_with_fee_rule(
fn build_context_with_rule(
offer: &OfferDlc,
accept_params: &DlcPartyParams,
fee_rule: FeeRule,
rule: BuildRule,
) -> Result<ContractContext, ContractError> {
let offer_params = dlc_party_params(
offer.funding_pubkey,
Expand All @@ -137,7 +142,7 @@ fn build_context_with_fee_rule(
&execution_infos,
&offer.tlvs,
&offer.into(),
fee_rule,
rule,
)?;
Ok(ContractContext {
execution_infos,
Expand Down Expand Up @@ -580,8 +585,11 @@ mod tests {
previous.output[input.prev_tx_vout as usize].value =
offer.offer_collateral + fund_fee + cet_fee;
input.prev_tx = bitcoin::consensus::serialize(&previous);
let legacy =
context_from_messages_with_fee_rule(&offer, &accept, FeeRule::OwnPayoutOnly).unwrap();
let legacy_rule = BuildRule {
fee_rule: ddk_dlc::FeeRule::OwnPayoutOnly,
..BuildRule::CURRENT
};
let legacy = context_from_messages_with_rule(&offer, &accept, legacy_rule).unwrap();
assert!(context_from_messages(&offer, &accept).is_err());

let secp = Secp256k1::new();
Expand Down
Loading
Loading