mirror of
https://github.com/conduition/dlctix.git
synced 2026-02-06 16:44:59 +01:00
add funding-close transaction for optimized closure when all players cooperate
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -314,9 +314,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "musig2"
|
||||
version = "0.0.10"
|
||||
version = "0.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0852115edbe44f8e258b61383dfded95a8c8d214a8d10e2711f1353789bf0b23"
|
||||
checksum = "bed08befaac75bfb31ca5e87678c4e8490bcd21d0c98ccb4f12f4065a7567e83"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"hmac",
|
||||
|
||||
@@ -16,7 +16,7 @@ exclude = ["/img"]
|
||||
[dependencies]
|
||||
bitcoin = { version = "0.31.1", default-features = false, features = ["std", "serde"] }
|
||||
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
|
||||
musig2 = { version = "0.0.10", default-features = false, features = ["secp256k1", "rand", "serde"] }
|
||||
musig2 = { version = "0.0.11", default-features = false, features = ["secp256k1", "rand", "serde"] }
|
||||
rand = { version = "0.8.5", default-features = false }
|
||||
secp = { version = "0.2.3", default-features = false, features = ["serde"] }
|
||||
secp256k1 = { version = "0.28.2", default-features = false, features = ["global-context"] }
|
||||
|
||||
76
src/lib.rs
76
src/lib.rs
@@ -745,6 +745,19 @@ impl SignedContract {
|
||||
Ok(split_tx)
|
||||
}
|
||||
|
||||
/// Returns prevout information for the market maker to create and sign a
|
||||
/// funding-close transaction.
|
||||
///
|
||||
/// See [SignedContract::sign_funding_close_tx_input] for more info on the funding-close TX.
|
||||
pub fn funding_close_tx_input_and_prevout(&self) -> (TxIn, TxOut) {
|
||||
let input = TxIn {
|
||||
previous_output: self.dlc.funding_outpoint(),
|
||||
..TxIn::default()
|
||||
};
|
||||
let prevout = self.dlc.funding_output();
|
||||
(input, prevout)
|
||||
}
|
||||
|
||||
/// Returns prevout information for the market maker to create and sign an
|
||||
/// outcome-reclaim transaction.
|
||||
///
|
||||
@@ -886,14 +899,16 @@ impl SignedContract {
|
||||
.input_weight_for_sellback_tx()
|
||||
}
|
||||
|
||||
/// Returns an input weight prediction value for the witnessed input of a split-close TX.
|
||||
/// All split-close TX inputs have the same weight, which is
|
||||
/// Returns an input weight prediction value for the witnessed input of a close TX (any close TX).
|
||||
/// All close TX inputs have the same weight, which is
|
||||
/// [`InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH`].
|
||||
///
|
||||
/// See [SignedContract::sign_funding_close_tx_input] for more info on the funding-close TX.
|
||||
/// See [SignedContract::sign_outcome_close_tx_input] for more info on the outcome-close TX.
|
||||
/// See [SignedContract::sign_split_close_tx_input] for more info on the split-close TX.
|
||||
///
|
||||
/// TODO: this should be a constant alias.
|
||||
pub fn split_close_tx_input_weight(&self) -> InputWeightPrediction {
|
||||
pub fn close_tx_input_weight(&self) -> InputWeightPrediction {
|
||||
InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH
|
||||
}
|
||||
|
||||
@@ -966,6 +981,61 @@ impl SignedContract {
|
||||
)
|
||||
}
|
||||
|
||||
/// Sign a cooperative closing transaction which spends the funding transaction output
|
||||
/// back to the market maker's control.
|
||||
///
|
||||
/// The funding-close TX should be used if all ticket-bearing winners have been paid
|
||||
/// out-of-band, and all players including non-winners are cooperative. Once the winners
|
||||
/// have their payouts, they can send their secret keys to the market maker to let him
|
||||
/// reclaim all the on-chain capital. Non-winners, once they realize they are not
|
||||
/// in a position to earn any money from the DLC, can surrender their secret keys to
|
||||
/// the market maker to cooperate and thereby enable the most efficient possible on-chain
|
||||
/// resolution of the DLC.
|
||||
///
|
||||
/// The market maker can only publish a funding-close TX once all players including non-winners
|
||||
/// have given the market maker their secret keys, which they should only do if they have been
|
||||
/// paid out completely, or won't be paid out ever.
|
||||
pub fn sign_funding_close_tx_input<T: Borrow<TxOut>>(
|
||||
&self,
|
||||
close_tx: &mut Transaction,
|
||||
input_index: usize,
|
||||
prevouts: &Prevouts<T>,
|
||||
market_maker_secret_key: impl Into<Scalar>,
|
||||
player_secret_keys: &BTreeMap<Point, Scalar>,
|
||||
) -> Result<(), Error> {
|
||||
let market_maker_secret_key = market_maker_secret_key.into();
|
||||
if market_maker_secret_key.base_point_mul() != self.dlc.params.market_maker.pubkey {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
// Confirm we're signing the correct input
|
||||
let (mut expected_input, expected_prevout) = self.funding_close_tx_input_and_prevout();
|
||||
|
||||
// The caller can use whatever sequence they want.
|
||||
expected_input.sequence = close_tx.input.get(input_index).ok_or(Error)?.sequence;
|
||||
|
||||
check_input_matches_expected(
|
||||
close_tx,
|
||||
prevouts,
|
||||
input_index,
|
||||
&expected_input,
|
||||
&expected_prevout,
|
||||
)?;
|
||||
|
||||
let funding_spend_info = self.dlc.outcome_tx_build.funding_spend_info();
|
||||
|
||||
let witness = funding_spend_info.witness_tx_close(
|
||||
close_tx,
|
||||
input_index,
|
||||
prevouts,
|
||||
market_maker_secret_key,
|
||||
player_secret_keys,
|
||||
)?;
|
||||
|
||||
close_tx.input[input_index].witness = witness;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sign a cooperative closing transaction which spends the outcome transaction output
|
||||
/// back to the market maker's control.
|
||||
///
|
||||
|
||||
@@ -6,7 +6,7 @@ use bitcoincore_rpc::{jsonrpc::serde_json, Auth, Client as BitcoinClient, RpcApi
|
||||
use serial_test::serial;
|
||||
|
||||
use bitcoin::{
|
||||
blockdata::transaction::{predict_weight, InputWeightPrediction},
|
||||
blockdata::transaction::predict_weight,
|
||||
hashes::Hash,
|
||||
key::TweakedPublicKey,
|
||||
locktime::absolute::LockTime,
|
||||
@@ -791,7 +791,7 @@ fn ticketed_dlc_individual_sellback() {
|
||||
script_pubkey: manager.script_pubkey_market_maker(),
|
||||
value: {
|
||||
let close_tx_weight = predict_weight(
|
||||
[InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH],
|
||||
[manager.contract.close_tx_input_weight()],
|
||||
[P2TR_SCRIPT_PUBKEY_SIZE],
|
||||
);
|
||||
let fee = close_tx_weight * FeeRate::from_sat_per_vb_unchecked(20);
|
||||
@@ -856,7 +856,7 @@ fn ticketed_dlc_all_winners_cooperate() {
|
||||
script_pubkey: manager.script_pubkey_market_maker(),
|
||||
value: {
|
||||
let close_tx_weight = predict_weight(
|
||||
[InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH],
|
||||
[manager.contract.close_tx_input_weight()],
|
||||
[P2TR_SCRIPT_PUBKEY_SIZE],
|
||||
);
|
||||
let fee = close_tx_weight * FeeRate::from_sat_per_vb_unchecked(20);
|
||||
@@ -997,7 +997,7 @@ fn ticketed_dlc_market_maker_reclaims_outcome_tx() {
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn ticketed_dlc_contract_expiry_with_on_chain_resolution() {
|
||||
fn ticketed_dlc_contract_expiry_on_chain_resolution() {
|
||||
let manager = SimulationManager::new();
|
||||
|
||||
// The contract expires, paying out to dave.
|
||||
@@ -1131,7 +1131,7 @@ fn ticketed_dlc_contract_expiry_with_on_chain_resolution() {
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn ticketed_dlc_contract_expiry_cooperative_close() {
|
||||
fn ticketed_dlc_contract_expiry_all_winners_cooperate() {
|
||||
let manager = SimulationManager::new();
|
||||
|
||||
// The contract expires, paying out to dave.
|
||||
@@ -1174,7 +1174,7 @@ fn ticketed_dlc_contract_expiry_cooperative_close() {
|
||||
script_pubkey: manager.script_pubkey_market_maker(),
|
||||
value: {
|
||||
let close_tx_weight = predict_weight(
|
||||
[InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH],
|
||||
[manager.contract.close_tx_input_weight()],
|
||||
[P2TR_SCRIPT_PUBKEY_SIZE],
|
||||
);
|
||||
let fee = close_tx_weight * FeeRate::from_sat_per_vb_unchecked(20);
|
||||
@@ -1202,6 +1202,64 @@ fn ticketed_dlc_contract_expiry_cooperative_close() {
|
||||
.expect("failed to broadcast outcome close TX");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn ticketed_dlc_all_players_cooperate() {
|
||||
let manager = SimulationManager::new();
|
||||
|
||||
// The oracle attests to outcome 0, paying out to Alice Bob and Carol.
|
||||
// Alice, Bob, Carol, and Dave all cooperate.
|
||||
//
|
||||
// Alice, Bob and Carol sell their payout preimages to the market maker,
|
||||
// and surrender their secret signing keys once they receive the payout.
|
||||
// Dave stands to earn nothing, so he surrenders his private key immediately.
|
||||
// The market maker can now sweep the funding output back to his control.
|
||||
// This is the most efficient on-chain recovery path possible.
|
||||
//
|
||||
// It is possible for one of the players to publish the split TX, which
|
||||
// double spends the funding TX, but even if the split TX is confirmed,
|
||||
// the market maker can then immediately sweep the output of the split TX anyway.
|
||||
let (close_tx_input, close_tx_prevout) = manager.contract.funding_close_tx_input_and_prevout();
|
||||
let mut close_tx = Transaction {
|
||||
version: bitcoin::transaction::Version::TWO,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: vec![close_tx_input],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: manager.script_pubkey_market_maker(),
|
||||
value: {
|
||||
let close_tx_weight = predict_weight(
|
||||
[manager.contract.close_tx_input_weight()],
|
||||
[P2TR_SCRIPT_PUBKEY_SIZE],
|
||||
);
|
||||
let fee = close_tx_weight * FeeRate::from_sat_per_vb_unchecked(20);
|
||||
close_tx_prevout.value - fee
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
manager
|
||||
.contract
|
||||
.sign_funding_close_tx_input(
|
||||
&mut close_tx,
|
||||
0, // input index
|
||||
&Prevouts::All(&[close_tx_prevout]),
|
||||
manager.market_maker_seckey,
|
||||
&BTreeMap::from([
|
||||
(manager.alice.player.pubkey, manager.alice.seckey),
|
||||
(manager.bob.player.pubkey, manager.bob.seckey),
|
||||
(manager.carol.player.pubkey, manager.carol.seckey),
|
||||
(manager.dave.player.pubkey, manager.dave.seckey),
|
||||
]),
|
||||
)
|
||||
.expect("failed to sign funding close TX");
|
||||
|
||||
// The close TX can be broadcast immediately.
|
||||
manager
|
||||
.rpc
|
||||
.send_raw_transaction(&close_tx)
|
||||
.expect("failed to broadcast funding close TX");
|
||||
}
|
||||
|
||||
// This stress-test confirms that signing large ticketed DLCs is plausible,
|
||||
// if computationally expensive.
|
||||
#[test]
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use bitcoin::{
|
||||
sighash::{Prevouts, SighashCache},
|
||||
Amount, ScriptBuf, TapSighash, TapSighashType, Transaction, TxOut,
|
||||
Amount, ScriptBuf, TapSighash, TapSighashType, Transaction, TxOut, Witness,
|
||||
};
|
||||
use musig2::KeyAggContext;
|
||||
use secp::Point;
|
||||
use musig2::{CompactSignature, KeyAggContext};
|
||||
use secp::{Point, Scalar};
|
||||
|
||||
use crate::{
|
||||
errors::Error,
|
||||
parties::{MarketMaker, Player},
|
||||
};
|
||||
|
||||
use std::{borrow::Borrow, collections::BTreeMap};
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub(crate) struct FundingSpendInfo {
|
||||
key_agg_ctx: KeyAggContext,
|
||||
@@ -72,4 +74,43 @@ impl FundingSpendInfo {
|
||||
TapSighashType::Default,
|
||||
)
|
||||
}
|
||||
|
||||
/// Derive the witness for a cooperative closing transaction which spends from
|
||||
/// the funding transaction. The market maker must provide the secret keys
|
||||
/// for all of the winning players involved in the whole DLC.
|
||||
pub(crate) fn witness_tx_close<T: Borrow<TxOut>>(
|
||||
&self,
|
||||
close_tx: &Transaction,
|
||||
input_index: usize,
|
||||
prevouts: &Prevouts<T>,
|
||||
market_maker_secret_key: Scalar,
|
||||
player_secret_keys: &BTreeMap<Point, Scalar>,
|
||||
) -> Result<Witness, Error> {
|
||||
let mm_pubkey = market_maker_secret_key.base_point_mul();
|
||||
let sighash = SighashCache::new(close_tx).taproot_key_spend_signature_hash(
|
||||
input_index,
|
||||
prevouts,
|
||||
TapSighashType::Default,
|
||||
)?;
|
||||
|
||||
let ordered_seckeys: Vec<Scalar> = self
|
||||
.key_agg_ctx
|
||||
.pubkeys()
|
||||
.into_iter()
|
||||
.map(|&pubkey| {
|
||||
if pubkey == mm_pubkey {
|
||||
Ok(market_maker_secret_key)
|
||||
} else {
|
||||
player_secret_keys.get(&pubkey).ok_or(Error).copied()
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
|
||||
let group_seckey: Scalar = self.key_agg_ctx.aggregated_seckey(ordered_seckeys)?;
|
||||
|
||||
let signature: CompactSignature = musig2::deterministic::sign_solo(group_seckey, sighash);
|
||||
|
||||
let witness = Witness::from_slice(&[signature.serialize()]);
|
||||
Ok(witness)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user