4 changed files with 499 additions and 459 deletions
@ -0,0 +1,15 @@ |
|||
use bdk::bitcoin::hashes::Hash; |
|||
use bdk::bitcoin::SigHash; |
|||
|
|||
pub(super) 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() |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
use anyhow::{Context, Result}; |
|||
use bdk::bitcoin::{OutPoint, Script, Transaction}; |
|||
|
|||
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, |
|||
}) |
|||
} |
|||
} |
@ -0,0 +1,441 @@ |
|||
use crate::protocol::sighash_ext::SigHashExt; |
|||
use crate::protocol::transaction_ext::TransactionExt; |
|||
use crate::protocol::{ |
|||
commit_descriptor, compute_adaptor_point, lock_descriptor, Payout, DUMMY_2OF2_MULTISIG, |
|||
}; |
|||
|
|||
use anyhow::{Context, Result}; |
|||
use bdk::bitcoin::util::bip143::SigHashCache; |
|||
use bdk::bitcoin::util::psbt::{Global, PartiallySignedTransaction}; |
|||
use bdk::bitcoin::{ |
|||
Address, Amount, OutPoint, PublicKey, SigHash, SigHashType, Transaction, TxIn, TxOut, |
|||
}; |
|||
use bdk::descriptor::Descriptor; |
|||
use bdk::miniscript::DescriptorTrait; |
|||
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; |
|||
|
|||
pub(super) 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_MULTISIG.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_MULTISIG.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(), |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub(super) 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; |
|||
|
|||
pub(super) 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, |
|||
}) |
|||
} |
|||
|
|||
pub(super) fn encsign( |
|||
&self, |
|||
sk: SecretKey, |
|||
publish_them_pk: &PublicKey, |
|||
) -> EcdsaAdaptorSignature { |
|||
EcdsaAdaptorSignature::encrypt( |
|||
SECP256K1, |
|||
&self.sighash.to_message(), |
|||
&sk, |
|||
&publish_them_pk.key, |
|||
) |
|||
} |
|||
|
|||
pub(super) fn into_inner(self) -> Transaction { |
|||
self.inner |
|||
} |
|||
|
|||
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 |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub(super) 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; |
|||
|
|||
pub(super) 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(), |
|||
}) |
|||
} |
|||
|
|||
pub(super) 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(super) fn into_inner(self) -> Transaction { |
|||
self.inner |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub(super) 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; |
|||
|
|||
pub(super) 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, |
|||
} |
|||
} |
|||
|
|||
pub(super) fn sighash(&self) -> SigHash { |
|||
self.sighash |
|||
} |
|||
|
|||
pub(super) fn into_inner(self) -> Transaction { |
|||
self.inner |
|||
} |
|||
} |
|||
|
|||
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) |
|||
} |
Loading…
Reference in new issue