Lucas Soriano del Pino
3 years ago
3 changed files with 1008 additions and 999 deletions
File diff suppressed because it is too large
@ -0,0 +1,987 @@ |
|||
use crate::{oracle, Interval}; |
|||
|
|||
use anyhow::{bail, Context, Result}; |
|||
use bdk::bitcoin::hashes::hex::ToHex; |
|||
use bdk::bitcoin::hashes::Hash; |
|||
use bdk::bitcoin::util::bip143::SigHashCache; |
|||
use bdk::bitcoin::util::psbt::{Global, PartiallySignedTransaction}; |
|||
use bdk::bitcoin::{ |
|||
Address, Amount, OutPoint, PublicKey, Script, SigHash, SigHashType, Transaction, TxIn, TxOut, |
|||
}; |
|||
use bdk::database::BatchDatabase; |
|||
use bdk::descriptor::Descriptor; |
|||
use bdk::miniscript::descriptor::Wsh; |
|||
use bdk::miniscript::DescriptorTrait; |
|||
use bdk::wallet::AddressIndex; |
|||
use bdk::FeeRate; |
|||
use itertools::Itertools; |
|||
use secp256k1_zkp::{self, schnorrsig, EcdsaAdaptorSignature, SecretKey, Signature, SECP256K1}; |
|||
use std::collections::HashMap; |
|||
use std::iter::FromIterator; |
|||
|
|||
/// In satoshi per vbyte.
|
|||
const SATS_PER_VBYTE: f64 = 1.0; |
|||
|
|||
/// Static script to be used to create lock tx
|
|||
const DUMMY_2OF2_MULITISIG: &str = |
|||
"0020b5aa99ed7e0fa92483eb045ab8b7a59146d4d9f6653f21ba729b4331895a5b46"; |
|||
|
|||
pub trait WalletExt { |
|||
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams>; |
|||
} |
|||
|
|||
impl<B, D> WalletExt for bdk::Wallet<B, D> |
|||
where |
|||
D: BatchDatabase, |
|||
{ |
|||
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams> { |
|||
let mut builder = self.build_tx(); |
|||
builder |
|||
.ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic) |
|||
.fee_rate(FeeRate::from_sat_per_vb(1.0)) |
|||
.add_recipient( |
|||
DUMMY_2OF2_MULITISIG |
|||
.parse() |
|||
.expect("Should be valid script"), |
|||
amount.as_sat(), |
|||
); |
|||
let (lock_psbt, _) = builder.finish()?; |
|||
let address = self.get_address(AddressIndex::New)?.address; |
|||
Ok(PartyParams { |
|||
lock_psbt, |
|||
identity_pk, |
|||
lock_amount: amount, |
|||
address, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
pub fn create_cfd_transactions( |
|||
(maker, maker_punish_params): (PartyParams, PunishParams), |
|||
(taker, taker_punish_params): (PartyParams, PunishParams), |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
refund_timelock: u32, |
|||
payouts: Vec<Payout>, |
|||
identity_sk: SecretKey, |
|||
) -> Result<CfdTransactions> { |
|||
let lock_tx = lock_transaction( |
|||
maker.lock_psbt.clone(), |
|||
taker.lock_psbt.clone(), |
|||
maker.identity_pk, |
|||
taker.identity_pk, |
|||
maker.lock_amount + taker.lock_amount, |
|||
); |
|||
|
|||
build_cfds( |
|||
lock_tx, |
|||
( |
|||
maker.identity_pk, |
|||
maker.lock_amount, |
|||
maker.address, |
|||
maker_punish_params, |
|||
), |
|||
( |
|||
taker.identity_pk, |
|||
taker.lock_amount, |
|||
taker.address, |
|||
taker_punish_params, |
|||
), |
|||
oracle_pk, |
|||
refund_timelock, |
|||
payouts, |
|||
identity_sk, |
|||
) |
|||
} |
|||
|
|||
pub fn renew_cfd_transactions( |
|||
lock_tx: PartiallySignedTransaction, |
|||
(maker_pk, maker_lock_amount, maker_address, maker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
(taker_pk, taker_lock_amount, taker_address, taker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
refund_timelock: u32, |
|||
payouts: Vec<Payout>, |
|||
identity_sk: SecretKey, |
|||
) -> Result<CfdTransactions> { |
|||
build_cfds( |
|||
lock_tx, |
|||
( |
|||
maker_pk, |
|||
maker_lock_amount, |
|||
maker_address, |
|||
maker_punish_params, |
|||
), |
|||
( |
|||
taker_pk, |
|||
taker_lock_amount, |
|||
taker_address, |
|||
taker_punish_params, |
|||
), |
|||
oracle_pk, |
|||
refund_timelock, |
|||
payouts, |
|||
identity_sk, |
|||
) |
|||
} |
|||
|
|||
fn build_cfds( |
|||
lock_tx: PartiallySignedTransaction, |
|||
(maker_pk, maker_lock_amount, maker_address, maker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
(taker_pk, taker_lock_amount, taker_address, taker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
refund_timelock: u32, |
|||
payouts: Vec<Payout>, |
|||
identity_sk: SecretKey, |
|||
) -> Result<CfdTransactions> { |
|||
/// Relative timelock used for every CET.
|
|||
///
|
|||
/// This is used to allow parties to punish the publication of revoked commitment transactions.
|
|||
///
|
|||
/// TODO: Should this be an argument to this function?
|
|||
const CET_TIMELOCK: u32 = 12; |
|||
|
|||
let commit_tx = CommitTransaction::new( |
|||
&lock_tx.global.unsigned_tx, |
|||
( |
|||
maker_pk, |
|||
maker_punish_params.revocation_pk, |
|||
maker_punish_params.publish_pk, |
|||
), |
|||
( |
|||
taker_pk, |
|||
taker_punish_params.revocation_pk, |
|||
taker_punish_params.publish_pk, |
|||
), |
|||
) |
|||
.context("cannot build commit tx")?; |
|||
|
|||
let identity_pk = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &identity_sk); |
|||
let commit_encsig = if identity_pk == maker_pk.key { |
|||
commit_tx.encsign(identity_sk, &taker_punish_params.publish_pk) |
|||
} else if identity_pk == taker_pk.key { |
|||
commit_tx.encsign(identity_sk, &maker_punish_params.publish_pk) |
|||
} else { |
|||
bail!("identity sk does not belong to taker or maker") |
|||
}; |
|||
|
|||
let refund = { |
|||
let tx = RefundTransaction::new( |
|||
&commit_tx, |
|||
refund_timelock, |
|||
&maker_address, |
|||
&taker_address, |
|||
maker_lock_amount, |
|||
taker_lock_amount, |
|||
); |
|||
|
|||
let sighash = tx.sighash().to_message(); |
|||
let sig = SECP256K1.sign(&sighash, &identity_sk); |
|||
|
|||
(tx.inner, sig) |
|||
}; |
|||
|
|||
let cets = payouts |
|||
.into_iter() |
|||
.map(|payout| { |
|||
let cet = ContractExecutionTransaction::new( |
|||
&commit_tx, |
|||
payout.clone(), |
|||
&maker_address, |
|||
&taker_address, |
|||
CET_TIMELOCK, |
|||
)?; |
|||
|
|||
let encsig = cet.encsign(identity_sk, &oracle_pk)?; |
|||
|
|||
Ok((cet.inner, encsig, payout.msg_nonce_pairs)) |
|||
}) |
|||
.collect::<Result<Vec<_>>>() |
|||
.context("cannot build and sign all cets")?; |
|||
|
|||
Ok(CfdTransactions { |
|||
lock: lock_tx, |
|||
commit: (commit_tx.inner, commit_encsig), |
|||
cets, |
|||
refund, |
|||
}) |
|||
} |
|||
|
|||
pub fn lock_descriptor(maker_pk: PublicKey, taker_pk: PublicKey) -> Descriptor<PublicKey> { |
|||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; |
|||
|
|||
let maker_pk = ToHex::to_hex(&maker_pk.key); |
|||
let taker_pk = ToHex::to_hex(&taker_pk.key); |
|||
|
|||
let miniscript = MINISCRIPT_TEMPLATE |
|||
.replace("A", &maker_pk) |
|||
.replace("B", &taker_pk); |
|||
|
|||
let miniscript = miniscript.parse().expect("a valid miniscript"); |
|||
|
|||
Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor")) |
|||
} |
|||
|
|||
pub fn commit_descriptor( |
|||
(maker_own_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
(taker_own_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
) -> Descriptor<PublicKey> { |
|||
let maker_own_pk_hash = maker_own_pk.pubkey_hash().as_hash(); |
|||
let maker_own_pk = (&maker_own_pk.key.serialize().to_vec()).to_hex(); |
|||
let maker_publish_pk_hash = maker_publish_pk.pubkey_hash().as_hash(); |
|||
let maker_rev_pk_hash = maker_rev_pk.pubkey_hash().as_hash(); |
|||
|
|||
let taker_own_pk_hash = taker_own_pk.pubkey_hash().as_hash(); |
|||
let taker_own_pk = (&taker_own_pk.key.serialize().to_vec()).to_hex(); |
|||
let taker_publish_pk_hash = taker_publish_pk.pubkey_hash().as_hash(); |
|||
let taker_rev_pk_hash = taker_rev_pk.pubkey_hash().as_hash(); |
|||
|
|||
// raw script:
|
|||
// or(and(pk(maker_own_pk),pk(taker_own_pk)),or(and(pk(maker_own_pk),and(pk(taker_publish_pk),
|
|||
// pk(taker_rev_pk))),and(pk(taker_own_pk),and(pk(maker_publish_pk),pk(maker_rev_pk)))))
|
|||
let full_script = format!("wsh(c:andor(pk({maker_own_pk}),pk_k({taker_own_pk}),or_i(and_v(v:pkh({maker_own_pk_hash}),and_v(v:pkh({taker_publish_pk_hash}),pk_h({taker_rev_pk_hash}))),and_v(v:pkh({taker_own_pk_hash}),and_v(v:pkh({maker_publish_pk_hash}),pk_h({maker_rev_pk_hash}))))))", |
|||
maker_own_pk = maker_own_pk, |
|||
taker_own_pk = taker_own_pk, |
|||
maker_own_pk_hash = maker_own_pk_hash, |
|||
taker_own_pk_hash = taker_own_pk_hash, |
|||
taker_publish_pk_hash = taker_publish_pk_hash, |
|||
taker_rev_pk_hash = taker_rev_pk_hash, |
|||
maker_publish_pk_hash = maker_publish_pk_hash, |
|||
maker_rev_pk_hash = maker_rev_pk_hash |
|||
); |
|||
|
|||
full_script.parse().expect("a valid miniscript") |
|||
} |
|||
|
|||
pub fn spending_tx_sighash( |
|||
spending_tx: &Transaction, |
|||
spent_descriptor: &Descriptor<PublicKey>, |
|||
spent_amount: Amount, |
|||
) -> secp256k1_zkp::Message { |
|||
let sighash = SigHashCache::new(spending_tx).signature_hash( |
|||
0, |
|||
&spent_descriptor.script_code(), |
|||
spent_amount.as_sat(), |
|||
SigHashType::All, |
|||
); |
|||
sighash.to_message() |
|||
} |
|||
|
|||
pub fn finalize_spend_transaction( |
|||
mut tx: Transaction, |
|||
spent_descriptor: &Descriptor<PublicKey>, |
|||
(pk_0, sig_0): (PublicKey, Signature), |
|||
(pk_1, sig_1): (PublicKey, Signature), |
|||
) -> Result<Transaction> { |
|||
let satisfier = HashMap::from_iter(vec![ |
|||
(pk_0, (sig_0, SigHashType::All)), |
|||
(pk_1, (sig_1, SigHashType::All)), |
|||
]); |
|||
|
|||
let input = tx |
|||
.input |
|||
.iter_mut() |
|||
.exactly_one() |
|||
.expect("all spend transactions to have one input"); |
|||
spent_descriptor.satisfy(input, satisfier)?; |
|||
|
|||
Ok(tx) |
|||
} |
|||
|
|||
pub fn punish_transaction( |
|||
commit_descriptor: &Descriptor<PublicKey>, |
|||
address: &Address, |
|||
encsig: EcdsaAdaptorSignature, |
|||
sk: SecretKey, |
|||
revocation_them_sk: SecretKey, |
|||
pub_them_pk: PublicKey, |
|||
revoked_commit_tx: &Transaction, |
|||
) -> Result<Transaction> { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 219.5 + (3.0 * 3.0) / 4.0; |
|||
|
|||
let input = revoked_commit_tx |
|||
.input |
|||
.clone() |
|||
.into_iter() |
|||
.exactly_one() |
|||
.context("commit transaction inputs != 1")?; |
|||
|
|||
let publish_them_sk = input |
|||
.witness |
|||
.iter() |
|||
.filter_map(|elem| { |
|||
let elem = elem.as_slice(); |
|||
Signature::from_der(&elem[..elem.len() - 1]).ok() |
|||
}) |
|||
.find_map(|sig| encsig.recover(SECP256K1, &sig, &pub_them_pk.key).ok()) |
|||
.context("could not recover publish sk from commit tx")?; |
|||
|
|||
let commit_outpoint = revoked_commit_tx |
|||
.outpoint(&commit_descriptor.script_pubkey()) |
|||
.expect("to find commit output in commit tx"); |
|||
let commit_amount = revoked_commit_tx.output[commit_outpoint.vout as usize].value; |
|||
|
|||
let mut punish_tx = { |
|||
let output = TxOut { |
|||
value: commit_amount, |
|||
script_pubkey: address.script_pubkey(), |
|||
}; |
|||
let mut tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![TxIn { |
|||
previous_output: commit_outpoint, |
|||
..Default::default() |
|||
}], |
|||
output: vec![output], |
|||
}; |
|||
|
|||
let fee = SIGNED_VBYTES * SATS_PER_VBYTE; |
|||
tx.output[0].value = commit_amount - fee as u64; |
|||
|
|||
tx |
|||
}; |
|||
|
|||
let sighash = SigHashCache::new(&punish_tx).signature_hash( |
|||
0, |
|||
&commit_descriptor.script_code(), |
|||
commit_amount, |
|||
SigHashType::All, |
|||
); |
|||
|
|||
let satisfier = { |
|||
let pk = { |
|||
let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &sk); |
|||
PublicKey { |
|||
compressed: true, |
|||
key, |
|||
} |
|||
}; |
|||
let pk_hash = pk.pubkey_hash().as_hash(); |
|||
let sig_sk = SECP256K1.sign(&sighash.to_message(), &sk); |
|||
|
|||
let pub_them_pk_hash = pub_them_pk.pubkey_hash().as_hash(); |
|||
let sig_pub_them = SECP256K1.sign(&sighash.to_message(), &publish_them_sk); |
|||
|
|||
let rev_them_pk = { |
|||
let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &revocation_them_sk); |
|||
PublicKey { |
|||
compressed: true, |
|||
key, |
|||
} |
|||
}; |
|||
let rev_them_pk_hash = rev_them_pk.pubkey_hash().as_hash(); |
|||
let sig_rev_them = SECP256K1.sign(&sighash.to_message(), &revocation_them_sk); |
|||
|
|||
let sighash_all = SigHashType::All; |
|||
HashMap::from_iter(vec![ |
|||
(pk_hash, (pk, (sig_sk, sighash_all))), |
|||
(pub_them_pk_hash, (pub_them_pk, (sig_pub_them, sighash_all))), |
|||
(rev_them_pk_hash, (rev_them_pk, (sig_rev_them, sighash_all))), |
|||
]) |
|||
}; |
|||
|
|||
commit_descriptor.satisfy(&mut punish_tx.input[0], satisfier)?; |
|||
|
|||
Ok(punish_tx) |
|||
} |
|||
|
|||
// NOTE: We have decided to not order any verification utility because
|
|||
// the APIs would be incredibly thin
|
|||
|
|||
#[derive(Clone)] |
|||
pub struct PartyParams { |
|||
pub lock_psbt: PartiallySignedTransaction, |
|||
pub identity_pk: PublicKey, |
|||
pub lock_amount: Amount, |
|||
pub address: Address, |
|||
} |
|||
|
|||
#[derive(Debug, Copy, Clone)] |
|||
pub struct PunishParams { |
|||
pub revocation_pk: PublicKey, |
|||
pub publish_pk: PublicKey, |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub struct CfdTransactions { |
|||
pub lock: PartiallySignedTransaction, |
|||
pub commit: (Transaction, EcdsaAdaptorSignature), |
|||
#[allow(clippy::type_complexity)] // TODO: Introduce type
|
|||
pub cets: Vec<( |
|||
Transaction, |
|||
EcdsaAdaptorSignature, |
|||
Vec<(Vec<u8>, schnorrsig::PublicKey)>, |
|||
)>, |
|||
pub refund: (Transaction, Signature), |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub struct Payout { |
|||
msg_nonce_pairs: Vec<(Vec<u8>, schnorrsig::PublicKey)>, |
|||
maker_amount: Amount, |
|||
taker_amount: Amount, |
|||
} |
|||
|
|||
impl Payout { |
|||
pub fn new( |
|||
interval: Interval, |
|||
nonce_pks: Vec<schnorrsig::PublicKey>, |
|||
maker_amount: Amount, |
|||
taker_amount: Amount, |
|||
) -> Vec<Self> { |
|||
interval |
|||
.as_digits() |
|||
.into_iter() |
|||
.map(|digits| { |
|||
let msg_nonce_pairs = digits |
|||
.to_bytes() |
|||
.into_iter() |
|||
.zip(nonce_pks.clone()) |
|||
.collect(); |
|||
Self { |
|||
msg_nonce_pairs, |
|||
maker_amount, |
|||
taker_amount, |
|||
} |
|||
}) |
|||
.collect() |
|||
} |
|||
|
|||
fn into_txouts(self, maker_address: &Address, taker_address: &Address) -> Vec<TxOut> { |
|||
let txouts = [ |
|||
(self.maker_amount, maker_address), |
|||
(self.taker_amount, taker_address), |
|||
] |
|||
.iter() |
|||
.filter_map(|(amount, address)| { |
|||
let script_pubkey = address.script_pubkey(); |
|||
let dust_limit = script_pubkey.dust_value(); |
|||
(amount >= &dust_limit).then(|| TxOut { |
|||
value: amount.as_sat(), |
|||
script_pubkey, |
|||
}) |
|||
}) |
|||
.collect::<Vec<_>>(); |
|||
|
|||
txouts |
|||
} |
|||
|
|||
/// Subtracts fee fairly from both outputs
|
|||
///
|
|||
/// We need to consider a few cases:
|
|||
/// - If both amounts are >= DUST, they share the fee equally
|
|||
/// - If one amount is < DUST, it set to 0 and the other output needs to cover for the fee.
|
|||
fn with_updated_fee( |
|||
self, |
|||
fee: Amount, |
|||
dust_limit_maker: Amount, |
|||
dust_limit_taker: Amount, |
|||
) -> Result<Self> { |
|||
let maker_amount = self.maker_amount; |
|||
let taker_amount = self.taker_amount; |
|||
|
|||
let mut updated = self; |
|||
match ( |
|||
maker_amount |
|||
.checked_sub(fee / 2) |
|||
.map(|a| a > dust_limit_maker) |
|||
.unwrap_or(false), |
|||
taker_amount |
|||
.checked_sub(fee / 2) |
|||
.map(|a| a > dust_limit_taker) |
|||
.unwrap_or(false), |
|||
) { |
|||
(true, true) => { |
|||
updated.maker_amount -= fee / 2; |
|||
updated.taker_amount -= fee / 2; |
|||
} |
|||
(false, true) => { |
|||
updated.maker_amount = Amount::ZERO; |
|||
updated.taker_amount = taker_amount - (fee + maker_amount); |
|||
} |
|||
(true, false) => { |
|||
updated.maker_amount = maker_amount - (fee + taker_amount); |
|||
updated.taker_amount = Amount::ZERO; |
|||
} |
|||
(false, false) => bail!("Amounts are too small, could not subtract fee."), |
|||
} |
|||
Ok(updated) |
|||
} |
|||
} |
|||
|
|||
/// Compute a signature point for the given oracle public key, announcement nonce public key and
|
|||
/// message.
|
|||
pub fn compute_signature_point( |
|||
oracle_pk: &schnorrsig::PublicKey, |
|||
nonce_pk: &schnorrsig::PublicKey, |
|||
msg: &[u8], |
|||
) -> Result<secp256k1_zkp::PublicKey> { |
|||
fn schnorr_pubkey_to_pubkey(pk: &schnorrsig::PublicKey) -> Result<secp256k1_zkp::PublicKey> { |
|||
let mut buf = Vec::<u8>::with_capacity(33); |
|||
buf.push(0x02); |
|||
buf.extend(&pk.serialize()); |
|||
Ok(secp256k1_zkp::PublicKey::from_slice(&buf)?) |
|||
} |
|||
|
|||
let hash = oracle::msg_hash(oracle_pk, nonce_pk, msg); |
|||
let mut oracle_pk = schnorr_pubkey_to_pubkey(oracle_pk)?; |
|||
oracle_pk.mul_assign(SECP256K1, &hash)?; |
|||
let nonce_pk = schnorr_pubkey_to_pubkey(nonce_pk)?; |
|||
Ok(nonce_pk.combine(&oracle_pk)?) |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
struct ContractExecutionTransaction { |
|||
inner: Transaction, |
|||
msg_nonce_pairs: Vec<(Vec<u8>, schnorrsig::PublicKey)>, |
|||
sighash: SigHash, |
|||
commit_descriptor: Descriptor<PublicKey>, |
|||
} |
|||
|
|||
impl ContractExecutionTransaction { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; |
|||
|
|||
fn new( |
|||
commit_tx: &CommitTransaction, |
|||
payout: Payout, |
|||
maker_address: &Address, |
|||
taker_address: &Address, |
|||
relative_timelock_in_blocks: u32, |
|||
) -> Result<Self> { |
|||
let msg_nonce_pairs = payout.msg_nonce_pairs.clone(); |
|||
let commit_input = TxIn { |
|||
previous_output: commit_tx.outpoint(), |
|||
sequence: relative_timelock_in_blocks, |
|||
..Default::default() |
|||
}; |
|||
|
|||
let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; |
|||
fee += commit_tx.fee() as f64; |
|||
let output = payout |
|||
.with_updated_fee( |
|||
Amount::from_sat(fee as u64), |
|||
maker_address.script_pubkey().dust_value(), |
|||
taker_address.script_pubkey().dust_value(), |
|||
)? |
|||
.into_txouts(maker_address, taker_address); |
|||
|
|||
let tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![commit_input], |
|||
output, |
|||
}; |
|||
|
|||
let sighash = SigHashCache::new(&tx).signature_hash( |
|||
0, |
|||
&commit_tx.descriptor.script_code(), |
|||
commit_tx.amount.as_sat(), |
|||
SigHashType::All, |
|||
); |
|||
|
|||
Ok(Self { |
|||
inner: tx, |
|||
msg_nonce_pairs, |
|||
sighash, |
|||
commit_descriptor: commit_tx.descriptor(), |
|||
}) |
|||
} |
|||
|
|||
fn encsign( |
|||
&self, |
|||
sk: SecretKey, |
|||
oracle_pk: &schnorrsig::PublicKey, |
|||
) -> Result<EcdsaAdaptorSignature> { |
|||
let adaptor_point = compute_adaptor_point(oracle_pk, &self.msg_nonce_pairs)?; |
|||
|
|||
Ok(EcdsaAdaptorSignature::encrypt( |
|||
SECP256K1, |
|||
&self.sighash.to_message(), |
|||
&sk, |
|||
&adaptor_point, |
|||
)) |
|||
} |
|||
} |
|||
|
|||
pub fn compute_adaptor_point( |
|||
oracle_pk: &schnorrsig::PublicKey, |
|||
msg_nonce_pairs: &[(Vec<u8>, schnorrsig::PublicKey)], |
|||
) -> Result<secp256k1_zkp::PublicKey> { |
|||
let sig_points = msg_nonce_pairs |
|||
.iter() |
|||
.map(|(msg, nonce_pk)| compute_signature_point(oracle_pk, nonce_pk, msg)) |
|||
.collect::<Result<Vec<_>>>()?; |
|||
let adaptor_point = |
|||
secp256k1_zkp::PublicKey::combine_keys(sig_points.iter().collect::<Vec<_>>().as_slice())?; |
|||
|
|||
Ok(adaptor_point) |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
struct RefundTransaction { |
|||
inner: Transaction, |
|||
sighash: SigHash, |
|||
commit_output_descriptor: Descriptor<PublicKey>, |
|||
} |
|||
|
|||
impl RefundTransaction { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; |
|||
|
|||
fn new( |
|||
commit_tx: &CommitTransaction, |
|||
relative_locktime_in_blocks: u32, |
|||
maker_address: &Address, |
|||
taker_address: &Address, |
|||
maker_amount: Amount, |
|||
taker_amount: Amount, |
|||
) -> Self { |
|||
let commit_input = TxIn { |
|||
previous_output: commit_tx.outpoint(), |
|||
sequence: relative_locktime_in_blocks, |
|||
..Default::default() |
|||
}; |
|||
|
|||
let maker_output = TxOut { |
|||
value: maker_amount.as_sat(), |
|||
script_pubkey: maker_address.script_pubkey(), |
|||
}; |
|||
|
|||
let taker_output = TxOut { |
|||
value: taker_amount.as_sat(), |
|||
script_pubkey: taker_address.script_pubkey(), |
|||
}; |
|||
|
|||
let mut tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![commit_input], |
|||
output: vec![maker_output, taker_output], |
|||
}; |
|||
|
|||
let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; |
|||
fee += commit_tx.fee() as f64; |
|||
tx.output[0].value -= (fee / 2.0) as u64; |
|||
tx.output[1].value -= (fee / 2.0) as u64; |
|||
|
|||
let commit_output_descriptor = commit_tx.descriptor(); |
|||
|
|||
let sighash = SigHashCache::new(&tx).signature_hash( |
|||
0, |
|||
&commit_tx.descriptor().script_code(), |
|||
commit_tx.amount().as_sat(), |
|||
SigHashType::All, |
|||
); |
|||
|
|||
Self { |
|||
inner: tx, |
|||
sighash, |
|||
commit_output_descriptor, |
|||
} |
|||
} |
|||
|
|||
fn sighash(&self) -> SigHash { |
|||
self.sighash |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
struct CommitTransaction { |
|||
inner: Transaction, |
|||
descriptor: Descriptor<PublicKey>, |
|||
amount: Amount, |
|||
sighash: SigHash, |
|||
lock_descriptor: Descriptor<PublicKey>, |
|||
fee: u64, |
|||
} |
|||
|
|||
impl CommitTransaction { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 148.5 + (3.0 * 2.0) / 4.0; |
|||
|
|||
fn new( |
|||
lock_tx: &Transaction, |
|||
(maker_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
(taker_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
) -> Result<Self> { |
|||
let lock_descriptor = lock_descriptor(maker_pk, taker_pk); |
|||
let (lock_outpoint, lock_amount) = { |
|||
let outpoint = lock_tx |
|||
.outpoint(&lock_descriptor.script_pubkey()) |
|||
.context("lock script not found in lock tx")?; |
|||
let amount = lock_tx.output[outpoint.vout as usize].value; |
|||
|
|||
(outpoint, amount) |
|||
}; |
|||
|
|||
let lock_input = TxIn { |
|||
previous_output: lock_outpoint, |
|||
..Default::default() |
|||
}; |
|||
|
|||
let descriptor = commit_descriptor( |
|||
(maker_pk, maker_rev_pk, maker_publish_pk), |
|||
(taker_pk, taker_rev_pk, taker_publish_pk), |
|||
); |
|||
|
|||
let output = TxOut { |
|||
value: lock_amount, |
|||
script_pubkey: descriptor.script_pubkey(), |
|||
}; |
|||
|
|||
let mut inner = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![lock_input], |
|||
output: vec![output], |
|||
}; |
|||
let fee = (Self::SIGNED_VBYTES * SATS_PER_VBYTE as f64) as u64; |
|||
|
|||
let commit_tx_amount = lock_amount - fee as u64; |
|||
inner.output[0].value = commit_tx_amount; |
|||
|
|||
let sighash = SigHashCache::new(&inner).signature_hash( |
|||
0, |
|||
&lock_descriptor.script_code(), |
|||
lock_amount, |
|||
SigHashType::All, |
|||
); |
|||
|
|||
Ok(Self { |
|||
inner, |
|||
descriptor, |
|||
lock_descriptor, |
|||
amount: Amount::from_sat(commit_tx_amount), |
|||
sighash, |
|||
fee, |
|||
}) |
|||
} |
|||
|
|||
fn encsign(&self, sk: SecretKey, publish_them_pk: &PublicKey) -> EcdsaAdaptorSignature { |
|||
EcdsaAdaptorSignature::encrypt( |
|||
SECP256K1, |
|||
&self.sighash.to_message(), |
|||
&sk, |
|||
&publish_them_pk.key, |
|||
) |
|||
} |
|||
|
|||
fn outpoint(&self) -> OutPoint { |
|||
self.inner |
|||
.outpoint(&self.descriptor.script_pubkey()) |
|||
.expect("to find commit output in commit tx") |
|||
} |
|||
|
|||
fn amount(&self) -> Amount { |
|||
self.amount |
|||
} |
|||
|
|||
fn descriptor(&self) -> Descriptor<PublicKey> { |
|||
self.descriptor.clone() |
|||
} |
|||
|
|||
fn fee(&self) -> u64 { |
|||
self.fee |
|||
} |
|||
} |
|||
|
|||
fn lock_transaction( |
|||
maker_psbt: PartiallySignedTransaction, |
|||
taker_psbt: PartiallySignedTransaction, |
|||
maker_pk: PublicKey, |
|||
taker_pk: PublicKey, |
|||
amount: Amount, |
|||
) -> PartiallySignedTransaction { |
|||
let lock_descriptor = lock_descriptor(maker_pk, taker_pk); |
|||
|
|||
let maker_change = maker_psbt |
|||
.global |
|||
.unsigned_tx |
|||
.output |
|||
.into_iter() |
|||
.filter(|out| { |
|||
out.script_pubkey != DUMMY_2OF2_MULITISIG.parse().expect("To be a valid script") |
|||
}) |
|||
.collect::<Vec<_>>(); |
|||
|
|||
let taker_change = taker_psbt |
|||
.global |
|||
.unsigned_tx |
|||
.output |
|||
.into_iter() |
|||
.filter(|out| { |
|||
out.script_pubkey != DUMMY_2OF2_MULITISIG.parse().expect("To be a valid script") |
|||
}) |
|||
.collect::<Vec<_>>(); |
|||
|
|||
let lock_output = TxOut { |
|||
value: amount.as_sat(), |
|||
script_pubkey: lock_descriptor.script_pubkey(), |
|||
}; |
|||
|
|||
let input = vec![ |
|||
maker_psbt.global.unsigned_tx.input, |
|||
taker_psbt.global.unsigned_tx.input, |
|||
] |
|||
.concat(); |
|||
|
|||
let output = std::iter::once(lock_output) |
|||
.chain(maker_change) |
|||
.chain(taker_change) |
|||
.collect(); |
|||
|
|||
let lock_tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input, |
|||
output, |
|||
}; |
|||
|
|||
PartiallySignedTransaction { |
|||
global: Global::from_unsigned_tx(lock_tx).expect("to be unsigned"), |
|||
inputs: vec![maker_psbt.inputs, taker_psbt.inputs].concat(), |
|||
outputs: vec![maker_psbt.outputs, taker_psbt.outputs].concat(), |
|||
} |
|||
} |
|||
|
|||
pub trait TransactionExt { |
|||
fn get_virtual_size(&self) -> f64; |
|||
fn outpoint(&self, script_pubkey: &Script) -> Result<OutPoint>; |
|||
} |
|||
|
|||
impl TransactionExt for Transaction { |
|||
fn get_virtual_size(&self) -> f64 { |
|||
self.get_weight() as f64 / 4.0 |
|||
} |
|||
|
|||
fn outpoint(&self, script_pubkey: &Script) -> Result<OutPoint> { |
|||
let vout = self |
|||
.output |
|||
.iter() |
|||
.position(|out| &out.script_pubkey == script_pubkey) |
|||
.context("script pubkey not found in tx")?; |
|||
|
|||
Ok(OutPoint { |
|||
txid: self.txid(), |
|||
vout: vout as u32, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
trait SigHashExt { |
|||
fn to_message(self) -> secp256k1_zkp::Message; |
|||
} |
|||
|
|||
impl SigHashExt for SigHash { |
|||
fn to_message(self) -> secp256k1_zkp::Message { |
|||
use secp256k1_zkp::bitcoin_hashes::Hash; |
|||
let hash = secp256k1_zkp::bitcoin_hashes::sha256d::Hash::from_inner(*self.as_inner()); |
|||
|
|||
hash.into() |
|||
} |
|||
} |
|||
|
|||
#[cfg(test)] |
|||
mod tests { |
|||
use super::*; |
|||
|
|||
use bdk::bitcoin::Network; |
|||
|
|||
// TODO add proptest for this
|
|||
|
|||
#[test] |
|||
fn test_fee_subtraction_bigger_than_dust() { |
|||
let nonce_pk = "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166" |
|||
.parse() |
|||
.unwrap(); |
|||
let key = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af" |
|||
.parse() |
|||
.unwrap(); |
|||
let dummy_address = Address::p2wpkh(&key, Network::Regtest).unwrap(); |
|||
let dummy_dust_limit = dummy_address.script_pubkey().dust_value(); |
|||
|
|||
let orig_maker_amount = 1000; |
|||
let orig_taker_amount = 1000; |
|||
let payouts = Payout::new( |
|||
Interval::new(0, 10_000).unwrap(), |
|||
vec![nonce_pk; 20], |
|||
Amount::from_sat(orig_maker_amount), |
|||
Amount::from_sat(orig_taker_amount), |
|||
); |
|||
let fee = 100; |
|||
|
|||
for payout in payouts { |
|||
let updated_payout = payout |
|||
.with_updated_fee(Amount::from_sat(fee), dummy_dust_limit, dummy_dust_limit) |
|||
.unwrap(); |
|||
|
|||
assert_eq!( |
|||
updated_payout.maker_amount, |
|||
Amount::from_sat(orig_maker_amount - fee / 2) |
|||
); |
|||
assert_eq!( |
|||
updated_payout.taker_amount, |
|||
Amount::from_sat(orig_taker_amount - fee / 2) |
|||
); |
|||
} |
|||
} |
|||
|
|||
#[test] |
|||
fn test_fee_subtraction_smaller_than_dust() { |
|||
let nonce_pk = "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166" |
|||
.parse() |
|||
.unwrap(); |
|||
let key = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af" |
|||
.parse() |
|||
.unwrap(); |
|||
let dummy_address = Address::p2wpkh(&key, Network::Regtest).unwrap(); |
|||
let dummy_dust_limit = dummy_address.script_pubkey().dust_value(); |
|||
|
|||
let orig_maker_amount = dummy_dust_limit.as_sat() - 1; |
|||
let orig_taker_amount = 1000; |
|||
let payouts = Payout::new( |
|||
Interval::new(0, 10_000).unwrap(), |
|||
vec![nonce_pk; 20], |
|||
Amount::from_sat(orig_maker_amount), |
|||
Amount::from_sat(orig_taker_amount), |
|||
); |
|||
let fee = 100; |
|||
|
|||
for payout in payouts { |
|||
let updated_payout = payout |
|||
.with_updated_fee(Amount::from_sat(fee), dummy_dust_limit, dummy_dust_limit) |
|||
.unwrap(); |
|||
|
|||
assert_eq!(updated_payout.maker_amount, Amount::from_sat(0)); |
|||
assert_eq!( |
|||
updated_payout.taker_amount, |
|||
Amount::from_sat(orig_taker_amount - (fee + orig_maker_amount)) |
|||
); |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue