Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cooperative Refund + Lowball Broadcast #40

Merged
merged 9 commits into from
May 1, 2024
270 changes: 195 additions & 75 deletions src/swaps/bitcoinv2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use bitcoin::{sighash::SighashCache, Network, Sequence, Transaction, TxIn, TxOut
use bitcoin::{Amount, EcdsaSighashType, TapLeafHash, TapSighashType, Txid, XOnlyPublicKey};
use electrum_client::ElectrumApi;
use elements::encode::serialize;
use elements::pset::serialize::Serialize;
use std::ops::{Add, Index};
use std::str::FromStr;

Expand All @@ -31,7 +32,9 @@ use crate::{
use bitcoin::{blockdata::locktime::absolute::LockTime, hashes::hash160};

use super::boltz::SwapType;
use super::boltzv2::{BoltzApiClientV2, ClaimTxResponse, CreateSubmarineResponse, CreateReverseResponse};
use super::boltzv2::{
BoltzApiClientV2, ClaimTxResponse, CreateReverseResponse, CreateSubmarineResponse,
};

use elements::secp256k1_zkp::{
MusigAggNonce, MusigKeyAggCache, MusigPartialSignature, MusigPubNonce, MusigSession,
Expand Down Expand Up @@ -241,12 +244,10 @@ impl BtcSwapScriptV2 {

let taproot_builder = TaprootBuilder::new();

let taproot_builder = taproot_builder
.add_leaf_with_ver(1, self.claim_script(), LeafVersion::TapScript)
?;
let taproot_builder = taproot_builder
.add_leaf_with_ver(1, self.refund_script(), LeafVersion::TapScript)
?;
let taproot_builder =
taproot_builder.add_leaf_with_ver(1, self.claim_script(), LeafVersion::TapScript)?;
let taproot_builder =
taproot_builder.add_leaf_with_ver(1, self.refund_script(), LeafVersion::TapScript)?;

let taproot_spend_info = taproot_builder.finalize(&secp, internal_key).unwrap();

Expand Down Expand Up @@ -448,9 +449,7 @@ impl BtcSwapTxV2 {

let session_id = MusigSessionId::new(&mut thread_rng());

let msg = Message::from_digest_slice(
&Vec::from_hex(&claim_tx_response.transaction_hash)?,
)?;
let msg = Message::from_digest_slice(&Vec::from_hex(&claim_tx_response.transaction_hash)?)?;

// Step 4: Start the Musig2 Signing session
let mut extra_rand = [0u8; 32];
Expand Down Expand Up @@ -534,8 +533,7 @@ impl BtcSwapTxV2 {
0,
&Prevouts::All(&[&self.utxo.1]),
bitcoin::TapSighashType::Default,
)
?;
)?;

let msg = Message::from_digest_slice(claim_tx_taproot_hash.as_byte_array())?;

Expand All @@ -557,24 +555,29 @@ impl BtcSwapTxV2 {
let mut extra_rand = [0u8; 32];
OsRng.fill_bytes(&mut extra_rand);

let (sec_nonce, pub_nonce) = key_agg_cache
.nonce_gen(&secp, session_id, keys.public_key(), msg, Some(extra_rand))
?;
let (sec_nonce, pub_nonce) = key_agg_cache.nonce_gen(
&secp,
session_id,
keys.public_key(),
msg,
Some(extra_rand),
)?;

// Step 7: Get boltz's partail sig
let claim_tx_hex = serialize(&claim_tx).to_lower_hex_string();
let partial_sig_resp = boltz_api
.get_reverse_partial_sig(&swap_id, &preimage, &pub_nonce, &claim_tx_hex)
?;
let partial_sig_resp = boltz_api.get_reverse_partial_sig(
&swap_id,
&preimage,
&pub_nonce,
&claim_tx_hex,
)?;

let boltz_public_nonce =
MusigPubNonce::from_slice(&Vec::from_hex(&partial_sig_resp.pub_nonce)?)
?;
MusigPubNonce::from_slice(&Vec::from_hex(&partial_sig_resp.pub_nonce)?)?;

let boltz_partial_sig = MusigPartialSignature::from_slice(
&Vec::from_hex(&partial_sig_resp.partial_signature)?,
)
?;
let boltz_partial_sig = MusigPartialSignature::from_slice(&Vec::from_hex(
&partial_sig_resp.partial_signature,
)?)?;

// Aggregate Our's and Other's Nonce and start the Musig session.
let agg_nonce = MusigAggNonce::new(&secp, &[boltz_public_nonce, pub_nonce]);
Expand All @@ -590,11 +593,14 @@ impl BtcSwapTxV2 {
self.swap_script.sender_pubkey.inner,
);

assert!(boltz_partial_sig_verify == true);
if !boltz_partial_sig_verify {
return Err(Error::Protocol(
"Invalid partial-sig received from Boltz".to_string(),
));
}
rajarshimaitra marked this conversation as resolved.
Show resolved Hide resolved

let our_partial_sig = musig_session
.partial_sign(&secp, sec_nonce, &keys, &key_agg_cache)
?;
let our_partial_sig =
musig_session.partial_sign(&secp, sec_nonce, &keys, &key_agg_cache)?;

let schnorr_sig = musig_session.partial_sig_agg(&[boltz_partial_sig, our_partial_sig]);

Expand All @@ -611,21 +617,17 @@ impl BtcSwapTxV2 {
witness.push(final_schnorr_sig.to_vec());

claim_tx.input[0].witness = witness;

let claim_tx_hex = serialize(&claim_tx).to_lower_hex_string();
} else {
// If Non-Cooperative claim use the Script Path spending
let leaf_hash =
TapLeafHash::from_script(&self.swap_script.claim_script(), LeafVersion::TapScript);

let sighash = SighashCache::new(claim_tx.clone())
.taproot_script_spend_signature_hash(
0,
&Prevouts::All(&[&self.utxo.1]),
leaf_hash,
TapSighashType::Default,
)
?;
let sighash = SighashCache::new(claim_tx.clone()).taproot_script_spend_signature_hash(
0,
&Prevouts::All(&[&self.utxo.1]),
leaf_hash,
TapSighashType::Default,
)?;

let msg = Message::from_digest_slice(sighash.as_byte_array())?;

Expand Down Expand Up @@ -657,7 +659,12 @@ impl BtcSwapTxV2 {

/// Sign a submarine swap refund transaction.
/// Panics if called on Reverse Swap, Claim type.
pub fn sign_refund(&self, keys: &Keypair, absolute_fees: u64) -> Result<Transaction, Error> {
pub fn sign_refund(
&self,
keys: &Keypair,
absolute_fees: u64,
is_cooperative: Option<(&BoltzApiClientV2, &String)>,
) -> Result<Transaction, Error> {
if self.swap_script.swap_type == SwapType::ReverseSubmarine {
return Err(Error::Protocol(
"Cannot sign refund tx, for a reverse-swap".to_string(),
Expand Down Expand Up @@ -710,52 +717,144 @@ impl BtcSwapTxV2 {
.next()
.unwrap();

let mut spending_tx = Transaction {
let mut refund_tx = Transaction {
version: Version::TWO,
lock_time,
input: vec![input],
output: vec![output],
};

let leaf_hash =
TapLeafHash::from_script(&self.swap_script.refund_script(), LeafVersion::TapScript);
let secp = Secp256k1::new();

let sighash = SighashCache::new(spending_tx.clone())
.taproot_script_spend_signature_hash(
0,
&Prevouts::All(&[&self.utxo.1]),
leaf_hash,
TapSighashType::Default,
)
?;
if let Some((boltz_api, swap_id)) = is_cooperative {
// Start the Musig session

// Step 1: Get the sighash
let refund_tx_taproot_hash = SighashCache::new(refund_tx.clone())
.taproot_key_spend_signature_hash(
0,
&Prevouts::All(&[&self.utxo.1]),
bitcoin::TapSighashType::Default,
)?;

let msg = Message::from_digest_slice(sighash.as_byte_array())?;
let msg = Message::from_digest_slice(refund_tx_taproot_hash.as_byte_array())?;

let sig = Secp256k1::new().sign_schnorr(&msg, &keys);
// Step 2: Get the Public and Secret nonces

let final_sig = Signature {
sig,
hash_ty: TapSighashType::Default,
};
let mut key_agg_cache = self.swap_script.musig_keyagg_cache();

let control_block = self
.swap_script
.taproot_spendinfo()?
.control_block(&(
self.swap_script.refund_script().clone(),
LeafVersion::TapScript,
))
.expect("Control block calculation failed");
let tweak = SecretKey::from_slice(
self.swap_script
.taproot_spendinfo()?
.tap_tweak()
.as_byte_array(),
)?;

let _ = key_agg_cache.pubkey_xonly_tweak_add(&secp, tweak)?;

let session_id = MusigSessionId::new(&mut thread_rng());

let mut extra_rand = [0u8; 32];
OsRng.fill_bytes(&mut extra_rand);

let (sec_nonce, pub_nonce) = key_agg_cache.nonce_gen(
&secp,
session_id,
keys.public_key(),
msg,
Some(extra_rand),
)?;

// Step 7: Get boltz's partail sig
rajarshimaitra marked this conversation as resolved.
Show resolved Hide resolved
let refund_tx_hex = serialize(&refund_tx).to_lower_hex_string();
let partial_sig_resp =
boltz_api.get_submarine_partial_sig(&swap_id, &pub_nonce, &refund_tx_hex)?;

let boltz_public_nonce =
MusigPubNonce::from_slice(&Vec::from_hex(&partial_sig_resp.pub_nonce)?)?;

let boltz_partial_sig = MusigPartialSignature::from_slice(&Vec::from_hex(
&partial_sig_resp.partial_signature,
)?)?;

// Aggregate Our's and Other's Nonce and start the Musig session.
let agg_nonce = MusigAggNonce::new(&secp, &[boltz_public_nonce, pub_nonce]);

let musig_session = MusigSession::new(&secp, &key_agg_cache, agg_nonce, msg);

// Verify the Boltz's sig.
let boltz_partial_sig_verify = musig_session.partial_verify(
&secp,
&key_agg_cache,
boltz_partial_sig,
boltz_public_nonce,
self.swap_script.sender_pubkey.inner,
);

if !boltz_partial_sig_verify {
return Err(Error::Protocol(
"Invalid partial-sig received from Boltz".to_string(),
));
}

let our_partial_sig =
musig_session.partial_sign(&secp, sec_nonce, &keys, &key_agg_cache)?;

let schnorr_sig = musig_session.partial_sig_agg(&[boltz_partial_sig, our_partial_sig]);

let final_schnorr_sig = Signature {
sig: schnorr_sig,
hash_ty: TapSighashType::Default,
};

let output_key = self.swap_script.taproot_spendinfo()?.output_key();

let _ = secp.verify_schnorr(&final_schnorr_sig.sig, &msg, &output_key.to_inner())?;

let mut witness = Witness::new();
witness.push(final_schnorr_sig.to_vec());

let mut witness = Witness::new();
refund_tx.input[0].witness = witness;
} else {
let leaf_hash =
TapLeafHash::from_script(&self.swap_script.refund_script(), LeafVersion::TapScript);

let sighash = SighashCache::new(refund_tx.clone())
.taproot_script_spend_signature_hash(
0,
&Prevouts::All(&[&self.utxo.1]),
leaf_hash,
TapSighashType::Default,
)?;

witness.push(final_sig.to_vec());
witness.push(self.swap_script.refund_script().as_bytes());
witness.push(control_block.serialize());
let msg = Message::from_digest_slice(sighash.as_byte_array())?;

let sig = Secp256k1::new().sign_schnorr(&msg, &keys);

spending_tx.input[0].witness = witness;
let final_sig = Signature {
sig,
hash_ty: TapSighashType::Default,
};

Ok(spending_tx)
let control_block = self
.swap_script
.taproot_spendinfo()?
.control_block(&(
self.swap_script.refund_script().clone(),
LeafVersion::TapScript,
))
.expect("Control block calculation failed");

let mut witness = Witness::new();

witness.push(final_sig.to_vec());
witness.push(self.swap_script.refund_script().as_bytes());
witness.push(control_block.serialize());

refund_tx.input[0].witness = witness;
}

Ok(refund_tx)
}

/// Calculate the size of a transaction.
Expand All @@ -765,18 +864,39 @@ impl BtcSwapTxV2 {
let dummy_abs_fee = 5_000;
let tx = match self.kind {
SwapTxKind::Claim => self.sign_claim(keys, preimage, dummy_abs_fee, None)?, // Can only calculate non-coperative claims
SwapTxKind::Refund => self.sign_refund(keys, dummy_abs_fee)?,
SwapTxKind::Refund => self.sign_refund(keys, dummy_abs_fee, None)?,
};
Ok(tx.vsize())
}
/// Broadcast transaction to the network

/// Broadcast transaction to the network.
///
/// Lowall sending can be enabled with `is_lowball` option and by providing a [BoltzApiClientV2] and a [Chain].
pub fn broadcast(
&self,
signed_tx: &Transaction,
network_config: &ElectrumConfig,
is_lowball: Option<(&BoltzApiClientV2, Chain)>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lowball does not exist on the mainchain. You might want to keep the parameter for interface compatibility (not sure), but definitely ignore it in the function body. Or rename it to broadcast_via_boltz

Copy link
Contributor Author

@rajarshimaitra rajarshimaitra Apr 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realised lowball does not exist for Bitcoin too. Removed lowball from Bitcoin broadcast and added a failure case for liquid if the chain == main..

) -> Result<Txid, Error> {
Ok(network_config
.build_client()?
.transaction_broadcast(signed_tx)?)
if let Some((boltz_api, chain)) = is_lowball {
log::info!("Attempting lowball braodcast");
let tx_hex = serialize(signed_tx).to_lower_hex_string();
let response = boltz_api.broadcast_tx(chain, &tx_hex)?;
let txid = Txid::from_str(
&response
.as_object()
.unwrap()
.get("id")
.unwrap()
.as_str()
.unwrap(),
)?;
log::info!("Broadcasted transaction via Boltz: {}", txid);
return Ok(txid);
} else {
Ok(network_config
.build_client()?
.transaction_broadcast(signed_tx)?)
}
}
}
Loading
Loading