From 641faceb8aa51e9a080953310821fdb867cf3bcb Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 13 Sep 2021 12:07:45 +1000 Subject: [PATCH] Add renew_cfd_transactions API --- cfd_protocol/src/lib.rs | 116 ++++++++-- cfd_protocol/tests/cfds.rs | 450 ++++++++++++++++++++++++++++++++++++- 2 files changed, 543 insertions(+), 23 deletions(-) diff --git a/cfd_protocol/src/lib.rs b/cfd_protocol/src/lib.rs index 18575dd..e0e1f75 100644 --- a/cfd_protocol/src/lib.rs +++ b/cfd_protocol/src/lib.rs @@ -55,7 +55,7 @@ where } } -pub fn build_cfd_transactions( +pub fn create_cfd_transactions( (maker, maker_punish_params): (PartyParams, PunishParams), (taker, taker_punish_params): (PartyParams, PunishParams), oracle_params: OracleParams, @@ -63,13 +63,6 @@ pub fn build_cfd_transactions( payouts: Vec, identity_sk: SecretKey, ) -> Result { - /// 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 lock_tx = lock_transaction( maker.lock_psbt.clone(), taker.lock_psbt.clone(), @@ -78,15 +71,102 @@ pub fn build_cfd_transactions( 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_params, + 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_params: OracleParams, + refund_timelock: u32, + payouts: Vec, + identity_sk: SecretKey, +) -> Result { + 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_params, + 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_params: OracleParams, + refund_timelock: u32, + payouts: Vec, + identity_sk: SecretKey, +) -> Result { + /// 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.identity_pk, + maker_pk, maker_punish_params.revocation_pk, maker_punish_params.publish_pk, ), ( - taker.identity_pk, + taker_pk, taker_punish_params.revocation_pk, taker_punish_params.publish_pk, ), @@ -94,9 +174,9 @@ pub fn build_cfd_transactions( .context("cannot build commit tx")?; let identity_pk = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &identity_sk); - let commit_encsig = if identity_pk == maker.identity_pk.key { + let commit_encsig = if identity_pk == maker_pk.key { commit_tx.encsign(identity_sk, &taker_punish_params.publish_pk) - } else if identity_pk == taker.identity_pk.key { + } 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") @@ -106,10 +186,10 @@ pub fn build_cfd_transactions( let tx = RefundTransaction::new( &commit_tx, refund_timelock, - &maker.address, - &taker.address, - maker.lock_amount, - taker.lock_amount, + &maker_address, + &taker_address, + maker_lock_amount, + taker_lock_amount, ); let sighash = tx.sighash().to_message(); @@ -125,8 +205,8 @@ pub fn build_cfd_transactions( let cet = ContractExecutionTransaction::new( &commit_tx, payout, - &maker.address, - &taker.address, + &maker_address, + &taker_address, CET_TIMELOCK, )?; diff --git a/cfd_protocol/tests/cfds.rs b/cfd_protocol/tests/cfds.rs index ffed5e4..bb68f1a 100644 --- a/cfd_protocol/tests/cfds.rs +++ b/cfd_protocol/tests/cfds.rs @@ -5,9 +5,9 @@ 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, OracleParams, Payout, PunishParams, - TransactionExt, WalletExt, + commit_descriptor, compute_signature_point, create_cfd_transactions, + finalize_spend_transaction, lock_descriptor, punish_transaction, renew_cfd_transactions, + spending_tx_sighash, OracleParams, Payout, PunishParams, TransactionExt, WalletExt, }; use rand::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaChaRng; @@ -63,7 +63,7 @@ fn create_cfd() { .build_party_params(taker_lock_amount, taker_pk) .unwrap(); - let maker_cfd_txs = build_cfd_transactions( + let maker_cfd_txs = create_cfd_transactions( ( maker_params.clone(), PunishParams { @@ -88,7 +88,7 @@ fn create_cfd() { ) .unwrap(); - let taker_cfd_txs = build_cfd_transactions( + let taker_cfd_txs = create_cfd_transactions( ( maker_params, PunishParams { @@ -372,6 +372,446 @@ fn create_cfd() { .expect("valid punish transaction signed by taker"); } +#[test] +fn renew_cfd() { + 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( + b"win".to_vec(), + Amount::from_btc(2.0).unwrap(), + Amount::ZERO, + ), + Payout::new( + b"lose".to_vec(), + 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 = create_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 = create_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(); + + // renew cfd transactions + + 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 (event, announcement) = announce(&mut rng); + + let payouts = vec![ + Payout::new( + b"win".to_vec(), + Amount::from_btc(1.5).unwrap(), + Amount::from_btc(0.5).unwrap(), + ), + Payout::new( + b"lose".to_vec(), + Amount::from_btc(0.5).unwrap(), + Amount::from_btc(1.5).unwrap(), + ), + ]; + + let maker_cfd_txs = renew_cfd_transactions( + maker_cfd_txs.lock, + ( + maker_pk, + maker_lock_amount, + maker_address.address.clone(), + PunishParams { + revocation_pk: maker_revocation_pk, + publish_pk: maker_publish_pk, + }, + ), + ( + taker_pk, + taker_lock_amount, + taker_address.address.clone(), + PunishParams { + revocation_pk: taker_revocation_pk, + publish_pk: taker_publish_pk, + }, + ), + OracleParams { + pk: oracle.public_key(), + nonce_pk: announcement.nonce_pk(), + }, + refund_timelock, + payouts.clone(), + maker_sk, + ) + .unwrap(); + + let taker_cfd_txs = renew_cfd_transactions( + taker_cfd_txs.lock, + ( + maker_pk, + maker_lock_amount, + maker_address.address.clone(), + PunishParams { + revocation_pk: maker_revocation_pk, + publish_pk: maker_publish_pk, + }, + ), + ( + taker_pk, + taker_lock_amount, + taker_address.address.clone(), + PunishParams { + revocation_pk: taker_revocation_pk, + publish_pk: taker_publish_pk, + }, + ), + OracleParams { + pk: oracle.public_key(), + nonce_pk: announcement.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 + .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 + .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 = ["win".as_bytes(), "lose".as_bytes()] + .iter() + .map(|msg| (*msg, oracle.attest(&event, msg))) + .collect::>(); + + 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.as_slice()) + .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 + .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 + .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 + .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