From cc0fb0e15c3c38365ee34534d5933109d1f47e5b Mon Sep 17 00:00:00 2001 From: conduition Date: Tue, 19 Mar 2024 19:44:30 +0000 Subject: [PATCH] add checks for OP_CSV enforcement of relative locktimes --- src/lib.rs | 114 ++++++++++++++++++++++++--------- {tests => src}/regtest.rs | 130 +++++++++++++++++++++++++++++++++++--- 2 files changed, 206 insertions(+), 38 deletions(-) rename {tests => src}/regtest.rs (89%) diff --git a/src/lib.rs b/src/lib.rs index a7b1ec0..11d529d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ //! //! See [the Github README](https://github.com/conduition/dlctix). +#[cfg(test)] +mod regtest; + pub(crate) mod consts; pub(crate) mod contract; pub(crate) mod errors; @@ -756,6 +759,32 @@ impl SignedContract { .input_weight_for_sellback_tx() } + pub(crate) fn unchecked_sign_outcome_reclaim_tx_input>( + &self, + outcome: &Outcome, + reclaim_tx: &mut Transaction, + input_index: usize, + prevouts: &Prevouts, + market_maker_secret_key: Scalar, + ) -> Result<(), Error> { + let outcome_spend_info = self + .dlc + .outcome_tx_build + .outcome_spend_infos() + .get(outcome) + .ok_or(Error)?; + + let witness = outcome_spend_info.witness_tx_reclaim( + reclaim_tx, + input_index, + prevouts, + market_maker_secret_key, + )?; + + reclaim_tx.input[input_index].witness = witness; + Ok(()) + } + pub fn sign_outcome_reclaim_tx_input>( &self, outcome: &Outcome, @@ -780,22 +809,13 @@ impl SignedContract { expected_prevout, )?; - let outcome_spend_info = self - .dlc - .outcome_tx_build - .outcome_spend_infos() - .get(outcome) - .ok_or(Error)?; - - let witness = outcome_spend_info.witness_tx_reclaim( + self.unchecked_sign_outcome_reclaim_tx_input( + outcome, reclaim_tx, input_index, prevouts, market_maker_secret_key, - )?; - - reclaim_tx.input[input_index].witness = witness; - Ok(()) + ) } /// Sign a cooperative closing transaction which spends the outcome transaction output. @@ -851,6 +871,34 @@ impl SignedContract { Ok(()) } + pub(crate) fn unchecked_sign_split_win_tx_input>( + &self, + win_cond: &WinCondition, + win_tx: &mut Transaction, + input_index: usize, + prevouts: &Prevouts, + ticket_preimage: Preimage, + player_secret_key: Scalar, + ) -> Result<(), Error> { + let split_spend_info = self + .dlc + .split_tx_build + .split_spend_infos() + .get(win_cond) + .ok_or(Error)?; + + let witness = split_spend_info.witness_tx_win( + win_tx, + input_index, + prevouts, + ticket_preimage, + player_secret_key, + )?; + + win_tx.input[input_index].witness = witness; + Ok(()) + } + pub fn sign_split_win_tx_input>( &self, win_cond: &WinCondition, @@ -885,6 +933,24 @@ impl SignedContract { expected_prevout, )?; + self.unchecked_sign_split_win_tx_input( + win_cond, + win_tx, + input_index, + prevouts, + ticket_preimage, + player_secret_key, + ) + } + + pub(crate) fn unchecked_sign_split_reclaim_tx_input>( + &self, + win_cond: &WinCondition, + reclaim_tx: &mut Transaction, + input_index: usize, + prevouts: &Prevouts, + market_maker_secret_key: Scalar, + ) -> Result<(), Error> { let split_spend_info = self .dlc .split_tx_build @@ -892,15 +958,14 @@ impl SignedContract { .get(win_cond) .ok_or(Error)?; - let witness = split_spend_info.witness_tx_win( - win_tx, + let witness = split_spend_info.witness_tx_reclaim( + reclaim_tx, input_index, prevouts, - ticket_preimage, - player_secret_key, + market_maker_secret_key, )?; - win_tx.input[input_index].witness = witness; + reclaim_tx.input[input_index].witness = witness; Ok(()) } @@ -928,22 +993,13 @@ impl SignedContract { expected_prevout, )?; - let split_spend_info = self - .dlc - .split_tx_build - .split_spend_infos() - .get(win_cond) - .ok_or(Error)?; - - let witness = split_spend_info.witness_tx_reclaim( + self.unchecked_sign_split_reclaim_tx_input( + win_cond, reclaim_tx, input_index, prevouts, market_maker_secret_key, - )?; - - reclaim_tx.input[input_index].witness = witness; - Ok(()) + ) } pub fn sign_split_sellback_tx_input>( diff --git a/tests/regtest.rs b/src/regtest.rs similarity index 89% rename from tests/regtest.rs rename to src/regtest.rs index a438556..74b4203 100644 --- a/tests/regtest.rs +++ b/src/regtest.rs @@ -1,5 +1,6 @@ +use crate::*; + use bitcoincore_rpc::{jsonrpc::serde_json, Auth, Client as BitcoinClient, RpcApi}; -use dlctix::*; use serial_test::serial; use bitcoin::{ @@ -7,7 +8,7 @@ use bitcoin::{ key::TweakedPublicKey, locktime::absolute::LockTime, sighash::{Prevouts, SighashCache, TapSighashType}, - Address, Amount, FeeRate, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, + Address, Amount, FeeRate, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, }; use musig2::{CompactSignature, LiftedSignature, PartialSignature, PubNonce}; use rand::{CryptoRng, RngCore}; @@ -587,8 +588,6 @@ fn ticketed_dlc_with_on_chain_resolutions() { .split_win_tx_input_and_prevout(&bob_win_cond) .unwrap(); - // TODO test OP_CSV by spending without correct min sequence number - let mut bob_win_tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: LockTime::ZERO, @@ -606,6 +605,36 @@ fn ticketed_dlc_with_on_chain_resolutions() { }], }; + // Ensure Bob cannot broadcast a win TX early. OP_CSV should + // enforce the relative locktime. + { + let mut invalid_bob_win_tx = bob_win_tx.clone(); + invalid_bob_win_tx.input[0].sequence = Sequence::MAX; + + manager + .contract + .unchecked_sign_split_win_tx_input( + &bob_win_cond, + &mut invalid_bob_win_tx, + 0, // input index + &Prevouts::All(&[bob_split_prevout]), + manager.bob.ticket_preimage, + manager.bob.seckey, + ) + .expect("failed to sign win TX"); + + let err = manager + .rpc + .send_raw_transaction(&invalid_bob_win_tx) + .expect_err("early broadcast of win TX should fail"); + assert_eq!( + err.to_string(), + "JSON-RPC error: RPC error response: RpcError { code: -26, \ + message: \"mandatory-script-verify-flag-failed (Locktime requirement not satisfied)\", \ + data: None }", + ); + } + manager .contract .sign_split_win_tx_input( @@ -638,8 +667,6 @@ fn ticketed_dlc_with_on_chain_resolutions() { .split_reclaim_tx_input_and_prevout(&carol_win_cond) .unwrap(); - // TODO test OP_CSV encumberance on reclaim script - let mut reclaim_tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: LockTime::ZERO, @@ -657,6 +684,35 @@ fn ticketed_dlc_with_on_chain_resolutions() { }], }; + // Ensure the Market Maker cannot broadcast a split reclaim TX early. OP_CSV + // should enforce the relative locktime. + { + let mut invalid_reclaim_tx = reclaim_tx.clone(); + invalid_reclaim_tx.input[0].sequence = Sequence::MAX; + + manager + .contract + .unchecked_sign_split_reclaim_tx_input( + &carol_win_cond, + &mut invalid_reclaim_tx, + 0, // input index + &Prevouts::All(&[carol_split_prevout]), + manager.market_maker_seckey, + ) + .expect("failed to sign win TX"); + + let err = manager + .rpc + .send_raw_transaction(&invalid_reclaim_tx) + .expect_err("early broadcast of split reclaim TX should fail"); + assert_eq!( + err.to_string(), + "JSON-RPC error: RPC error response: RpcError { code: -26, \ + message: \"mandatory-script-verify-flag-failed (Locktime requirement not satisfied)\", \ + data: None }", + ); + } + manager .contract .sign_split_reclaim_tx_input( @@ -870,6 +926,35 @@ fn ticketed_dlc_market_maker_reclaims_outcome_tx() { }], }; + // Ensure the Market Maker cannot broadcast an outcome reclaim TX early. OP_CSV + // should enforce the relative locktime. + { + let mut invalid_reclaim_tx = reclaim_tx.clone(); + invalid_reclaim_tx.input[0].sequence = Sequence::MAX; + + manager + .contract + .unchecked_sign_outcome_reclaim_tx_input( + &outcome, + &mut invalid_reclaim_tx, + 0, // input index + &Prevouts::All(&[reclaim_tx_prevout]), + manager.market_maker_seckey, + ) + .expect("failed to sign outcome reclaim TX"); + + let err = manager + .rpc + .send_raw_transaction(&invalid_reclaim_tx) + .expect_err("early broadcast of outcome reclaim TX should fail"); + assert_eq!( + err.to_string(), + "JSON-RPC error: RPC error response: RpcError { code: -26, \ + message: \"mandatory-script-verify-flag-failed (Locktime requirement not satisfied)\", \ + data: None }", + ); + } + manager .contract .sign_outcome_reclaim_tx_input( @@ -912,7 +997,6 @@ fn ticketed_dlc_contract_expiry_with_on_chain_resolution() { let manager = SimulationManager::new(); // The contract expires, paying out to dave. - let outcome = Outcome::Expiry; let expiry_tx = manager .contract .expiry_tx() @@ -973,8 +1057,6 @@ fn ticketed_dlc_contract_expiry_with_on_chain_resolution() { .split_win_tx_input_and_prevout(&dave_win_cond) .unwrap(); - // TODO test OP_CSV by spending without correct min sequence number - let mut dave_win_tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: LockTime::ZERO, @@ -992,6 +1074,36 @@ fn ticketed_dlc_contract_expiry_with_on_chain_resolution() { }], }; + // Ensure Dave cannot broadcast the win TX early. OP_CSV should + // enforce the relative locktime. + { + let mut invalid_dave_win_tx = dave_win_tx.clone(); + invalid_dave_win_tx.input[0].sequence = Sequence::MAX; + + manager + .contract + .unchecked_sign_split_win_tx_input( + &dave_win_cond, + &mut invalid_dave_win_tx, + 0, // input index + &Prevouts::All(&[dave_split_prevout]), + manager.dave.ticket_preimage, + manager.dave.seckey, + ) + .expect("failed to sign win TX"); + + let err = manager + .rpc + .send_raw_transaction(&invalid_dave_win_tx) + .expect_err("early broadcast of win TX should fail"); + assert_eq!( + err.to_string(), + "JSON-RPC error: RPC error response: RpcError { code: -26, \ + message: \"mandatory-script-verify-flag-failed (Locktime requirement not satisfied)\", \ + data: None }", + ); + } + manager .contract .sign_split_win_tx_input(