add funding-close transaction for optimized closure when all players cooperate

This commit is contained in:
conduition
2024-03-20 18:39:46 +00:00
parent f8ac9eb138
commit 4ed448d5c5
5 changed files with 184 additions and 15 deletions

4
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"] }

View File

@@ -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.
///

View File

@@ -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]

View File

@@ -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)
}
}