Lucas Soriano
3 years ago
committed by
GitHub
2 changed files with 602 additions and 592 deletions
@ -0,0 +1,599 @@ |
|||
use anyhow::{bail, Context, Result}; |
|||
use bdk::bitcoin::util::bip32::ExtendedPrivKey; |
|||
use bdk::bitcoin::{Amount, Network, PrivateKey, PublicKey, Transaction}; |
|||
use bdk::miniscript::DescriptorTrait; |
|||
use bdk::wallet::AddressIndex; |
|||
use bdk::SignOptions; |
|||
use cfd_protocol::{ |
|||
build_cfd_transactions, commit_descriptor, compute_signature_point, finalize_spend_transaction, |
|||
lock_descriptor, punish_transaction, spending_tx_sighash, Message, OracleParams, Payout, |
|||
PunishParams, TransactionExt, WalletExt, |
|||
}; |
|||
use rand::{CryptoRng, RngCore, SeedableRng}; |
|||
use rand_chacha::ChaChaRng; |
|||
use secp256k1_zkp::{schnorrsig, SecretKey, SECP256K1}; |
|||
use std::collections::HashMap; |
|||
|
|||
#[test] |
|||
fn run_cfd_protocol() { |
|||
let mut rng = ChaChaRng::seed_from_u64(0); |
|||
|
|||
let maker_lock_amount = Amount::ONE_BTC; |
|||
let taker_lock_amount = Amount::ONE_BTC; |
|||
|
|||
let oracle = Oracle::new(&mut rng); |
|||
let (event, announcement) = announce(&mut rng); |
|||
|
|||
let (maker_sk, maker_pk) = make_keypair(&mut rng); |
|||
let (taker_sk, taker_pk) = make_keypair(&mut rng); |
|||
|
|||
let payouts = vec![ |
|||
Payout::new(Message::Win, Amount::from_btc(2.0).unwrap(), Amount::ZERO), |
|||
Payout::new(Message::Lose, Amount::ZERO, Amount::from_btc(2.0).unwrap()), |
|||
]; |
|||
|
|||
let refund_timelock = 0; |
|||
|
|||
let maker_wallet = build_wallet(&mut rng, Amount::from_btc(0.4).unwrap(), 5).unwrap(); |
|||
let taker_wallet = build_wallet(&mut rng, Amount::from_btc(0.4).unwrap(), 5).unwrap(); |
|||
|
|||
let maker_address = maker_wallet.get_address(AddressIndex::New).unwrap(); |
|||
let taker_address = taker_wallet.get_address(AddressIndex::New).unwrap(); |
|||
|
|||
let lock_amount = maker_lock_amount + taker_lock_amount; |
|||
let (maker_revocation_sk, maker_revocation_pk) = make_keypair(&mut rng); |
|||
let (maker_publish_sk, maker_publish_pk) = make_keypair(&mut rng); |
|||
|
|||
let (taker_revocation_sk, taker_revocation_pk) = make_keypair(&mut rng); |
|||
let (taker_publish_sk, taker_publish_pk) = make_keypair(&mut rng); |
|||
|
|||
let maker_params = maker_wallet |
|||
.build_party_params(maker_lock_amount, maker_pk) |
|||
.unwrap(); |
|||
let taker_params = taker_wallet |
|||
.build_party_params(taker_lock_amount, taker_pk) |
|||
.unwrap(); |
|||
|
|||
let maker_cfd_txs = build_cfd_transactions( |
|||
( |
|||
maker_params.clone(), |
|||
PunishParams { |
|||
revocation_pk: maker_revocation_pk, |
|||
publish_pk: maker_publish_pk, |
|||
}, |
|||
), |
|||
( |
|||
taker_params.clone(), |
|||
PunishParams { |
|||
revocation_pk: taker_revocation_pk, |
|||
publish_pk: taker_publish_pk, |
|||
}, |
|||
), |
|||
OracleParams { |
|||
pk: oracle.public_key(), |
|||
nonce_pk: event.nonce_pk, |
|||
}, |
|||
refund_timelock, |
|||
payouts.clone(), |
|||
maker_sk, |
|||
) |
|||
.unwrap(); |
|||
|
|||
let taker_cfd_txs = build_cfd_transactions( |
|||
( |
|||
maker_params, |
|||
PunishParams { |
|||
revocation_pk: maker_revocation_pk, |
|||
publish_pk: maker_publish_pk, |
|||
}, |
|||
), |
|||
( |
|||
taker_params, |
|||
PunishParams { |
|||
revocation_pk: taker_revocation_pk, |
|||
publish_pk: taker_publish_pk, |
|||
}, |
|||
), |
|||
OracleParams { |
|||
pk: oracle.public_key(), |
|||
nonce_pk: event.nonce_pk, |
|||
}, |
|||
refund_timelock, |
|||
payouts, |
|||
taker_sk, |
|||
) |
|||
.unwrap(); |
|||
|
|||
let commit_descriptor = commit_descriptor( |
|||
(maker_pk, maker_revocation_pk, maker_publish_pk), |
|||
(taker_pk, taker_revocation_pk, taker_publish_pk), |
|||
); |
|||
|
|||
let commit_amount = Amount::from_sat(maker_cfd_txs.commit.0.output[0].value); |
|||
assert_eq!( |
|||
commit_amount.as_sat(), |
|||
taker_cfd_txs.commit.0.output[0].value |
|||
); |
|||
|
|||
{ |
|||
let refund_sighash = |
|||
spending_tx_sighash(&taker_cfd_txs.refund.0, &commit_descriptor, commit_amount); |
|||
SECP256K1 |
|||
.verify(&refund_sighash, &maker_cfd_txs.refund.1, &maker_pk.key) |
|||
.expect("valid maker refund sig") |
|||
}; |
|||
|
|||
{ |
|||
let refund_sighash = |
|||
spending_tx_sighash(&maker_cfd_txs.refund.0, &commit_descriptor, commit_amount); |
|||
SECP256K1 |
|||
.verify(&refund_sighash, &taker_cfd_txs.refund.1, &taker_pk.key) |
|||
.expect("valid taker refund sig") |
|||
}; |
|||
|
|||
// TODO: We should not rely on order
|
|||
for (maker_cet, taker_cet) in maker_cfd_txs.cets.iter().zip(taker_cfd_txs.cets.iter()) { |
|||
let cet_sighash = { |
|||
let maker_sighash = |
|||
spending_tx_sighash(&maker_cet.0, &commit_descriptor, commit_amount); |
|||
let taker_sighash = |
|||
spending_tx_sighash(&taker_cet.0, &commit_descriptor, commit_amount); |
|||
|
|||
assert_eq!(maker_sighash, taker_sighash); |
|||
maker_sighash |
|||
}; |
|||
|
|||
let encryption_point = { |
|||
let maker_encryption_point = compute_signature_point( |
|||
&oracle.public_key(), |
|||
&announcement.nonce_pk(), |
|||
maker_cet.2, |
|||
) |
|||
.unwrap(); |
|||
let taker_encryption_point = compute_signature_point( |
|||
&oracle.public_key(), |
|||
&announcement.nonce_pk(), |
|||
taker_cet.2, |
|||
) |
|||
.unwrap(); |
|||
|
|||
assert_eq!(maker_encryption_point, taker_encryption_point); |
|||
maker_encryption_point |
|||
}; |
|||
|
|||
let maker_encsig = maker_cet.1; |
|||
maker_encsig |
|||
.verify(SECP256K1, &cet_sighash, &maker_pk.key, &encryption_point) |
|||
.expect("valid maker cet encsig"); |
|||
|
|||
let taker_encsig = taker_cet.1; |
|||
taker_encsig |
|||
.verify(SECP256K1, &cet_sighash, &taker_pk.key, &encryption_point) |
|||
.expect("valid taker cet encsig"); |
|||
} |
|||
|
|||
let lock_descriptor = lock_descriptor(maker_pk, taker_pk); |
|||
|
|||
{ |
|||
let commit_sighash = |
|||
spending_tx_sighash(&maker_cfd_txs.commit.0, &lock_descriptor, lock_amount); |
|||
let commit_encsig = maker_cfd_txs.commit.1; |
|||
commit_encsig |
|||
.verify( |
|||
SECP256K1, |
|||
&commit_sighash, |
|||
&maker_pk.key, |
|||
&taker_publish_pk.key, |
|||
) |
|||
.expect("valid maker commit encsig"); |
|||
}; |
|||
|
|||
{ |
|||
let commit_sighash = |
|||
spending_tx_sighash(&taker_cfd_txs.commit.0, &lock_descriptor, lock_amount); |
|||
let commit_encsig = taker_cfd_txs.commit.1; |
|||
commit_encsig |
|||
.verify( |
|||
SECP256K1, |
|||
&commit_sighash, |
|||
&taker_pk.key, |
|||
&maker_publish_pk.key, |
|||
) |
|||
.expect("valid taker commit encsig"); |
|||
}; |
|||
|
|||
// sign lock transaction
|
|||
|
|||
let mut signed_lock_tx = maker_cfd_txs.lock; |
|||
maker_wallet |
|||
.sign( |
|||
&mut signed_lock_tx, |
|||
SignOptions { |
|||
trust_witness_utxo: true, |
|||
..Default::default() |
|||
}, |
|||
) |
|||
.unwrap(); |
|||
|
|||
taker_wallet |
|||
.sign( |
|||
&mut signed_lock_tx, |
|||
SignOptions { |
|||
trust_witness_utxo: true, |
|||
..Default::default() |
|||
}, |
|||
) |
|||
.unwrap(); |
|||
|
|||
let signed_lock_tx = signed_lock_tx.extract_tx(); |
|||
|
|||
// verify commit transaction
|
|||
|
|||
let commit_tx = maker_cfd_txs.commit.0; |
|||
let maker_sig = maker_cfd_txs.commit.1.decrypt(&taker_publish_sk).unwrap(); |
|||
let taker_sig = taker_cfd_txs.commit.1.decrypt(&maker_publish_sk).unwrap(); |
|||
let signed_commit_tx = finalize_spend_transaction( |
|||
commit_tx, |
|||
&lock_descriptor, |
|||
(maker_pk, maker_sig), |
|||
(taker_pk, taker_sig), |
|||
) |
|||
.unwrap(); |
|||
|
|||
check_tx_fee(&[&signed_lock_tx], &signed_commit_tx).expect("correct fees for commit tx"); |
|||
|
|||
lock_descriptor |
|||
.address(Network::Regtest) |
|||
.expect("can derive address from descriptor") |
|||
.script_pubkey() |
|||
.verify( |
|||
0, |
|||
lock_amount.as_sat(), |
|||
bitcoin::consensus::serialize(&signed_commit_tx).as_slice(), |
|||
) |
|||
.expect("valid signed commit transaction"); |
|||
|
|||
// verify refund transaction
|
|||
|
|||
let maker_sig = maker_cfd_txs.refund.1; |
|||
let taker_sig = taker_cfd_txs.refund.1; |
|||
let signed_refund_tx = finalize_spend_transaction( |
|||
maker_cfd_txs.refund.0, |
|||
&commit_descriptor, |
|||
(maker_pk, maker_sig), |
|||
(taker_pk, taker_sig), |
|||
) |
|||
.unwrap(); |
|||
|
|||
check_tx_fee(&[&signed_commit_tx], &signed_refund_tx).expect("correct fees for refund tx"); |
|||
|
|||
commit_descriptor |
|||
.address(Network::Regtest) |
|||
.expect("can derive address from descriptor") |
|||
.script_pubkey() |
|||
.verify( |
|||
0, |
|||
commit_amount.as_sat(), |
|||
bitcoin::consensus::serialize(&signed_refund_tx).as_slice(), |
|||
) |
|||
.expect("valid signed refund transaction"); |
|||
|
|||
// verify cets
|
|||
|
|||
let attestations = [Message::Win, Message::Lose] |
|||
.iter() |
|||
.map(|msg| (*msg, oracle.attest(&event, *msg))) |
|||
.collect::<HashMap<_, _>>(); |
|||
|
|||
maker_cfd_txs |
|||
.cets |
|||
.into_iter() |
|||
.zip(taker_cfd_txs.cets) |
|||
.try_for_each(|((cet, maker_encsig, msg), (_, taker_encsig, _))| { |
|||
let oracle_sig = attestations |
|||
.get(&msg) |
|||
.expect("oracle to sign all messages in test"); |
|||
let (_nonce_pk, signature_scalar) = schnorrsig_decompose(oracle_sig); |
|||
|
|||
let maker_sig = maker_encsig |
|||
.decrypt(&signature_scalar) |
|||
.context("could not decrypt maker encsig on cet")?; |
|||
let taker_sig = taker_encsig |
|||
.decrypt(&signature_scalar) |
|||
.context("could not decrypt taker encsig on cet")?; |
|||
|
|||
let signed_cet = finalize_spend_transaction( |
|||
cet, |
|||
&commit_descriptor, |
|||
(maker_pk, maker_sig), |
|||
(taker_pk, taker_sig), |
|||
)?; |
|||
|
|||
check_tx_fee(&[&signed_commit_tx], &signed_cet).expect("correct fees for cet"); |
|||
|
|||
commit_descriptor |
|||
.address(Network::Regtest) |
|||
.expect("can derive address from descriptor") |
|||
.script_pubkey() |
|||
.verify( |
|||
0, |
|||
commit_amount.as_sat(), |
|||
bitcoin::consensus::serialize(&signed_cet).as_slice(), |
|||
) |
|||
.context("failed to verify cet") |
|||
}) |
|||
.expect("all cets to be properly signed"); |
|||
|
|||
// verify punishment transactions
|
|||
|
|||
let punish_tx = punish_transaction( |
|||
&commit_descriptor, |
|||
&maker_address, |
|||
maker_cfd_txs.commit.1, |
|||
maker_sk, |
|||
taker_revocation_sk, |
|||
taker_publish_pk, |
|||
&signed_commit_tx, |
|||
) |
|||
.unwrap(); |
|||
|
|||
check_tx_fee(&[&signed_commit_tx], &punish_tx).expect("correct fees for punish tx"); |
|||
|
|||
commit_descriptor |
|||
.address(Network::Regtest) |
|||
.expect("can derive address from descriptor") |
|||
.script_pubkey() |
|||
.verify( |
|||
0, |
|||
commit_amount.as_sat(), |
|||
bitcoin::consensus::serialize(&punish_tx).as_slice(), |
|||
) |
|||
.expect("valid punish transaction signed by maker"); |
|||
|
|||
let punish_tx = punish_transaction( |
|||
&commit_descriptor, |
|||
&taker_address, |
|||
taker_cfd_txs.commit.1, |
|||
taker_sk, |
|||
maker_revocation_sk, |
|||
maker_publish_pk, |
|||
&signed_commit_tx, |
|||
) |
|||
.unwrap(); |
|||
|
|||
commit_descriptor |
|||
.address(Network::Regtest) |
|||
.expect("can derive address from descriptor") |
|||
.script_pubkey() |
|||
.verify( |
|||
0, |
|||
commit_amount.as_sat(), |
|||
bitcoin::consensus::serialize(&punish_tx).as_slice(), |
|||
) |
|||
.expect("valid punish transaction signed by taker"); |
|||
} |
|||
|
|||
fn check_tx_fee(input_txs: &[&Transaction], spend_tx: &Transaction) -> Result<()> { |
|||
let input_amount = spend_tx |
|||
.input |
|||
.iter() |
|||
.try_fold::<_, _, Result<_>>(0, |acc, input| { |
|||
let value = input_txs |
|||
.iter() |
|||
.find_map(|tx| { |
|||
(tx.txid() == input.previous_output.txid) |
|||
.then(|| tx.output[input.previous_output.vout as usize].value) |
|||
}) |
|||
.with_context(|| { |
|||
format!( |
|||
"spend tx input {} not found in input_txs", |
|||
input.previous_output |
|||
) |
|||
}) |
|||
.context("foo")?; |
|||
|
|||
Ok(acc + value) |
|||
})?; |
|||
|
|||
let output_amount = spend_tx |
|||
.output |
|||
.iter() |
|||
.fold(0, |acc, output| acc + output.value); |
|||
let fee = input_amount - output_amount; |
|||
|
|||
let min_relay_fee = spend_tx.get_virtual_size(); |
|||
if (fee as f64) < min_relay_fee { |
|||
bail!("min relay fee not met, {} < {}", fee, min_relay_fee) |
|||
} |
|||
|
|||
Ok(()) |
|||
} |
|||
|
|||
fn build_wallet<R>( |
|||
rng: &mut R, |
|||
utxo_amount: Amount, |
|||
num_utxos: u8, |
|||
) -> Result<bdk::Wallet<(), bdk::database::MemoryDatabase>> |
|||
where |
|||
R: RngCore + CryptoRng, |
|||
{ |
|||
use bdk::{populate_test_db, testutils}; |
|||
|
|||
let mut seed = [0u8; 32]; |
|||
rng.fill_bytes(&mut seed); |
|||
|
|||
let key = ExtendedPrivKey::new_master(Network::Regtest, &seed)?; |
|||
let descriptors = testutils!(@descriptors (&format!("wpkh({}/*)", key))); |
|||
|
|||
let mut database = bdk::database::MemoryDatabase::new(); |
|||
|
|||
for index in 0..num_utxos { |
|||
populate_test_db!( |
|||
&mut database, |
|||
testutils! { |
|||
@tx ( (@external descriptors, index as u32) => utxo_amount.as_sat() ) (@confirmations 1) |
|||
}, |
|||
Some(100) |
|||
); |
|||
} |
|||
|
|||
let wallet = bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database)?; |
|||
|
|||
Ok(wallet) |
|||
} |
|||
|
|||
struct Oracle { |
|||
key_pair: schnorrsig::KeyPair, |
|||
} |
|||
|
|||
impl Oracle { |
|||
fn new<R>(rng: &mut R) -> Self |
|||
where |
|||
R: RngCore + CryptoRng, |
|||
{ |
|||
let key_pair = schnorrsig::KeyPair::new(SECP256K1, rng); |
|||
|
|||
Self { key_pair } |
|||
} |
|||
|
|||
fn public_key(&self) -> schnorrsig::PublicKey { |
|||
schnorrsig::PublicKey::from_keypair(SECP256K1, &self.key_pair) |
|||
} |
|||
|
|||
fn attest(&self, event: &Event, msg: Message) -> schnorrsig::Signature { |
|||
secp_utils::schnorr_sign_with_nonce(&msg.into(), &self.key_pair, &event.nonce) |
|||
} |
|||
} |
|||
|
|||
fn announce<R>(rng: &mut R) -> (Event, Announcement) |
|||
where |
|||
R: RngCore + CryptoRng, |
|||
{ |
|||
let event = Event::new(rng); |
|||
let announcement = event.announcement(); |
|||
|
|||
(event, announcement) |
|||
} |
|||
|
|||
/// Represents the oracle's commitment to a nonce that will be used to
|
|||
/// sign a specific event in the future.
|
|||
struct Event { |
|||
/// Nonce.
|
|||
///
|
|||
/// Must remain secret.
|
|||
nonce: SecretKey, |
|||
nonce_pk: schnorrsig::PublicKey, |
|||
} |
|||
|
|||
impl Event { |
|||
fn new<R>(rng: &mut R) -> Self |
|||
where |
|||
R: RngCore + CryptoRng, |
|||
{ |
|||
let nonce = SecretKey::new(rng); |
|||
|
|||
let key_pair = schnorrsig::KeyPair::from_secret_key(SECP256K1, nonce); |
|||
let nonce_pk = schnorrsig::PublicKey::from_keypair(SECP256K1, &key_pair); |
|||
|
|||
Self { nonce, nonce_pk } |
|||
} |
|||
|
|||
fn announcement(&self) -> Announcement { |
|||
Announcement { |
|||
nonce_pk: self.nonce_pk, |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// Public message which can be used by anyone to perform a DLC
|
|||
/// protocol based on a specific event.
|
|||
///
|
|||
/// These would normally include more information to identify the
|
|||
/// specific event, but we omit this for simplicity. See:
|
|||
/// https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#oracle-events
|
|||
#[derive(Clone, Copy)] |
|||
struct Announcement { |
|||
nonce_pk: schnorrsig::PublicKey, |
|||
} |
|||
|
|||
impl Announcement { |
|||
fn nonce_pk(&self) -> schnorrsig::PublicKey { |
|||
self.nonce_pk |
|||
} |
|||
} |
|||
|
|||
fn make_keypair<R>(rng: &mut R) -> (SecretKey, PublicKey) |
|||
where |
|||
R: RngCore + CryptoRng, |
|||
{ |
|||
let sk = SecretKey::new(rng); |
|||
let pk = PublicKey::from_private_key( |
|||
SECP256K1, |
|||
&PrivateKey { |
|||
compressed: true, |
|||
network: Network::Regtest, |
|||
key: sk, |
|||
}, |
|||
); |
|||
|
|||
(sk, pk) |
|||
} |
|||
|
|||
/// Decompose a BIP340 signature into R and s.
|
|||
pub fn schnorrsig_decompose( |
|||
signature: &schnorrsig::Signature, |
|||
) -> (schnorrsig::PublicKey, SecretKey) { |
|||
let bytes = signature.as_ref(); |
|||
|
|||
let nonce_pk = schnorrsig::PublicKey::from_slice(&bytes[0..32]).expect("R value in sig"); |
|||
let s = SecretKey::from_slice(&bytes[32..64]).expect("s value in sig"); |
|||
|
|||
(nonce_pk, s) |
|||
} |
|||
|
|||
mod secp_utils { |
|||
use super::*; |
|||
|
|||
use secp256k1_zkp::secp256k1_zkp_sys::types::c_void; |
|||
use secp256k1_zkp::secp256k1_zkp_sys::CPtr; |
|||
use std::os::raw::{c_int, c_uchar}; |
|||
use std::ptr; |
|||
|
|||
/// Create a Schnorr signature using the provided nonce instead of generating one.
|
|||
pub fn schnorr_sign_with_nonce( |
|||
msg: &secp256k1_zkp::Message, |
|||
keypair: &schnorrsig::KeyPair, |
|||
nonce: &SecretKey, |
|||
) -> schnorrsig::Signature { |
|||
unsafe { |
|||
let mut sig = [0u8; secp256k1_zkp::constants::SCHNORRSIG_SIGNATURE_SIZE]; |
|||
assert_eq!( |
|||
1, |
|||
secp256k1_zkp::ffi::secp256k1_schnorrsig_sign( |
|||
*SECP256K1.ctx(), |
|||
sig.as_mut_c_ptr(), |
|||
msg.as_c_ptr(), |
|||
keypair.as_ptr(), |
|||
Some(constant_nonce_fn), |
|||
nonce.as_c_ptr() as *const c_void |
|||
) |
|||
); |
|||
|
|||
schnorrsig::Signature::from_slice(&sig).unwrap() |
|||
} |
|||
} |
|||
|
|||
extern "C" fn constant_nonce_fn( |
|||
nonce32: *mut c_uchar, |
|||
_msg32: *const c_uchar, |
|||
_key32: *const c_uchar, |
|||
_xonly_pk32: *const c_uchar, |
|||
_algo16: *const c_uchar, |
|||
data: *mut c_void, |
|||
) -> c_int { |
|||
unsafe { |
|||
ptr::copy_nonoverlapping(data as *const c_uchar, nonce32, 32); |
|||
} |
|||
1 |
|||
} |
|||
} |
Loading…
Reference in new issue