diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c1b963df..ff03aa06 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -35,7 +35,7 @@ jobs: key: test-cache-${{ github.run_id }}-${{ github.run_number }} - uses: actions/checkout@v2 - id: set-matrix - run: cargo test --no-run && echo "::set-output name=matrix::$(scripts/get_test_list.sh execution manager channel_execution)" + run: cargo test --no-run && echo "::set-output name=matrix::$(scripts/get_test_list.sh execution manager channel_execution ordinals)" integration_tests: name: integration-tests needs: integration_tests_prepare diff --git a/bitcoin-rpc-provider/Cargo.toml b/bitcoin-rpc-provider/Cargo.toml index e7926592..c1b7d282 100644 --- a/bitcoin-rpc-provider/Cargo.toml +++ b/bitcoin-rpc-provider/Cargo.toml @@ -12,4 +12,6 @@ dlc-manager = {path = "../dlc-manager"} lightning = {version = "0.0.116"} log = "0.4.14" rust-bitcoin-coin-selection = { version = "0.1.0", git = "https://github.com/p2pderivatives/rust-bitcoin-coin-selection", rev = "405451929568422f7df809e35d6ad8f36fccce90", features = ["rand"] } +serde = "1.0" simple-wallet = {path = "../simple-wallet"} +secp256k1-zkp = {version = "0.7.0", default-features=false, features = ["global-context"]} diff --git a/bitcoin-rpc-provider/src/lib.rs b/bitcoin-rpc-provider/src/lib.rs index 13e7c9ca..43213963 100644 --- a/bitcoin-rpc-provider/src/lib.rs +++ b/bitcoin-rpc-provider/src/lib.rs @@ -13,8 +13,8 @@ use bitcoin::{ Txid, }; use bitcoin::{Address, OutPoint, TxOut}; +use bitcoincore_rpc::jsonrpc::serde_json::{self}; use bitcoincore_rpc::{json, Auth, Client, RpcApi}; -use bitcoincore_rpc_json::AddressType; use dlc_manager::error::Error as ManagerError; use dlc_manager::{Blockchain, Signer, Utxo, Wallet}; use json::EstimateMode; @@ -30,6 +30,8 @@ pub struct BitcoinCoreProvider { // Used to implement the FeeEstimator interface, heavily inspired by // https://github.com/lightningdevkit/ldk-sample/blob/main/src/bitcoind_client.rs#L26 fees: Arc>, + /// Indicates whether the wallet is descriptor based or not. + is_descriptor: bool, } #[derive(Debug)] @@ -95,10 +97,10 @@ impl BitcoinCoreProvider { rpc_base }; let auth = Auth::UserPass(rpc_user, rpc_password); - Ok(Self::new_from_rpc_client(Client::new(&rpc_url, auth)?)) + Self::new_from_rpc_client(Client::new(&rpc_url, auth)?) } - pub fn new_from_rpc_client(rpc_client: Client) -> Self { + pub fn new_from_rpc_client(rpc_client: Client) -> Result { let client = Arc::new(Mutex::new(rpc_client)); let mut fees: HashMap = HashMap::new(); fees.insert(ConfirmationTarget::Background, AtomicU32::new(MIN_FEERATE)); @@ -106,7 +108,22 @@ impl BitcoinCoreProvider { fees.insert(ConfirmationTarget::HighPriority, AtomicU32::new(5000)); let fees = Arc::new(fees); poll_for_fee_estimates(client.clone(), fees.clone()); - BitcoinCoreProvider { client, fees } + + #[derive(serde::Deserialize)] + struct Descriptor { + descriptors: bool, + } + + let is_descriptor = client + .lock() + .unwrap() + .call::("getwalletinfo", &[])? + .descriptors; + Ok(BitcoinCoreProvider { + client, + fees, + is_descriptor, + }) } } @@ -144,22 +161,46 @@ fn enc_err_to_manager_err(_e: EncodeError) -> ManagerError { Error::BitcoinError.into() } +#[derive(serde::Deserialize, Debug)] +struct DescriptorInfo { + desc: String, +} + +#[derive(serde::Deserialize, Debug)] +struct DescriptorListResponse { + descriptors: Vec, +} + impl Signer for BitcoinCoreProvider { fn get_secret_key_for_pubkey(&self, pubkey: &PublicKey) -> Result { - let b_pubkey = bitcoin::PublicKey { - compressed: true, - inner: *pubkey, - }; - let address = - Address::p2wpkh(&b_pubkey, self.get_network()?).or(Err(Error::BitcoinError))?; - - let pk = self - .client - .lock() - .unwrap() - .dump_private_key(&address) - .map_err(rpc_err_to_manager_err)?; - Ok(pk.inner) + if self.is_descriptor { + let client = self.client.lock().unwrap(); + let DescriptorListResponse { descriptors } = client + .call::("listdescriptors", &[serde_json::Value::Bool(true)]) + .map_err(rpc_err_to_manager_err)?; + descriptors + .iter() + .filter_map(|x| descriptor_to_secret_key(&x.desc)) + .find(|x| x.public_key(secp256k1_zkp::SECP256K1) == *pubkey) + .ok_or(ManagerError::InvalidState( + "Expected a descriptor at this position".to_string(), + )) + } else { + let b_pubkey = bitcoin::PublicKey { + compressed: true, + inner: *pubkey, + }; + let address = + Address::p2wpkh(&b_pubkey, self.get_network()?).or(Err(Error::BitcoinError))?; + + let pk = self + .client + .lock() + .unwrap() + .dump_private_key(&address) + .map_err(rpc_err_to_manager_err)?; + Ok(pk.inner) + } } fn sign_tx_input( @@ -200,27 +241,34 @@ impl Wallet for BitcoinCoreProvider { self.client .lock() .unwrap() - .get_new_address(None, Some(AddressType::Bech32)) + .call( + "getnewaddress", + &[ + serde_json::Value::Null, + serde_json::Value::String("bech32m".to_string()), + ], + ) .map_err(rpc_err_to_manager_err) } fn get_new_secret_key(&self) -> Result { let sk = SecretKey::new(&mut thread_rng()); let network = self.get_network()?; - self.client - .lock() - .unwrap() - .import_private_key( - &PrivateKey { - compressed: true, - network, - inner: sk, - }, - None, - Some(false), - ) - .map_err(rpc_err_to_manager_err)?; - + let client = self.client.lock().unwrap(); + let pk = PrivateKey { + compressed: true, + network, + inner: sk, + }; + if self.is_descriptor { + let wif = pk.to_wif(); + let desc = format!("rawtr({wif})"); + import_descriptor(&client, &desc)?; + } else { + client + .import_private_key(&pk, None, Some(false)) + .map_err(rpc_err_to_manager_err)?; + } Ok(sk) } @@ -234,9 +282,18 @@ impl Wallet for BitcoinCoreProvider { let utxo_res = client .list_unspent(None, None, None, Some(false), None) .map_err(rpc_err_to_manager_err)?; + let locked = client + .call::>("listlockunspent", &[]) + .map_err(rpc_err_to_manager_err)? + .iter() + .map(|x| OutPoint { + txid: x["txid"].as_str().unwrap().parse().unwrap(), + vout: x["vout"].as_u64().unwrap() as u32, + }) + .collect::>(); let mut utxo_pool: Vec = utxo_res .iter() - .filter(|x| x.spendable) + .filter(|x| x.spendable && locked.iter().all(|y| y.txid != x.txid || y.vout != x.vout)) .map(|x| { Ok(UtxoWrap(Utxo { tx_out: TxOut { @@ -267,11 +324,16 @@ impl Wallet for BitcoinCoreProvider { } fn import_address(&self, address: &Address) -> Result<(), ManagerError> { - self.client - .lock() - .unwrap() - .import_address(address, None, Some(false)) - .map_err(rpc_err_to_manager_err) + if self.is_descriptor { + let desc = format!("addr({address})"); + import_descriptor(&self.client.lock().unwrap(), &desc) + } else { + self.client + .lock() + .unwrap() + .import_address(address, None, Some(false)) + .map_err(rpc_err_to_manager_err) + } } } @@ -412,3 +474,27 @@ fn poll_for_fee_estimates( std::thread::sleep(Duration::from_secs(60)); }); } + +fn import_descriptor(client: &Client, desc: &str) -> Result<(), ManagerError> { + let info = client + .get_descriptor_info(desc) + .map_err(rpc_err_to_manager_err)?; + let checksum = info.checksum; + let args: serde_json::Value = serde_json::from_str(&format!( + "[{{ \"desc\": \"{desc}#{checksum}\", \"timestamp\": \"now\" }}]" + )) + .unwrap(); + client + .call::>("importdescriptors", &[args]) + .map_err(rpc_err_to_manager_err)?; + Ok(()) +} + +fn descriptor_to_secret_key(desc: &str) -> Option { + if !desc.starts_with("rawtr") { + return None; + } + let wif = desc.split_once('(')?.1.split_once(')')?.0; + let priv_key = PrivateKey::from_wif(wif).ok()?; + Some(priv_key.inner) +} diff --git a/bitcoin-test-utils/src/rpc_helpers.rs b/bitcoin-test-utils/src/rpc_helpers.rs index 8f71327a..5390bc6f 100644 --- a/bitcoin-test-utils/src/rpc_helpers.rs +++ b/bitcoin-test-utils/src/rpc_helpers.rs @@ -1,5 +1,4 @@ use bitcoincore_rpc::{Auth, Client, RpcApi}; -use bitcoincore_rpc_json::AddressType; use std::env; pub const OFFER_PARTY: &str = "alice"; @@ -64,13 +63,13 @@ pub fn init_clients() -> (Client, Client, Client) { let sink_rpc = get_new_wallet_rpc(&rpc, SINK, auth).unwrap(); let offer_address = offer_rpc - .get_new_address(None, Some(AddressType::Bech32)) + .call("getnewaddress", &["".into(), "bech32m".into()]) .unwrap(); let accept_address = accept_rpc - .get_new_address(None, Some(AddressType::Bech32)) + .call("getnewaddress", &["".into(), "bech32m".into()]) .unwrap(); let sink_address = sink_rpc - .get_new_address(None, Some(AddressType::Bech32)) + .call("getnewaddress", &["".into(), "bech32m".into()]) .unwrap(); sink_rpc.generate_to_address(1, &offer_address).unwrap(); diff --git a/dlc-manager/src/contract/contract_info.rs b/dlc-manager/src/contract/contract_info.rs index 5d11e0e7..3924dca3 100644 --- a/dlc-manager/src/contract/contract_info.rs +++ b/dlc-manager/src/contract/contract_info.rs @@ -1,5 +1,6 @@ //! #ContractInfo +use super::ord_descriptor::OrdOutcomeDescriptor; use super::AdaptorInfo; use super::ContractDescriptor; use crate::error::Error; @@ -37,6 +38,9 @@ impl ContractInfo { match &self.contract_descriptor { ContractDescriptor::Enum(e) => Ok(e.get_payouts()), ContractDescriptor::Numerical(n) => n.get_payouts(total_collateral), + ContractDescriptor::Ord(_) => Err(Error::InvalidState( + "Should not call this method for Ord contracts".to_string(), + )), } } @@ -81,7 +85,21 @@ impl ContractInfo { funding_script_pubkey, fund_output_value, ), - _ => unreachable!(), + ContractDescriptor::Ord(o) => match &o.outcome_descriptor { + super::ord_descriptor::OrdOutcomeDescriptor::Enum(e) => { + e.descriptor.get_adaptor_signatures( + secp, + &self.get_oracle_infos(), + self.threshold, + cets, + fund_privkey, + funding_script_pubkey, + fund_output_value, + ) + } + super::ord_descriptor::OrdOutcomeDescriptor::Numerical(_) => unreachable!(), + }, + ContractDescriptor::Numerical(_) => unreachable!(), }, AdaptorInfo::Numerical(trie) => Ok(trie.sign( secp, @@ -140,6 +158,33 @@ impl ContractInfo { adaptor_sigs, adaptor_sig_start, )?), + ContractDescriptor::Ord(o) => match &o.outcome_descriptor { + OrdOutcomeDescriptor::Enum(e) => Ok(e.descriptor.verify_and_get_adaptor_info( + secp, + &oracle_infos, + self.threshold, + fund_pubkey, + funding_script_pubkey, + fund_output_value, + cets, + adaptor_sigs, + adaptor_sig_start, + )?), + OrdOutcomeDescriptor::Numerical(n) => { + Ok(n.descriptor.verify_and_get_adaptor_info_internal( + secp, + &n.get_range_payouts(total_collateral)?, + fund_pubkey, + funding_script_pubkey, + fund_output_value, + self.threshold, + &self.precompute_points(secp)?, + cets, + adaptor_sigs, + adaptor_sig_start, + )?) + } + }, } } @@ -158,6 +203,15 @@ impl ContractInfo { outcomes, adaptor_sig_start, ), + ContractDescriptor::Ord(o) => match &o.outcome_descriptor { + OrdOutcomeDescriptor::Enum(e) => e.descriptor.get_range_info_for_outcome( + self.oracle_announcements.len(), + self.threshold, + outcomes, + adaptor_sig_start, + ), + OrdOutcomeDescriptor::Numerical(_) => unreachable!(), + }, _ => unreachable!(), }, AdaptorInfo::Numerical(n) => { @@ -204,7 +258,26 @@ impl ContractInfo { adaptor_sigs, adaptor_sig_start, )?), - ContractDescriptor::Numerical(_) => match adaptor_info { + ContractDescriptor::Ord(o) + if matches!(&o.outcome_descriptor, OrdOutcomeDescriptor::Enum(_)) => + { + if let OrdOutcomeDescriptor::Enum(e) = &o.outcome_descriptor { + Ok(e.descriptor.verify_adaptor_info( + secp, + &oracle_infos, + self.threshold, + fund_pubkey, + funding_script_pubkey, + fund_output_value, + cets, + adaptor_sigs, + adaptor_sig_start, + )?) + } else { + unreachable!() + } + } + _ => match adaptor_info { AdaptorInfo::Enum => unreachable!(), AdaptorInfo::Numerical(trie) => Ok(trie.verify( secp, @@ -263,6 +336,28 @@ impl ContractInfo { cets, adaptor_index_start, )?), + ContractDescriptor::Ord(o) => match &o.outcome_descriptor { + OrdOutcomeDescriptor::Enum(e) => Ok(e.descriptor.get_adaptor_info( + secp, + &self.get_oracle_infos(), + self.threshold, + fund_priv_key, + funding_script_pubkey, + fund_output_value, + cets, + )?), + OrdOutcomeDescriptor::Numerical(n) => Ok(n.descriptor.get_adaptor_info_internal( + secp, + &n.get_range_payouts(total_collateral)?, + fund_priv_key, + funding_script_pubkey, + fund_output_value, + self.threshold, + &self.precompute_points(secp)?, + cets, + adaptor_index_start, + )?), + }, } } diff --git a/dlc-manager/src/contract/enum_descriptor.rs b/dlc-manager/src/contract/enum_descriptor.rs index 440d9a4b..1fd11e41 100644 --- a/dlc-manager/src/contract/enum_descriptor.rs +++ b/dlc-manager/src/contract/enum_descriptor.rs @@ -1,17 +1,15 @@ //! #EnumDescriptor use super::contract_info::OracleIndexAndPrefixLength; -use super::utils::{get_majority_combination, unordered_equal}; +use super::utils::unordered_equal; use super::AdaptorInfo; use crate::error::Error; use bitcoin::{Script, Transaction}; -use dlc::OracleInfo; +use dlc::{DlcTransactions, OracleInfo, PartyParams}; use dlc::{EnumerationPayout, Payout}; use dlc_messages::oracle_msgs::EnumEventDescriptor; -use dlc_trie::{combination_iterator::CombinationIterator, RangeInfo}; -use secp256k1_zkp::{ - All, EcdsaAdaptorSignature, Message, PublicKey, Secp256k1, SecretKey, Verification, -}; +use dlc_trie::RangeInfo; +use secp256k1_zkp::{All, EcdsaAdaptorSignature, Message, PublicKey, Secp256k1, SecretKey}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -30,10 +28,7 @@ pub struct EnumDescriptor { impl EnumDescriptor { /// Returns the set of payouts. pub fn get_payouts(&self) -> Vec { - self.outcome_payouts - .iter() - .map(|x| x.payout.clone()) - .collect() + self.outcome_payouts.iter().map(|x| x.payout).collect() } /// Validate that the descriptor covers all possible outcomes of the given @@ -63,49 +58,19 @@ impl EnumDescriptor { outcomes: &[(usize, &Vec)], adaptor_sig_start: usize, ) -> Option<(OracleIndexAndPrefixLength, RangeInfo)> { - if outcomes.len() < threshold { - return None; - } - - let filtered_outcomes: Vec<(usize, &Vec)> = outcomes - .iter() - .filter(|x| x.1.len() == 1) - .cloned() - .collect(); - let (mut outcome, mut actual_combination) = get_majority_combination(&filtered_outcomes)?; - let outcome = outcome.remove(0); - - if actual_combination.len() < threshold { - return None; - } - - actual_combination.truncate(threshold); - - let pos = self + let payout_outcomes = self .outcome_payouts .iter() - .position(|x| x.outcome == outcome)?; - - let combinator = CombinationIterator::new(nb_oracles, threshold); - let mut comb_pos = 0; - let mut comb_count = 0; - - for (i, combination) in combinator.enumerate() { - if combination == actual_combination { - comb_pos = i; - } - comb_count += 1; - } - - let range_info = RangeInfo { - cet_index: pos, - adaptor_index: comb_count * pos + comb_pos + adaptor_sig_start, - }; - - Some(( - actual_combination.iter().map(|x| (*x, 1)).collect(), - range_info, - )) + .map(|x| &x.outcome) + .cloned() + .collect::>(); + super::utils::get_range_info_for_enum_outcome( + nb_oracles, + threshold, + outcomes, + &payout_outcomes, + adaptor_sig_start, + ) } /// Verify the given set adaptor signatures. @@ -121,26 +86,19 @@ impl EnumDescriptor { adaptor_sigs: &[EcdsaAdaptorSignature], adaptor_sig_start: usize, ) -> Result { - let mut adaptor_sig_index = adaptor_sig_start; - let mut callback = - |adaptor_point: &PublicKey, cet_index: usize| -> Result<(), dlc::Error> { - let sig = adaptor_sigs[adaptor_sig_index]; - adaptor_sig_index += 1; - dlc::verify_cet_adaptor_sig_from_point( - secp, - &sig, - &cets[cet_index], - adaptor_point, - fund_pubkey, - funding_script_pubkey, - fund_output_value, - )?; - Ok(()) - }; - - self.iter_outcomes(secp, oracle_infos, threshold, &mut callback)?; - - Ok(adaptor_sig_index) + let messages = self.get_outcome_messages(threshold); + super::utils::verify_adaptor_info( + secp, + &messages, + oracle_infos, + threshold, + fund_pubkey, + funding_script_pubkey, + fund_output_value, + cets, + adaptor_sigs, + adaptor_sig_start, + ) } /// Verify the given set of adaptor signature and generates the adaptor info. @@ -206,38 +164,21 @@ impl EnumDescriptor { funding_script_pubkey: &Script, fund_output_value: u64, ) -> Result, Error> { - let mut adaptor_sigs = Vec::new(); - let mut callback = - |adaptor_point: &PublicKey, cet_index: usize| -> Result<(), dlc::Error> { - let sig = dlc::create_cet_adaptor_sig_from_point( - secp, - &cets[cet_index], - adaptor_point, - fund_privkey, - funding_script_pubkey, - fund_output_value, - )?; - adaptor_sigs.push(sig); - Ok(()) - }; - - self.iter_outcomes(secp, oracle_infos, threshold, &mut callback)?; - - Ok(adaptor_sigs) + let messages = self.get_outcome_messages(threshold); + super::utils::get_enum_adaptor_signatures( + secp, + &messages, + oracle_infos, + threshold, + cets, + fund_privkey, + funding_script_pubkey, + fund_output_value, + ) } - fn iter_outcomes( - &self, - secp: &Secp256k1, - oracle_infos: &[OracleInfo], - threshold: usize, - callback: &mut F, - ) -> Result<(), dlc::Error> - where - F: FnMut(&PublicKey, usize) -> Result<(), dlc::Error>, - { - let messages: Vec>> = self - .outcome_payouts + fn get_outcome_messages(&self, threshold: usize) -> Vec>> { + self.outcome_payouts .iter() .map(|x| { let message = vec![Message::from_hashed_data::< @@ -245,32 +186,28 @@ impl EnumDescriptor { >(x.outcome.as_bytes())]; std::iter::repeat(message).take(threshold).collect() }) - .collect(); - let combination_iter = CombinationIterator::new(oracle_infos.len(), threshold); - let combinations: Vec> = combination_iter.collect(); - - for (i, outcome_messages) in messages.iter().enumerate() { - for selector in &combinations { - let cur_oracle_infos: Vec<_> = oracle_infos - .iter() - .enumerate() - .filter_map(|(i, x)| { - if selector.contains(&i) { - Some(x.clone()) - } else { - None - } - }) - .collect(); - let adaptor_point = dlc::get_adaptor_point_from_oracle_info( - secp, - &cur_oracle_infos, - outcome_messages, - )?; - callback(&adaptor_point, i)?; - } - } + .collect() + } - Ok(()) + pub(crate) fn create_dlc_transactions( + &self, + offer_params: &PartyParams, + accept_params: &PartyParams, + refund_locktime: u32, + fee_rate_per_vb: u64, + fund_locktime: u32, + cet_locktime: u32, + fund_output_serial_id: u64, + ) -> Result { + crate::utils::create_dlc_transactions_from_payouts( + offer_params, + accept_params, + &self.get_payouts(), + refund_locktime, + fee_rate_per_vb, + fund_locktime, + cet_locktime, + fund_output_serial_id, + ) } } diff --git a/dlc-manager/src/contract/mod.rs b/dlc-manager/src/contract/mod.rs index ad54d6cd..fdb7b65d 100644 --- a/dlc-manager/src/contract/mod.rs +++ b/dlc-manager/src/contract/mod.rs @@ -22,6 +22,7 @@ pub mod contract_input; pub mod enum_descriptor; pub mod numerical_descriptor; pub mod offered_contract; +pub mod ord_descriptor; pub mod ser; pub mod signed_contract; pub(crate) mod utils; @@ -211,6 +212,8 @@ pub enum ContractDescriptor { Enum(enum_descriptor::EnumDescriptor), /// Case for numerical outcome DLC. Numerical(numerical_descriptor::NumericalDescriptor), + /// Case for a contract involving an ordinal. + Ord(ord_descriptor::OrdDescriptor), } impl ContractDescriptor { @@ -219,6 +222,7 @@ impl ContractDescriptor { match self { ContractDescriptor::Enum(_) => None, ContractDescriptor::Numerical(n) => n.difference_params.clone(), + ContractDescriptor::Ord(_) => None, } } @@ -242,7 +246,7 @@ impl ContractDescriptor { )); } } - _ => { + EventDescriptor::DigitDecompositionEvent(_) => { return Err(Error::InvalidParameters( "Expected enum event descriptor.".to_string(), )) @@ -251,9 +255,24 @@ impl ContractDescriptor { } match self { ContractDescriptor::Enum(ed) => ed.validate(ee), - _ => Err(Error::InvalidParameters( + ContractDescriptor::Numerical(_) => Err(Error::InvalidParameters( "Event descriptor from contract and oracle differ.".to_string(), )), + ContractDescriptor::Ord(ord_desc) => { + match &ord_desc.outcome_descriptor { + ord_descriptor::OrdOutcomeDescriptor::Enum(e) => { + if e.to_offer_payouts.len() != e.descriptor.outcome_payouts.len() { + return Err(Error::InvalidParameters("Ordinal to offer list length differ from outcome list length.".to_string())); + } + e.descriptor.validate(ee) + } + ord_descriptor::OrdOutcomeDescriptor::Numerical(_) => { + Err(Error::InvalidParameters( + "Event descriptor from contract and oracle differ.".to_string(), + )) + } + } + } } } EventDescriptor::DigitDecompositionEvent(_) => match self { @@ -268,6 +287,40 @@ impl ContractDescriptor { })?; n.validate((max_value - 1) as u64) } + ContractDescriptor::Ord(ord_desc) => match &ord_desc.outcome_descriptor { + ord_descriptor::OrdOutcomeDescriptor::Numerical(n) => { + let min_nb_digits = n.descriptor.oracle_numeric_infos.get_min_nb_digits(); + let max_value = n + .descriptor + .oracle_numeric_infos + .base + .checked_pow(min_nb_digits as u32) + .ok_or_else(|| { + Error::InvalidParameters("Could not compute max value".to_string()) + })?; + + for i in 0..n.to_offer_ranges.len() { + if n.to_offer_ranges[i].0 > n.to_offer_ranges[i].1 { + return Err(Error::InvalidParameters( + "Invalid range, start greater than end".to_string(), + )); + } + if i > 0 && n.to_offer_ranges[i - 1].1 >= n.to_offer_ranges[i].0 { + return Err(Error::InvalidParameters( + "Consecutive to offer ranges should not overlap.".to_string(), + )); + } + + if n.to_offer_ranges[i].1 as usize > max_value - 1 { + return Err(Error::InvalidParameters("To offer range end is greater than maximum value for the event.".to_string())); + } + } + n.descriptor.validate((max_value - 1) as u64) + } + ord_descriptor::OrdOutcomeDescriptor::Enum(_) => Err(Error::InvalidParameters( + "Event descriptor from contract and oracle differ.".to_string(), + )), + }, _ => Err(Error::InvalidParameters( "Event descriptor from contract and oracle differ.".to_string(), )), diff --git a/dlc-manager/src/contract/numerical_descriptor.rs b/dlc-manager/src/contract/numerical_descriptor.rs index 99dceec9..a105a00c 100644 --- a/dlc-manager/src/contract/numerical_descriptor.rs +++ b/dlc-manager/src/contract/numerical_descriptor.rs @@ -4,7 +4,7 @@ use super::AdaptorInfo; use crate::error::Error; use crate::payout_curve::{PayoutFunction, RoundingIntervals}; use bitcoin::{Script, Transaction}; -use dlc::{Payout, RangePayout}; +use dlc::{DlcTransactions, PartyParams, Payout, RangePayout}; use dlc_trie::multi_oracle_trie::MultiOracleTrie; use dlc_trie::multi_oracle_trie_with_diff::MultiOracleTrieWithDiff; use dlc_trie::{DlcTrie, OracleNumericInfo}; @@ -73,7 +73,7 @@ impl NumericalDescriptor { Ok(self .get_range_payouts(total_collateral)? .iter() - .map(|x| x.payout.clone()) + .map(|x| x.payout) .collect()) } @@ -90,6 +90,33 @@ impl NumericalDescriptor { cets: &[Transaction], adaptor_pairs: &[EcdsaAdaptorSignature], adaptor_index_start: usize, + ) -> Result<(AdaptorInfo, usize), Error> { + self.verify_and_get_adaptor_info_internal( + secp, + &self.get_range_payouts(total_collateral)?, + fund_pubkey, + funding_script_pubkey, + fund_output_value, + threshold, + precomputed_points, + cets, + adaptor_pairs, + adaptor_index_start, + ) + } + + pub(crate) fn verify_and_get_adaptor_info_internal( + &self, + secp: &Secp256k1, + range_payouts: &[RangePayout], + fund_pubkey: &PublicKey, + funding_script_pubkey: &Script, + fund_output_value: u64, + threshold: usize, + precomputed_points: &[Vec>], + cets: &[Transaction], + adaptor_pairs: &[EcdsaAdaptorSignature], + adaptor_index_start: usize, ) -> Result<(AdaptorInfo, usize), Error> { match &self.difference_params { Some(params) => { @@ -104,7 +131,7 @@ impl NumericalDescriptor { fund_pubkey, funding_script_pubkey, fund_output_value, - &self.get_range_payouts(total_collateral)?, + range_payouts, cets, precomputed_points, adaptor_pairs, @@ -119,7 +146,7 @@ impl NumericalDescriptor { fund_pubkey, funding_script_pubkey, fund_output_value, - &self.get_range_payouts(total_collateral)?, + range_payouts, cets, precomputed_points, adaptor_pairs, @@ -142,6 +169,31 @@ impl NumericalDescriptor { precomputed_points: &[Vec>], cets: &[Transaction], adaptor_index_start: usize, + ) -> Result<(AdaptorInfo, Vec), Error> { + self.get_adaptor_info_internal( + secp, + &self.get_range_payouts(total_collateral)?, + fund_priv_key, + funding_script_pubkey, + fund_output_value, + threshold, + precomputed_points, + cets, + adaptor_index_start, + ) + } + + pub(crate) fn get_adaptor_info_internal( + &self, + secp: &Secp256k1, + range_payouts: &[RangePayout], + fund_priv_key: &SecretKey, + funding_script_pubkey: &Script, + fund_output_value: u64, + threshold: usize, + precomputed_points: &[Vec>], + cets: &[Transaction], + adaptor_index_start: usize, ) -> Result<(AdaptorInfo, Vec), Error> { match &self.difference_params { Some(params) => { @@ -156,7 +208,7 @@ impl NumericalDescriptor { fund_priv_key, funding_script_pubkey, fund_output_value, - &self.get_range_payouts(total_collateral)?, + range_payouts, cets, precomputed_points, adaptor_index_start, @@ -174,7 +226,7 @@ impl NumericalDescriptor { fund_priv_key, funding_script_pubkey, fund_output_value, - &self.get_range_payouts(total_collateral)?, + range_payouts, cets, precomputed_points, adaptor_index_start, @@ -183,4 +235,26 @@ impl NumericalDescriptor { } } } + + pub(crate) fn create_dlc_transactions( + &self, + offer_params: &PartyParams, + accept_params: &PartyParams, + refund_locktime: u32, + fee_rate_per_vb: u64, + fund_locktime: u32, + cet_locktime: u32, + fund_output_serial_id: u64, + ) -> Result { + crate::utils::create_dlc_transactions_from_payouts( + offer_params, + accept_params, + &self.get_payouts(offer_params.collateral + accept_params.collateral)?, + refund_locktime, + fee_rate_per_vb, + fund_locktime, + cet_locktime, + fund_output_serial_id, + ) + } } diff --git a/dlc-manager/src/contract/offered_contract.rs b/dlc-manager/src/contract/offered_contract.rs index 159cc246..eb15d2f4 100644 --- a/dlc-manager/src/contract/offered_contract.rs +++ b/dlc-manager/src/contract/offered_contract.rs @@ -3,10 +3,12 @@ use crate::conversion_utils::{ get_contract_info_and_announcements, get_tx_input_infos, BITCOIN_CHAINHASH, PROTOCOL_VERSION, }; +use crate::error::Error; use crate::utils::get_new_serial_id; use super::contract_info::ContractInfo; use super::contract_input::ContractInput; +use super::ord_descriptor::OrdOutcomeDescriptor; use super::{ContractDescriptor, FundingInputInfo}; use dlc::PartyParams; use dlc_messages::oracle_msgs::OracleAnnouncement; @@ -49,7 +51,7 @@ pub struct OfferedContract { impl OfferedContract { /// Validate that the contract info covers all the possible outcomes that /// can be attested by the oracle(s). - pub fn validate(&self) -> Result<(), crate::error::Error> { + pub fn validate(&self) -> Result<(), Error> { dlc::util::validate_fee_rate(self.fee_rate_per_vb).map_err(|_| { crate::error::Error::InvalidParameters("Fee rate is too high".to_string()) })?; @@ -59,6 +61,12 @@ impl OfferedContract { let payouts = match &info.contract_descriptor { ContractDescriptor::Enum(e) => e.get_payouts(), ContractDescriptor::Numerical(e) => e.get_payouts(self.total_collateral)?, + ContractDescriptor::Ord(o) => match &o.outcome_descriptor { + OrdOutcomeDescriptor::Enum(e) => e.descriptor.get_payouts(), + OrdOutcomeDescriptor::Numerical(n) => { + n.descriptor.get_payouts(self.total_collateral)? + } + }, }; let valid = payouts .iter() @@ -94,7 +102,7 @@ impl OfferedContract { let contract_info = contract .contract_infos .iter() - .zip(oracle_announcements.into_iter()) + .zip(oracle_announcements) .map(|(x, y)| ContractInfo { contract_descriptor: x.contract_descriptor.clone(), oracle_announcements: y, @@ -248,4 +256,32 @@ mod tests { "../../test_inputs/offer_numerical_empty_rounding_interval.json" )); } + + #[test] + fn offer_ord_overlapping_to_offer_ranges() { + validate_offer_test_common(include_str!( + "../../test_inputs/offer_ord_overlapping_to_offer.json" + )); + } + + #[test] + fn offer_ord_invalid_interval_to_offer_ranges() { + validate_offer_test_common(include_str!( + "../../test_inputs/offer_ord_invalid_interval_to_offer.json" + )); + } + + #[test] + fn offer_ord_out_of_bound_to_offer_ranges() { + validate_offer_test_common(include_str!( + "../../test_inputs/offer_ord_out_of_bound_to_offer_ranges.json" + )); + } + + #[test] + fn offer_ord_invalid_enum_to_offer_count() { + validate_offer_test_common(include_str!( + "../../test_inputs/offer_ord_invalid_enum_to_offer_count.json" + )); + } } diff --git a/dlc-manager/src/contract/ord_descriptor.rs b/dlc-manager/src/contract/ord_descriptor.rs new file mode 100644 index 00000000..8822b1f4 --- /dev/null +++ b/dlc-manager/src/contract/ord_descriptor.rs @@ -0,0 +1,375 @@ +//! + +use bitcoin::Transaction; +use dlc::{ + ord::{OrdPayout, SatPoint}, + RangePayout, +}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::error::Error; + +use super::{enum_descriptor::EnumDescriptor, numerical_descriptor::NumericalDescriptor}; + +/// Descriptor for a contract involving an ordinal sat being traded. +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct OrdDescriptor { + /// The descriptor for the contract outcomes. + pub outcome_descriptor: OrdOutcomeDescriptor, + /// The original location of the ordinal within the UTXO set. + pub ordinal_sat_point: SatPoint, + /// The transaction where the UTXO containing the ordinal is located. + pub ordinal_tx: Transaction, + /// Whether it the offer party that should be refunded in case of a contract timeout. + pub refund_offer: bool, +} + +impl OrdDescriptor { + pub(crate) fn get_ord_payouts(&self, total_collateral: u64) -> Result, Error> { + match &self.outcome_descriptor { + OrdOutcomeDescriptor::Enum(e) => { + let payouts = e.descriptor.get_payouts(); + Ok(payouts + .iter() + .zip(&e.to_offer_payouts) + .map(|(x, y)| OrdPayout { + to_offer: *y, + offer: x.offer, + accept: x.accept, + }) + .collect()) + } + OrdOutcomeDescriptor::Numerical(n) => { + let ord_range_payouts = n.get_ord_range_payouts(total_collateral)?; + Ok(ord_range_payouts + .iter() + .map(|x| OrdPayout { + to_offer: x.to_offer, + offer: x.payout.payout.offer, + accept: x.payout.payout.accept, + }) + .collect()) + } + } + } + + pub(crate) fn get_postage(&self) -> u64 { + self.ordinal_tx.output[self.ordinal_sat_point.outpoint.vout as usize].value + } +} + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +/// Descriptor for the outcomes of a contract involving an ordinal. +pub enum OrdOutcomeDescriptor { + /// Descriptor for an event that has enumerated outcomes. + Enum(OrdEnumDescriptor), + /// Descriptor for an event that has numerical outcomes. + Numerical(OrdNumericalDescriptor), +} + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +/// Descriptor for an event that has enumerated outcomes for contracts that involve an ordinal. +pub struct OrdEnumDescriptor { + /// The description of the enumerated outcomes. + pub descriptor: EnumDescriptor, + /// The outcomes for which the ordinal is given to the offer party (one boolean for each + /// possible outcome). + pub to_offer_payouts: Vec, +} + +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +/// Descriptor for an event that has numerical outcomes for contracts that involve an ordinal. +pub struct OrdNumericalDescriptor { + /// The descriptor of the numerical event. + pub descriptor: NumericalDescriptor, + /// The ranges for which the ordinal should be given to the offer party. + pub to_offer_ranges: Vec<(u64, u64)>, +} + +impl OrdNumericalDescriptor { + /// The [`RangePayout`]s for the event. This will likely differ from the one generated by the + /// underlying [`NumericalDescriptor`] if the `to_offer_ranges` do not overlap with the ranges + /// of the numerical event. + pub fn get_range_payouts(&self, total_collateral: u64) -> Result, Error> { + let base = self.descriptor.get_range_payouts(total_collateral)?; + + Ok(build_ord_range_payouts(&base, &self.to_offer_ranges) + .into_iter() + .map(|x| x.payout) + .collect()) + } + + /// The payouts and ordinal assignments for the ranges that cover the event domain. + fn get_ord_range_payouts(&self, total_collateral: u64) -> Result, Error> { + let base = self.descriptor.get_range_payouts(total_collateral)?; + + Ok(build_ord_range_payouts(&base, &self.to_offer_ranges)) + } +} + +fn build_ord_range_payouts( + base: &[RangePayout], + to_offer_ranges: &[(u64, u64)], +) -> Vec { + let mut res = Vec::new(); + let mut start = 0; + let mut cur_offer_range_index = 0; + let mut cur_range_payout_index = 0; + + while cur_offer_range_index < to_offer_ranges.len() || cur_range_payout_index < base.len() { + let (start1, end1) = to_offer_ranges + .get(cur_offer_range_index) + .map_or((usize::MIN, usize::MAX), |x| (x.0 as usize, x.1 as usize)); + let (start2, end2) = base + .get(cur_range_payout_index) + .map_or((usize::MIN, usize::MAX), |x| { + (x.start, x.start + x.count - 1) + }); + start = usize::min(usize::max(start, start1), usize::max(start, start2)); + let n_start = usize::max(start1, start2).saturating_sub(1); + let end = if n_start >= start && n_start < usize::min(end1, end2) { + n_start + } else if end1 < end2 { + cur_offer_range_index += 1; + end1 + } else if end1 == end2 { + cur_range_payout_index += 1; + cur_offer_range_index += 1; + end2 + } else { + cur_range_payout_index += 1; + end2 + }; + res.push(OrdRangePayout { + payout: RangePayout { + start, + count: end - start + 1, + payout: base + .get(cur_range_payout_index) + .map_or(base.last().unwrap().payout, |x| x.payout), + }, + to_offer: to_offer_ranges + .iter() + .any(|x| start as u64 >= x.0 && end as u64 <= x.1), + }); + start = end + 1; + } + + res +} + +#[derive(Debug, PartialEq, Eq)] +struct OrdRangePayout { + payout: RangePayout, + to_offer: bool, +} + +#[cfg(test)] +mod tests { + use bitcoin::{OutPoint, PackedLockTime, Transaction}; + use dlc::{ + ord::{OrdPayout, SatPoint}, + EnumerationPayout, Payout, RangePayout, + }; + + use crate::contract::enum_descriptor::EnumDescriptor; + + use super::{build_ord_range_payouts, OrdDescriptor, OrdOutcomeDescriptor, OrdRangePayout}; + + #[test] + fn build_range_payouts_test() { + let payout = (0..3) + .map(|_| Payout { + offer: 10, + accept: 0, + }) + .collect::>(); + let base = vec![ + RangePayout { + start: 0, + count: 11, + payout: payout[0], + }, + RangePayout { + start: 11, + count: 9, + payout: payout[1], + }, + RangePayout { + start: 20, + count: 11, + payout: payout[2], + }, + ]; + + let to_offer_ranges = vec![(5, 8), (10, 12), (14, 16), (20, 30)]; + + let expected = vec![ + OrdRangePayout { + payout: RangePayout { + start: 0, + count: 5, + payout: payout[0], + }, + to_offer: false, + }, + OrdRangePayout { + payout: RangePayout { + start: 5, + count: 4, + payout: payout[0], + }, + to_offer: true, + }, + OrdRangePayout { + to_offer: false, + payout: RangePayout { + start: 9, + count: 1, + payout: payout[0], + }, + }, + OrdRangePayout { + to_offer: true, + payout: RangePayout { + start: 10, + count: 1, + payout: payout[0], + }, + }, + OrdRangePayout { + to_offer: true, + payout: RangePayout { + start: 11, + count: 2, + payout: payout[1], + }, + }, + OrdRangePayout { + to_offer: false, + payout: RangePayout { + start: 13, + count: 1, + payout: payout[1], + }, + }, + OrdRangePayout { + to_offer: true, + payout: RangePayout { + start: 14, + count: 3, + payout: payout[1], + }, + }, + OrdRangePayout { + to_offer: false, + payout: RangePayout { + start: 17, + count: 3, + payout: payout[1], + }, + }, + OrdRangePayout { + to_offer: true, + payout: RangePayout { + start: 20, + count: 11, + payout: payout[2], + }, + }, + ]; + let actual = build_ord_range_payouts(&base, &to_offer_ranges); + + assert_eq!(expected, actual); + } + + #[test] + fn get_ord_payouts_enum_test() { + let descriptor = OrdDescriptor { + outcome_descriptor: OrdOutcomeDescriptor::Enum(super::OrdEnumDescriptor { + descriptor: EnumDescriptor { + outcome_payouts: vec![ + EnumerationPayout { + outcome: "A".to_string(), + payout: Payout { + offer: 10, + accept: 0, + }, + }, + EnumerationPayout { + outcome: "B".to_string(), + payout: Payout { + offer: 9, + accept: 1, + }, + }, + EnumerationPayout { + outcome: "C".to_string(), + payout: Payout { + offer: 8, + accept: 2, + }, + }, + ], + }, + to_offer_payouts: vec![true, true, false], + }), + ordinal_sat_point: SatPoint { + outpoint: OutPoint::default(), + offset: 0, + }, + ordinal_tx: Transaction { + version: 2, + lock_time: PackedLockTime::ZERO, + input: Vec::new(), + output: Vec::new(), + }, + refund_offer: true, + }; + + let expected = vec![ + OrdPayout { + to_offer: true, + offer: 10, + accept: 0, + }, + OrdPayout { + to_offer: true, + offer: 9, + accept: 1, + }, + OrdPayout { + to_offer: false, + offer: 8, + accept: 2, + }, + ]; + + let actual = descriptor + .get_ord_payouts(10) + .expect("to be able to compute the ord payouts"); + + assert_eq!(expected, actual); + } +} diff --git a/dlc-manager/src/contract/ser.rs b/dlc-manager/src/contract/ser.rs index d5c9a885..b9cfb08c 100644 --- a/dlc-manager/src/contract/ser.rs +++ b/dlc-manager/src/contract/ser.rs @@ -18,7 +18,7 @@ use crate::payout_curve::{ }; use dlc::DlcTransactions; use dlc_messages::ser_impls::{ - read_ecdsa_adaptor_signatures, read_option_cb, read_usize, read_vec, read_vec_cb, + read_ecdsa_adaptor_signatures, read_option_cb, read_usize, read_vec, read_vec_cb, sat_point, write_ecdsa_adaptor_signatures, write_option_cb, write_usize, write_vec, write_vec_cb, }; use dlc_trie::digit_trie::{DigitNodeData, DigitTrieDump}; @@ -30,6 +30,10 @@ use lightning::ln::msgs::DecodeError; use lightning::util::ser::{Readable, Writeable, Writer}; use std::io::Read; +use super::ord_descriptor::{ + OrdDescriptor, OrdEnumDescriptor, OrdNumericalDescriptor, OrdOutcomeDescriptor, +}; + /// Trait used to de/serialize an object to/from a vector of bytes. pub trait Serializable where @@ -79,7 +83,19 @@ impl_dlc_writeable!(HyperbolaPayoutCurvePiece, { (c, float), (d, float) }); -impl_dlc_writeable_enum!(ContractDescriptor, (0, Enum), (1, Numerical);;;); +impl_dlc_writeable!( + OrdDescriptor, + { + (outcome_descriptor, writeable), + (ordinal_sat_point, {cb_writeable, sat_point::write, sat_point::read}), + (ordinal_tx, writeable), + (refund_offer, writeable) + } +); +impl_dlc_writeable_enum!(OrdOutcomeDescriptor, (0, Enum), (1, Numerical);;;); +impl_dlc_writeable!(OrdEnumDescriptor, { (descriptor, writeable), (to_offer_payouts, vec) }); +impl_dlc_writeable!(OrdNumericalDescriptor, {(descriptor, writeable), (to_offer_ranges, vec)}); +impl_dlc_writeable_enum!(ContractDescriptor, (0, Enum), (1, Numerical), (2, Ord);;;); impl_dlc_writeable!(ContractInfo, { (contract_descriptor, writeable), (oracle_announcements, vec), (threshold, usize)}); impl_dlc_writeable!(FundingInputInfo, { (funding_input, writeable), (address, {option_cb, dlc_messages::ser_impls::write_address, dlc_messages::ser_impls::read_address}) }); impl_dlc_writeable!(EnumDescriptor, { diff --git a/dlc-manager/src/contract/utils.rs b/dlc-manager/src/contract/utils.rs index 310df307..ec8ba729 100644 --- a/dlc-manager/src/contract/utils.rs +++ b/dlc-manager/src/contract/utils.rs @@ -1,6 +1,17 @@ use std::collections::HashMap; use std::hash::Hash; +use bitcoin::{Script, Transaction}; +use dlc::OracleInfo; +use dlc_trie::{combination_iterator::CombinationIterator, RangeInfo}; +use secp256k1_zkp::{ + All, EcdsaAdaptorSignature, Message, PublicKey, Secp256k1, SecretKey, Verification, +}; + +use crate::error::Error; + +use super::contract_info::OracleIndexAndPrefixLength; + pub(crate) fn get_majority_combination( outcomes: &[(usize, &Vec)], ) -> Option<(Vec, Vec)> { @@ -39,6 +50,155 @@ pub(super) fn unordered_equal(a: &[T], b: &[T]) -> bool { count(a) == count(b) } +/// Returns the `RangeInfo` that matches the given set of outcomes if any. +pub(crate) fn get_range_info_for_enum_outcome( + nb_oracles: usize, + threshold: usize, + outcomes: &[(usize, &Vec)], + payout_outcomes: &[String], + adaptor_sig_start: usize, +) -> Option<(OracleIndexAndPrefixLength, RangeInfo)> { + if outcomes.len() < threshold { + return None; + } + + let filtered_outcomes: Vec<(usize, &Vec)> = outcomes + .iter() + .filter(|x| x.1.len() == 1) + .cloned() + .collect(); + let (mut outcome, mut actual_combination) = get_majority_combination(&filtered_outcomes)?; + let outcome = outcome.remove(0); + + if actual_combination.len() < threshold { + return None; + } + + actual_combination.truncate(threshold); + + let pos = payout_outcomes.iter().position(|x| x == &outcome)?; + + let combinator = CombinationIterator::new(nb_oracles, threshold); + let mut comb_pos = 0; + let mut comb_count = 0; + + for (i, combination) in combinator.enumerate() { + if combination == actual_combination { + comb_pos = i; + } + comb_count += 1; + } + + let range_info = RangeInfo { + cet_index: pos, + adaptor_index: comb_count * pos + comb_pos + adaptor_sig_start, + }; + + Some(( + actual_combination.iter().map(|x| (*x, 1)).collect(), + range_info, + )) +} + +/// Verify the given set adaptor signatures. +pub fn verify_adaptor_info( + secp: &Secp256k1, + messages: &[Vec>], + oracle_infos: &[OracleInfo], + threshold: usize, + fund_pubkey: &PublicKey, + funding_script_pubkey: &Script, + fund_output_value: u64, + cets: &[Transaction], + adaptor_sigs: &[EcdsaAdaptorSignature], + adaptor_sig_start: usize, +) -> Result { + let mut adaptor_sig_index = adaptor_sig_start; + let mut callback = |adaptor_point: &PublicKey, cet_index: usize| -> Result<(), dlc::Error> { + let sig = adaptor_sigs[adaptor_sig_index]; + adaptor_sig_index += 1; + dlc::verify_cet_adaptor_sig_from_point( + secp, + &sig, + &cets[cet_index], + adaptor_point, + fund_pubkey, + funding_script_pubkey, + fund_output_value, + )?; + Ok(()) + }; + + iter_outcomes(secp, messages, oracle_infos, threshold, &mut callback)?; + + Ok(adaptor_sig_index) +} + +/// Generate the set of adaptor signatures. +pub fn get_enum_adaptor_signatures( + secp: &Secp256k1, + messages: &[Vec>], + oracle_infos: &[OracleInfo], + threshold: usize, + cets: &[Transaction], + fund_privkey: &SecretKey, + funding_script_pubkey: &Script, + fund_output_value: u64, +) -> Result, Error> { + let mut adaptor_sigs = Vec::new(); + let mut callback = |adaptor_point: &PublicKey, cet_index: usize| -> Result<(), dlc::Error> { + let sig = dlc::create_cet_adaptor_sig_from_point( + secp, + &cets[cet_index], + adaptor_point, + fund_privkey, + funding_script_pubkey, + fund_output_value, + )?; + adaptor_sigs.push(sig); + Ok(()) + }; + + iter_outcomes(secp, messages, oracle_infos, threshold, &mut callback)?; + + Ok(adaptor_sigs) +} + +pub(crate) fn iter_outcomes( + secp: &Secp256k1, + messages: &[Vec>], + oracle_infos: &[OracleInfo], + threshold: usize, + callback: &mut F, +) -> Result<(), dlc::Error> +where + F: FnMut(&PublicKey, usize) -> Result<(), dlc::Error>, +{ + let combination_iter = CombinationIterator::new(oracle_infos.len(), threshold); + let combinations: Vec> = combination_iter.collect(); + + for (i, outcome_messages) in messages.iter().enumerate() { + for selector in &combinations { + let cur_oracle_infos: Vec<_> = oracle_infos + .iter() + .enumerate() + .filter_map(|(i, x)| { + if selector.contains(&i) { + Some(x.clone()) + } else { + None + } + }) + .collect(); + let adaptor_point = + dlc::get_adaptor_point_from_oracle_info(secp, &cur_oracle_infos, outcome_messages)?; + callback(&adaptor_point, i)?; + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/dlc-manager/src/contract_updater.rs b/dlc-manager/src/contract_updater.rs index 4fefb8c2..b198442c 100644 --- a/dlc-manager/src/contract_updater.rs +++ b/dlc-manager/src/contract_updater.rs @@ -1,12 +1,13 @@ //! # This module contains static functions to update the state of a DLC. -use std::ops::Deref; +use std::{io::Cursor, ops::Deref}; -use bitcoin::{consensus::Decodable, Script, Transaction, Witness}; -use dlc::{DlcTransactions, PartyParams}; +use bitcoin::{consensus::Decodable, Script, Sequence, Transaction, Witness}; +use dlc::{ord::OrdinalUtxo, DlcTransactions, PartyParams}; use dlc_messages::{ oracle_msgs::{OracleAnnouncement, OracleAttestation}, - AcceptDlc, FundingSignature, FundingSignatures, OfferDlc, SignDlc, WitnessElement, + AcceptDlc, FundingInput, FundingSignature, FundingSignatures, OfferDlc, SignDlc, + WitnessElement, }; use secp256k1_zkp::{ ecdsa::Signature, All, EcdsaAdaptorSignature, PublicKey, Secp256k1, SecretKey, Signing, @@ -16,7 +17,7 @@ use crate::{ contract::{ accepted_contract::AcceptedContract, contract_info::ContractInfo, contract_input::ContractInput, offered_contract::OfferedContract, - signed_contract::SignedContract, AdaptorInfo, FundingInputInfo, + signed_contract::SignedContract, AdaptorInfo, ContractDescriptor, FundingInputInfo, }, conversion_utils::get_tx_input_infos, error::Error, @@ -87,17 +88,7 @@ where blockchain, )?; - let dlc_transactions = dlc::create_dlc_transactions( - &offered_contract.offer_params, - &accept_params, - &offered_contract.contract_info[0].get_payouts(total_collateral)?, - offered_contract.refund_locktime, - offered_contract.fee_rate_per_vb, - 0, - offered_contract.cet_locktime, - offered_contract.fund_output_serial_id, - )?; - + let dlc_transactions = create_dlc_transactions(offered_contract, &accept_params)?; let fund_output_value = dlc_transactions.get_fund_output().value; let (accepted_contract, adaptor_sigs) = accept_contract_internal( @@ -133,7 +124,15 @@ pub(crate) fn accept_contract_internal( let cet_input = dlc_transactions.cets[0].input[0].clone(); - let (adaptor_info, adaptor_sig) = offered_contract.contract_info[0].get_adaptor_info( + let first_contract = &offered_contract.contract_info[0]; + + let is_ord = if let ContractDescriptor::Ord(_) = &first_contract.contract_descriptor { + true + } else { + false + }; + + let (adaptor_info, adaptor_sig) = first_contract.get_adaptor_info( secp, offered_contract.total_collateral, adaptor_secret_key, @@ -155,18 +154,14 @@ pub(crate) fn accept_contract_internal( let mut cets = cets.clone(); for contract_info in offered_contract.contract_info.iter().skip(1) { - let payouts = contract_info.get_payouts(total_collateral)?; - - let tmp_cets = dlc::create_cets( + let tmp_cets = create_cets( + contract_info, + &offered_contract.offer_params, + accept_params, &cet_input, - &offered_contract.offer_params.payout_script_pubkey, - offered_contract.offer_params.payout_serial_id, - &accept_params.payout_script_pubkey, - accept_params.payout_serial_id, - &payouts, - 0, - ); - + total_collateral, + is_ord, + )?; let (adaptor_info, adaptor_sig) = contract_info.get_adaptor_info( secp, offered_contract.total_collateral, @@ -244,18 +239,7 @@ where .map(|x| x.signature) .collect::>(); - let total_collateral = offered_contract.total_collateral; - - let dlc_transactions = dlc::create_dlc_transactions( - &offered_contract.offer_params, - &accept_params, - &offered_contract.contract_info[0].get_payouts(total_collateral)?, - offered_contract.refund_locktime, - offered_contract.fee_rate_per_vb, - 0, - offered_contract.cet_locktime, - offered_contract.fund_output_serial_id, - )?; + let dlc_transactions = create_dlc_transactions(offered_contract, &accept_params)?; let fund_output_value = dlc_transactions.get_fund_output().value; let fund_privkey = signer.get_secret_key_for_pubkey(&offered_contract.offer_params.fund_pubkey)?; @@ -339,23 +323,44 @@ where let mut adaptor_infos = vec![adaptor_info]; + let mut own_funding_inputs_info = offered_contract.funding_inputs_info.clone(); + + let is_ord = if let ContractDescriptor::Ord(o) = + &offered_contract.contract_info[0].contract_descriptor + { + own_funding_inputs_info.insert( + 0, + FundingInputInfo { + funding_input: FundingInput { + input_serial_id: 0, + prev_tx: bitcoin::consensus::encode::serialize(&o.ordinal_tx), + prev_tx_vout: o.ordinal_sat_point.outpoint.vout, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME.into(), + max_witness_len: 107, + redeem_script: Script::default(), + }, + address: None, + }, + ); + + true + } else { + false + }; + let cet_input = cets[0].input[0].clone(); let total_collateral = offered_contract.offer_params.collateral + accept_params.collateral; for contract_info in offered_contract.contract_info.iter().skip(1) { - let payouts = contract_info.get_payouts(total_collateral)?; - - let tmp_cets = dlc::create_cets( + let tmp_cets = create_cets( + contract_info, + &offered_contract.offer_params, + accept_params, &cet_input, - &offered_contract.offer_params.payout_script_pubkey, - offered_contract.offer_params.payout_serial_id, - &accept_params.payout_script_pubkey, - accept_params.payout_serial_id, - &payouts, - 0, - ); - + total_collateral, + is_ord, + )?; let (adaptor_info, tmp_adaptor_index) = contract_info.verify_and_get_adaptor_info( secp, offered_contract.total_collateral, @@ -392,34 +397,26 @@ where own_signatures.extend(sigs); } - let mut input_serial_ids: Vec<_> = offered_contract - .funding_inputs_info - .iter() - .map(|x| x.funding_input.input_serial_id) - .chain(accept_params.inputs.iter().map(|x| x.serial_id)) - .collect(); - input_serial_ids.sort_unstable(); - // Vec - let witnesses: Vec = offered_contract - .funding_inputs_info + let witnesses: Vec = own_funding_inputs_info .iter() .map(|x| { - let input_index = input_serial_ids - .iter() - .position(|y| y == &x.funding_input.input_serial_id) - .ok_or_else(|| { - Error::InvalidState(format!( - "Could not find input for serial id {}", - x.funding_input.input_serial_id - )) - })?; let tx = Transaction::consensus_decode(&mut x.funding_input.prev_tx.as_slice()) .map_err(|_| { Error::InvalidParameters( "Could not decode funding input previous tx parameter".to_string(), ) })?; + let input_index = fund + .input + .iter() + .position(|y| { + y.previous_output.txid == tx.txid() + && y.previous_output.vout == x.funding_input.prev_tx_vout + }) + .ok_or(Error::InvalidParameters( + "Could not find matching input in fund transaction".to_string(), + ))?; let vout = x.funding_input.prev_tx_vout; let tx_out = tx.output.get(vout as usize).ok_or_else(|| { Error::InvalidParameters(format!("Previous tx output not found at index {}", vout)) @@ -445,8 +442,6 @@ where }) .collect::, Error>>()?; - input_serial_ids.sort_unstable(); - let offer_refund_signature = dlc::util::get_raw_sig_for_tx_input( secp, refund, @@ -565,30 +560,47 @@ where )?; } - let mut input_serials: Vec<_> = offered_contract - .funding_inputs_info - .iter() - .chain(accepted_contract.funding_inputs.iter()) - .map(|x| x.funding_input.input_serial_id) - .collect(); - input_serials.sort_unstable(); - let mut fund_tx = accepted_contract.dlc_transactions.fund.clone(); - for (funding_input, funding_signatures) in offered_contract - .funding_inputs_info + let mut funding_inputs_info = offered_contract.funding_inputs_info.clone(); + + if let ContractDescriptor::Ord(o) = &offered_contract.contract_info[0].contract_descriptor { + funding_inputs_info.insert( + 0, + FundingInputInfo { + funding_input: FundingInput { + input_serial_id: 0, + prev_tx: bitcoin::consensus::encode::serialize(&o.ordinal_tx), + prev_tx_vout: o.ordinal_sat_point.outpoint.vout, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME.into(), + max_witness_len: 107, + redeem_script: Script::default(), + }, + address: None, + }, + ); + } + + for (funding_input, funding_signatures) in funding_inputs_info .iter() .zip(funding_signatures.funding_signatures.iter()) { - let input_index = input_serials + let txid = + Transaction::consensus_decode(&mut Cursor::new(&funding_input.funding_input.prev_tx)) + .map_err(|e| { + Error::InvalidParameters(format!("Error decoding transaction: {}", e)) + })? + .txid(); + let input_index = fund_tx + .input .iter() - .position(|x| x == &funding_input.funding_input.input_serial_id) - .ok_or_else(|| { - Error::InvalidState(format!( - "Could not find input for serial id {}", - funding_input.funding_input.input_serial_id - )) - })?; + .position(|y| { + y.previous_output.txid == txid + && y.previous_output.vout == funding_input.funding_input.prev_tx_vout + }) + .ok_or(Error::InvalidParameters( + "Could not find matching input in fund transaction".to_string(), + ))?; fund_tx.input[input_index].witness = Witness::from_vec( funding_signatures @@ -600,15 +612,6 @@ where } for funding_input_info in &accepted_contract.funding_inputs { - let input_index = input_serials - .iter() - .position(|x| x == &funding_input_info.funding_input.input_serial_id) - .ok_or_else(|| { - Error::InvalidState(format!( - "Could not find input for serial id {}", - funding_input_info.funding_input.input_serial_id, - )) - })?; let tx = Transaction::consensus_decode(&mut funding_input_info.funding_input.prev_tx.as_slice()) .map_err(|_| { @@ -620,6 +623,16 @@ where let tx_out = tx.output.get(vout as usize).ok_or_else(|| { Error::InvalidParameters(format!("Previous tx output not found at index {}", vout)) })?; + let input_index = fund_tx + .input + .iter() + .position(|y| { + y.previous_output.txid == tx.txid() + && y.previous_output.vout == funding_input_info.funding_input.prev_tx_vout + }) + .ok_or(Error::InvalidParameters( + "Could not find matching input in fund transaction".to_string(), + ))?; signer.sign_tx_input(&mut fund_tx, input_index, tx_out, None)?; } @@ -735,6 +748,106 @@ where Ok(refund) } +fn create_dlc_transactions( + offered_contract: &OfferedContract, + accept_params: &PartyParams, +) -> Result { + match &offered_contract.contract_info[0].contract_descriptor { + crate::contract::ContractDescriptor::Enum(e) => Ok(e.create_dlc_transactions( + &offered_contract.offer_params, + accept_params, + offered_contract.refund_locktime, + offered_contract.fee_rate_per_vb, + 0, + offered_contract.cet_locktime, + offered_contract.fund_output_serial_id, + )?), + crate::contract::ContractDescriptor::Numerical(n) => Ok(n.create_dlc_transactions( + &offered_contract.offer_params, + accept_params, + offered_contract.refund_locktime, + offered_contract.fee_rate_per_vb, + 0, + offered_contract.cet_locktime, + offered_contract.fund_output_serial_id, + )?), + crate::contract::ContractDescriptor::Ord(o) => { + let ordinal_utxo = OrdinalUtxo { + sat_point: o.ordinal_sat_point, + value: o.ordinal_tx.output[o.ordinal_sat_point.outpoint.vout as usize].value, + }; + + Ok(dlc::ord::create_dlc_transactions( + &offered_contract.offer_params, + accept_params, + &o.get_ord_payouts(offered_contract.total_collateral)?, + &ordinal_utxo, + offered_contract.refund_locktime, + offered_contract.fee_rate_per_vb, + 0, + offered_contract.cet_locktime, + o.refund_offer, + 0, + )?) + } + } +} + +fn create_cets( + contract_info: &ContractInfo, + offer_params: &PartyParams, + accept_params: &PartyParams, + cet_input: &bitcoin::TxIn, + total_collateral: u64, + is_ord: bool, +) -> Result, Error> { + match &contract_info.contract_descriptor { + ContractDescriptor::Ord(_) if !is_ord => Err(Error::InvalidParameters( + "OrdDescriptor cannot be combined with other descripto".to_string(), + )), + ContractDescriptor::Enum(_) | ContractDescriptor::Numerical(_) if is_ord => { + Err(Error::InvalidParameters( + "OrdDescriptor cannot be combined with other descripto".to_string(), + )) + } + ContractDescriptor::Ord(o) => { + let payouts = o.get_ord_payouts(total_collateral)?; + Ok(dlc::ord::create_cets( + &offer_params.payout_script_pubkey, + &accept_params.payout_script_pubkey, + &payouts, + o.get_postage(), + cet_input, + 0, + )) + } + ContractDescriptor::Numerical(n) => { + let payouts = n.get_payouts(total_collateral)?; + Ok(dlc::create_cets( + cet_input, + &offer_params.payout_script_pubkey, + offer_params.payout_serial_id, + &accept_params.payout_script_pubkey, + accept_params.payout_serial_id, + &payouts, + 0, + )) + } + ContractDescriptor::Enum(e) => { + let payouts = e.get_payouts(); + Ok(dlc::create_cets( + cet_input, + &offer_params.payout_script_pubkey, + offer_params.payout_serial_id, + &accept_params.payout_script_pubkey, + accept_params.payout_serial_id, + &payouts, + 0, + )) + } + } +} + #[cfg(test)] mod tests { use std::rc::Rc; diff --git a/dlc-manager/src/conversion_utils.rs b/dlc-manager/src/conversion_utils.rs index 04ebfc1f..dd4962d7 100644 --- a/dlc-manager/src/conversion_utils.rs +++ b/dlc-manager/src/conversion_utils.rs @@ -3,6 +3,9 @@ use crate::contract::{ enum_descriptor::EnumDescriptor, numerical_descriptor::{DifferenceParams, NumericalDescriptor}, offered_contract::OfferedContract, + ord_descriptor::{ + OrdDescriptor, OrdEnumDescriptor, OrdNumericalDescriptor, OrdOutcomeDescriptor, + }, ContractDescriptor, FundingInputInfo, }; use crate::payout_curve::{ @@ -11,10 +14,6 @@ use crate::payout_curve::{ }; use bitcoin::{consensus::encode::Decodable, OutPoint, Transaction}; use dlc::{EnumerationPayout, Payout, TxInputInfo}; -use dlc_messages::oracle_msgs::{ - MultiOracleInfo, OracleInfo as SerOracleInfo, OracleParams, SingleOracleInfo, -}; -use dlc_messages::FundingInput; use dlc_messages::{ contract_msgs::{ ContractDescriptor as SerContractDescriptor, ContractInfo as SerContractInfo, @@ -26,7 +25,15 @@ use dlc_messages::{ RoundingInterval as SerRoundingInterval, RoundingIntervals as SerRoundingIntervals, SingleContractInfo, }, - oracle_msgs::EventDescriptor, + oracle_msgs::{EventDescriptor, OracleAnnouncement}, +}; +use dlc_messages::{ + contract_msgs::{OrdContractDescriptor, OrdEnumContractDescriptor}, + FundingInput, +}; +use dlc_messages::{ + contract_msgs::{OrdContractInfo, OrdNumericalContractDescriptor}, + oracle_msgs::{MultiOracleInfo, OracleInfo as SerOracleInfo, OracleParams, SingleOracleInfo}, }; use dlc_trie::OracleNumericInfo; use std::error; @@ -108,24 +115,41 @@ pub(crate) fn get_contract_info_and_announcements( SerContractInfo::DisjointContractInfo(disjoint) => { (disjoint.total_collateral, disjoint.contract_infos.clone()) } + SerContractInfo::OrdContractInfo(o) => { + let mut threshold = 1; + let announcements = match &o.oracle_info { + SerOracleInfo::Single(single) => vec![single.oracle_announcement.clone()], + SerOracleInfo::Multi(multi) => { + threshold = multi.threshold as usize; + multi.oracle_announcements.clone() + } + }; + return Ok(vec![ContractInfo { + contract_descriptor: ContractDescriptor::Ord(OrdDescriptor { + outcome_descriptor: ord_contract_descriptor_to_ord_outcome_descriptor( + &o.contract_descriptor, + &o.oracle_info, + contract_info.get_total_collateral(), + )?, + ordinal_sat_point: o.ordinal_sat_point, + ordinal_tx: o.ordinal_tx.clone(), + refund_offer: o.refund_offer, + }), + oracle_announcements: announcements, + threshold, + }]); + } }; for contract_info in inner_contract_infos { let (descriptor, oracle_announcements, threshold) = match contract_info.contract_descriptor { SerContractDescriptor::EnumeratedContractDescriptor(enumerated) => { - let outcome_payouts = enumerated - .payouts - .iter() - .map(|x| EnumerationPayout { - outcome: x.outcome.clone(), - payout: Payout { - offer: x.offer_payout, - accept: total_collateral - x.offer_payout, - }, - }) - .collect(); - let descriptor = ContractDescriptor::Enum(EnumDescriptor { outcome_payouts }); + let descriptor = + ContractDescriptor::Enum(enumerated_contract_descriptor_to_enum_descriptor( + &enumerated, + total_collateral, + )); let mut threshold = 1; let announcements = match contract_info.oracle_info { SerOracleInfo::Single(single) => vec![single.oracle_announcement], @@ -148,57 +172,9 @@ pub(crate) fn get_contract_info_and_announcements( (descriptor, announcements, threshold) } SerContractDescriptor::NumericOutcomeContractDescriptor(numeric) => { - let threshold; - let mut difference_params: Option = None; - let announcements = match contract_info.oracle_info { - SerOracleInfo::Single(single) => { - threshold = 1; - vec![single.oracle_announcement] - } - SerOracleInfo::Multi(multi) => { - threshold = multi.threshold; - if let Some(params) = multi.oracle_params { - difference_params = Some(DifferenceParams { - max_error_exp: params.max_error_exp as usize, - min_support_exp: params.min_fail_exp as usize, - maximize_coverage: params.maximize_coverage, - }) - } - multi.oracle_announcements.clone() - } - }; - if announcements.is_empty() { - return Err(Error::InvalidParameters); - } - let expected_base = if let EventDescriptor::DigitDecompositionEvent(d) = - &announcements[0].oracle_event.event_descriptor - { - d.base - } else { - return Err(Error::InvalidParameters); - }; - let nb_digits = announcements - .iter() - .map(|x| match &x.oracle_event.event_descriptor { - EventDescriptor::DigitDecompositionEvent(d) => { - if d.base == expected_base { - Ok(d.nb_digits as usize) - } else { - Err(Error::InvalidParameters) - } - } - _ => Err(Error::InvalidParameters), - }) - .collect::, _>>()?; - let descriptor = ContractDescriptor::Numerical(NumericalDescriptor { - payout_function: (&numeric.payout_function).into(), - rounding_intervals: (&numeric.rounding_intervals).into(), - difference_params, - oracle_numeric_infos: OracleNumericInfo { - base: expected_base as usize, - nb_digits, - }, - }); + let (numeric_descriptor, announcements, threshold) = + get_numeric_contract_descriptor(&numeric, &contract_info.oracle_info)?; + let descriptor = ContractDescriptor::Numerical(numeric_descriptor); (descriptor, announcements, threshold) } }; @@ -212,32 +188,165 @@ pub(crate) fn get_contract_info_and_announcements( Ok(contract_infos) } +fn get_numeric_contract_descriptor( + numeric: &NumericOutcomeContractDescriptor, + oracle_info: &SerOracleInfo, +) -> Result<(NumericalDescriptor, Vec, u16), Error> { + let threshold; + let mut difference_params: Option = None; + let announcements = match oracle_info { + SerOracleInfo::Single(single) => { + threshold = 1; + vec![single.oracle_announcement.clone()] + } + SerOracleInfo::Multi(multi) => { + threshold = multi.threshold; + if let Some(params) = &multi.oracle_params { + difference_params = Some(DifferenceParams { + max_error_exp: params.max_error_exp as usize, + min_support_exp: params.min_fail_exp as usize, + maximize_coverage: params.maximize_coverage, + }) + } + multi.oracle_announcements.clone() + } + }; + if announcements.is_empty() { + return Err(Error::InvalidParameters); + } + let expected_base = if let EventDescriptor::DigitDecompositionEvent(d) = + &announcements[0].oracle_event.event_descriptor + { + d.base + } else { + return Err(Error::InvalidParameters); + }; + + let nb_digits = announcements + .iter() + .map(|x| match &x.oracle_event.event_descriptor { + EventDescriptor::DigitDecompositionEvent(d) => { + if d.base == expected_base { + Ok(d.nb_digits as usize) + } else { + Err(Error::InvalidParameters) + } + } + _ => Err(Error::InvalidParameters), + }) + .collect::, _>>()?; + let numeric_descriptor = NumericalDescriptor { + payout_function: (&numeric.payout_function).into(), + rounding_intervals: (&numeric.rounding_intervals).into(), + difference_params, + oracle_numeric_infos: OracleNumericInfo { + base: expected_base as usize, + nb_digits, + }, + }; + + Ok((numeric_descriptor, announcements, threshold)) +} + impl From<&OfferedContract> for SerContractInfo { fn from(offered_contract: &OfferedContract) -> SerContractInfo { let oracle_infos: Vec = offered_contract.into(); - let mut contract_infos: Vec = offered_contract - .contract_info - .iter() - .zip(oracle_infos.into_iter()) - .map(|(c, o)| ContractInfoInner { - contract_descriptor: (&c.contract_descriptor).into(), - oracle_info: o, - }) - .collect(); - if contract_infos.len() == 1 { - SerContractInfo::SingleContractInfo(SingleContractInfo { + if let ContractDescriptor::Ord(o) = &offered_contract.contract_info[0].contract_descriptor { + SerContractInfo::OrdContractInfo(OrdContractInfo { total_collateral: offered_contract.total_collateral, - contract_info: contract_infos.remove(0), + contract_descriptor: (&o.outcome_descriptor).into(), + ordinal_sat_point: o.ordinal_sat_point, + ordinal_tx: o.ordinal_tx.clone(), + refund_offer: o.refund_offer, + oracle_info: oracle_infos[0].clone(), }) } else { - SerContractInfo::DisjointContractInfo(DisjointContractInfo { - total_collateral: offered_contract.total_collateral, - contract_infos, - }) + let mut contract_infos: Vec = offered_contract + .contract_info + .iter() + .zip(oracle_infos) + .map(|(c, o)| ContractInfoInner { + contract_descriptor: (&c.contract_descriptor).into(), + oracle_info: o, + }) + .collect(); + if contract_infos.len() == 1 { + SerContractInfo::SingleContractInfo(SingleContractInfo { + total_collateral: offered_contract.total_collateral, + contract_info: contract_infos.remove(0), + }) + } else { + SerContractInfo::DisjointContractInfo(DisjointContractInfo { + total_collateral: offered_contract.total_collateral, + contract_infos, + }) + } } } } +impl From<&OrdOutcomeDescriptor> for OrdContractDescriptor { + fn from(value: &OrdOutcomeDescriptor) -> Self { + match value { + OrdOutcomeDescriptor::Enum(e) => { + OrdContractDescriptor::Enum(OrdEnumContractDescriptor { + ord_payouts: e.to_offer_payouts.clone(), + descriptor: (&e.descriptor).into(), + }) + } + OrdOutcomeDescriptor::Numerical(n) => { + OrdContractDescriptor::Numerical(OrdNumericalContractDescriptor { + to_offer_payouts: n.to_offer_ranges.clone(), + descriptor: (&n.descriptor).into(), + }) + } + } + } +} + +fn ord_contract_descriptor_to_ord_outcome_descriptor( + value: &OrdContractDescriptor, + oracle_info: &SerOracleInfo, + total_collateral: u64, +) -> Result { + match value { + OrdContractDescriptor::Enum(e) => Ok(OrdOutcomeDescriptor::Enum(OrdEnumDescriptor { + descriptor: enumerated_contract_descriptor_to_enum_descriptor( + &e.descriptor, + total_collateral, + ), + to_offer_payouts: e.ord_payouts.clone(), + })), + OrdContractDescriptor::Numerical(n) => { + let (numeric_descriptor, _, _) = + get_numeric_contract_descriptor(&n.descriptor, oracle_info)?; + Ok(OrdOutcomeDescriptor::Numerical(OrdNumericalDescriptor { + descriptor: numeric_descriptor, + to_offer_ranges: n.to_offer_payouts.clone(), + })) + } + } +} + +fn enumerated_contract_descriptor_to_enum_descriptor( + value: &EnumeratedContractDescriptor, + total_collateral: u64, +) -> EnumDescriptor { + EnumDescriptor { + outcome_payouts: value + .payouts + .iter() + .map(|x| EnumerationPayout { + outcome: x.outcome.clone(), + payout: Payout { + offer: x.offer_payout, + accept: total_collateral - x.offer_payout, + }, + }) + .collect(), + } +} + impl From<&OfferedContract> for Vec { fn from(offered_contract: &OfferedContract) -> Vec { let mut infos = Vec::new(); @@ -312,6 +421,7 @@ impl From<&ContractDescriptor> for SerContractDescriptor { ContractDescriptor::Numerical(n) => { SerContractDescriptor::NumericOutcomeContractDescriptor(n.into()) } + ContractDescriptor::Ord(_) => unimplemented!(), } } } diff --git a/dlc-manager/src/utils.rs b/dlc-manager/src/utils.rs index 6066a764..d59656b2 100644 --- a/dlc-manager/src/utils.rs +++ b/dlc-manager/src/utils.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use bitcoin::{consensus::Encodable, Txid}; -use dlc::{PartyParams, TxInputInfo}; +use dlc::{DlcTransactions, PartyParams, Payout, TxInputInfo}; use dlc_messages::{ oracle_msgs::{OracleAnnouncement, OracleAttestation}, FundingInput, @@ -185,6 +185,28 @@ pub(crate) fn get_latest_maturity_date( }) } +pub(crate) fn create_dlc_transactions_from_payouts( + offer_params: &PartyParams, + accept_params: &PartyParams, + payouts: &[Payout], + refund_locktime: u32, + fee_rate_per_vb: u64, + fund_locktime: u32, + cet_locktime: u32, + fund_output_serial_id: u64, +) -> Result { + Ok(dlc::create_dlc_transactions( + offer_params, + accept_params, + payouts, + refund_locktime, + fee_rate_per_vb, + fund_locktime, + cet_locktime, + fund_output_serial_id, + )?) +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/dlc-manager/test_inputs/offer_ord_invalid_enum_to_offer_count.json b/dlc-manager/test_inputs/offer_ord_invalid_enum_to_offer_count.json new file mode 100644 index 00000000..c1beeb87 --- /dev/null +++ b/dlc-manager/test_inputs/offer_ord_invalid_enum_to_offer_count.json @@ -0,0 +1,158 @@ +{ + "id": [ + 9, + 164, + 248, + 98, + 221, + 224, + 186, + 237, + 180, + 155, + 198, + 201, + 213, + 182, + 7, + 99, + 192, + 179, + 34, + 214, + 206, + 92, + 177, + 1, + 209, + 63, + 226, + 247, + 31, + 128, + 33, + 115 + ], + "isOfferParty": false, + "contractInfo": [ + { + "contractDescriptor": { + "ord": { + "ordinalSatPoint": { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "offset": 0 + }, + "refundOffer": true, + "ordinalTx": { + "txid": "3bab4655158e9bcb00af8addb5c58c3427d3b32fe0df535ee1546f2994e31eee", + "version": 2, + "lock_time": 0, + "input": [], + "output": [] + }, + "outcomeDescriptor": { + "enum": { + "toOfferPayouts": [ + false, + true, + false + ], + "descriptor": { + "outcomePayouts": [ + { + "outcome": "a", + "payout": { + "offer": 200000000, + "accept": 0 + } + }, + { + "outcome": "b", + "payout": { + "offer": 0, + "accept": 200000000 + } + }, + { + "outcome": "c", + "payout": { + "offer": 200000000, + "accept": 0 + } + }, + { + "outcome": "d", + "payout": { + "offer": 0, + "accept": 200000000 + } + } + ] + } + } + } + } + }, + "oracleAnnouncements": [ + { + "announcementSignature": "706e97a76e5e4c25e2f1c180f7f6b5596304ae1c84c602cb3cb97c5b878ede942e6e4e9cabadb9f3b7ec5eb0370d92b2e8f0b3df05530b111cc69a633bf16908", + "oraclePublicKey": "8a629938a0b7700ae7357c5d4447453aa502d4b644f8c62baad5d406d58b7f6f", + "oracleEvent": { + "oracleNonces": [ + "a49b73a5260e01713f14474ac140476ae30fbbfd5134341db6e6654eb845eb71" + ], + "eventMaturityEpoch": 1623133104, + "eventDescriptor": { + "enumEvent": { + "outcomes": [ + "a", + "b", + "c", + "d" + ] + } + }, + "eventId": "Test" + } + } + ], + "threshold": 1 + } + ], + "counterParty": "0218845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + "offerParams": { + "fundPubkey": "02d962a5e200e3c4cd9d425212d87cf92924d875126f9f6168b3757c6cb2ec419b", + "changeScriptPubkey": "001479ee50e61e88a21baa2b5124b881c188bdf63c37", + "changeSerialId": 13956474821580554639, + "payoutScriptPubkey": "0014ed9c3ca30e6ff4b93c86ab4c7ea5a8efde31b874", + "payoutSerialId": 14595850945083503669, + "inputs": [ + { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "maxWitnessLen": 107, + "redeemScript": "", + "serialId": 4408916189615191417 + } + ], + "inputAmount": 5000000000, + "collateral": 100000000 + }, + "totalCollateral": 200000000, + "fundingInputsInfo": [ + { + "fundingInput": { + "inputSerialId": 4408916189615191417, + "prevTx": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a01000000160014b586157864b9427a2083a765b2b3a927ade49e170000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "prevTxVout": 0, + "sequence": 4294967295, + "maxWitnessLen": 107, + "redeemScript": "" + }, + "address": null + } + ], + "fundOutputSerialId": 16475280753107887427, + "feeRatePerVb": 2, + "cetLocktime": 1623133104, + "refundLocktime": 1623737904 +} diff --git a/dlc-manager/test_inputs/offer_ord_invalid_interval_to_offer.json b/dlc-manager/test_inputs/offer_ord_invalid_interval_to_offer.json new file mode 100644 index 00000000..a4b63605 --- /dev/null +++ b/dlc-manager/test_inputs/offer_ord_invalid_interval_to_offer.json @@ -0,0 +1,192 @@ +{ + "id": [ + 9, + 164, + 248, + 98, + 221, + 224, + 186, + 237, + 180, + 155, + 198, + 201, + 213, + 182, + 7, + 99, + 192, + 179, + 34, + 214, + 206, + 92, + 177, + 1, + 209, + 63, + 226, + 247, + 31, + 128, + 33, + 115 + ], + "isOfferParty": false, + "contractInfo": [ + { + "contractDescriptor": { + "ord": { + "ordinalSatPoint": { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "offset": 0 + }, + "refundOffer": true, + "ordinalTx": { + "txid": "3bab4655158e9bcb00af8addb5c58c3427d3b32fe0df535ee1546f2994e31eee", + "version": 2, + "lock_time": 0, + "input": [], + "output": [] + }, + "outcomeDescriptor": { + "numerical": { + "toOfferRanges": [ + [ + 10, + 12 + ], + [ + 14, + 13 + ] + ], + "descriptor": { + "payoutFunction": { + "payoutFunctionPieces": [ + { + "polynomialPayoutCurvePiece": { + "payoutPoints": [ + { + "eventOutcome": 0, + "outcomePayout": 0, + "extraPrecision": 0 + }, + { + "eventOutcome": 5, + "outcomePayout": 200000000, + "extraPrecision": 0 + } + ] + } + }, + { + "polynomialPayoutCurvePiece": { + "payoutPoints": [ + { + "eventOutcome": 5, + "outcomePayout": 200000000, + "extraPrecision": 0 + }, + { + "eventOutcome": 1023, + "outcomePayout": 200000000, + "extraPrecision": 0 + } + ] + } + } + ] + }, + "roundingIntervals": { + "intervals": [ + { + "beginInterval": 0, + "roundingMod": 1 + } + ] + }, + "differenceParams": null, + "oracleNumericInfos": { + "base": 2, + "nbDigits": [ + 10 + ] + } + } + } + } + } + }, + "oracleAnnouncements": [ + { + "announcementSignature": "706e97a76e5e4c25e2f1c180f7f6b5596304ae1c84c602cb3cb97c5b878ede942e6e4e9cabadb9f3b7ec5eb0370d92b2e8f0b3df05530b111cc69a633bf16908", + "oraclePublicKey": "8a629938a0b7700ae7357c5d4447453aa502d4b644f8c62baad5d406d58b7f6f", + "oracleEvent": { + "oracleNonces": [ + "7bc4eae76b8fa69d241b812e681f535dc93ba171c6d752813ac7710cb401b81b", + "d01ea767509b40360e7a2b3ac1e4caf7c114760ab5e2091de426a6942a55fffc", + "021d1d3b4e33876fc37fe106354a4109dcc064f24aae36d752e303f54ee0436d", + "f7cedc1b24d697098484210110e07ee891569ee678c6616eb70990807e8a38ba", + "b2d587c50e7dbcb0c975595835d966141f396ae849f8745f19cc877099842899", + "9973eafc9436c8fe772e6ba86d35a189a0da03d0d05ae6299d1322ff755932b7", + "d6d2333c3be484672b0ac3c8b4b407db08aa6a2c634240d77fbb39191e7f4658", + "bd65cb669dd9eb467fd748a84f097165210e587a97677fb925be0f45d8d9e142", + "b04bb0e368aaae1657f8ec7b50a9411dd96fdd763fb6732a83df52a95848b2cc", + "51ada014e7194a596efab67568da0d0f622950517f3807a6ec555fcbebfceecd" + ], + "eventMaturityEpoch": 1623133104, + "eventDescriptor": { + "digitDecompositionEvent": { + "base": 2, + "isSigned": false, + "unit": "sats/sec", + "precision": 0, + "nbDigits": 10 + } + }, + "eventId": "Test" + } + } + ], + "threshold": 1 + } + ], + "counterParty": "0218845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + "offerParams": { + "fundPubkey": "02d962a5e200e3c4cd9d425212d87cf92924d875126f9f6168b3757c6cb2ec419b", + "changeScriptPubkey": "001479ee50e61e88a21baa2b5124b881c188bdf63c37", + "changeSerialId": 13956474821580554639, + "payoutScriptPubkey": "0014ed9c3ca30e6ff4b93c86ab4c7ea5a8efde31b874", + "payoutSerialId": 14595850945083503669, + "inputs": [ + { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "maxWitnessLen": 107, + "redeemScript": "", + "serialId": 4408916189615191417 + } + ], + "inputAmount": 5000000000, + "collateral": 100000000 + }, + "totalCollateral": 200000000, + "fundingInputsInfo": [ + { + "fundingInput": { + "inputSerialId": 4408916189615191417, + "prevTx": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a01000000160014b586157864b9427a2083a765b2b3a927ade49e170000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "prevTxVout": 0, + "sequence": 4294967295, + "maxWitnessLen": 107, + "redeemScript": "" + }, + "address": null + } + ], + "fundOutputSerialId": 16475280753107887427, + "feeRatePerVb": 2, + "cetLocktime": 1623133104, + "refundLocktime": 1623737904 +} diff --git a/dlc-manager/test_inputs/offer_ord_out_of_bound_to_offer_ranges.json b/dlc-manager/test_inputs/offer_ord_out_of_bound_to_offer_ranges.json new file mode 100644 index 00000000..ffeb1db8 --- /dev/null +++ b/dlc-manager/test_inputs/offer_ord_out_of_bound_to_offer_ranges.json @@ -0,0 +1,196 @@ +{ + "id": [ + 9, + 164, + 248, + 98, + 221, + 224, + 186, + 237, + 180, + 155, + 198, + 201, + 213, + 182, + 7, + 99, + 192, + 179, + 34, + 214, + 206, + 92, + 177, + 1, + 209, + 63, + 226, + 247, + 31, + 128, + 33, + 115 + ], + "isOfferParty": false, + "contractInfo": [ + { + "contractDescriptor": { + "ord": { + "ordinalSatPoint": { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "offset": 0 + }, + "refundOffer": true, + "ordinalTx": { + "txid": "3bab4655158e9bcb00af8addb5c58c3427d3b32fe0df535ee1546f2994e31eee", + "version": 2, + "lock_time": 0, + "input": [], + "output": [] + }, + "outcomeDescriptor": { + "numerical": { + "toOfferRanges": [ + [ + 10, + 12 + ], + [ + 14, + 15 + ], + [ + 1000, + 1024 + ] + ], + "descriptor": { + "payoutFunction": { + "payoutFunctionPieces": [ + { + "polynomialPayoutCurvePiece": { + "payoutPoints": [ + { + "eventOutcome": 0, + "outcomePayout": 0, + "extraPrecision": 0 + }, + { + "eventOutcome": 5, + "outcomePayout": 200000000, + "extraPrecision": 0 + } + ] + } + }, + { + "polynomialPayoutCurvePiece": { + "payoutPoints": [ + { + "eventOutcome": 5, + "outcomePayout": 200000000, + "extraPrecision": 0 + }, + { + "eventOutcome": 1023, + "outcomePayout": 200000000, + "extraPrecision": 0 + } + ] + } + } + ] + }, + "roundingIntervals": { + "intervals": [ + { + "beginInterval": 0, + "roundingMod": 1 + } + ] + }, + "differenceParams": null, + "oracleNumericInfos": { + "base": 2, + "nbDigits": [ + 10 + ] + } + } + } + } + } + }, + "oracleAnnouncements": [ + { + "announcementSignature": "706e97a76e5e4c25e2f1c180f7f6b5596304ae1c84c602cb3cb97c5b878ede942e6e4e9cabadb9f3b7ec5eb0370d92b2e8f0b3df05530b111cc69a633bf16908", + "oraclePublicKey": "8a629938a0b7700ae7357c5d4447453aa502d4b644f8c62baad5d406d58b7f6f", + "oracleEvent": { + "oracleNonces": [ + "7bc4eae76b8fa69d241b812e681f535dc93ba171c6d752813ac7710cb401b81b", + "d01ea767509b40360e7a2b3ac1e4caf7c114760ab5e2091de426a6942a55fffc", + "021d1d3b4e33876fc37fe106354a4109dcc064f24aae36d752e303f54ee0436d", + "f7cedc1b24d697098484210110e07ee891569ee678c6616eb70990807e8a38ba", + "b2d587c50e7dbcb0c975595835d966141f396ae849f8745f19cc877099842899", + "9973eafc9436c8fe772e6ba86d35a189a0da03d0d05ae6299d1322ff755932b7", + "d6d2333c3be484672b0ac3c8b4b407db08aa6a2c634240d77fbb39191e7f4658", + "bd65cb669dd9eb467fd748a84f097165210e587a97677fb925be0f45d8d9e142", + "b04bb0e368aaae1657f8ec7b50a9411dd96fdd763fb6732a83df52a95848b2cc", + "51ada014e7194a596efab67568da0d0f622950517f3807a6ec555fcbebfceecd" + ], + "eventMaturityEpoch": 1623133104, + "eventDescriptor": { + "digitDecompositionEvent": { + "base": 2, + "isSigned": false, + "unit": "sats/sec", + "precision": 0, + "nbDigits": 10 + } + }, + "eventId": "Test" + } + } + ], + "threshold": 1 + } + ], + "counterParty": "0218845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + "offerParams": { + "fundPubkey": "02d962a5e200e3c4cd9d425212d87cf92924d875126f9f6168b3757c6cb2ec419b", + "changeScriptPubkey": "001479ee50e61e88a21baa2b5124b881c188bdf63c37", + "changeSerialId": 13956474821580554639, + "payoutScriptPubkey": "0014ed9c3ca30e6ff4b93c86ab4c7ea5a8efde31b874", + "payoutSerialId": 14595850945083503669, + "inputs": [ + { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "maxWitnessLen": 107, + "redeemScript": "", + "serialId": 4408916189615191417 + } + ], + "inputAmount": 5000000000, + "collateral": 100000000 + }, + "totalCollateral": 200000000, + "fundingInputsInfo": [ + { + "fundingInput": { + "inputSerialId": 4408916189615191417, + "prevTx": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a01000000160014b586157864b9427a2083a765b2b3a927ade49e170000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "prevTxVout": 0, + "sequence": 4294967295, + "maxWitnessLen": 107, + "redeemScript": "" + }, + "address": null + } + ], + "fundOutputSerialId": 16475280753107887427, + "feeRatePerVb": 2, + "cetLocktime": 1623133104, + "refundLocktime": 1623737904 +} diff --git a/dlc-manager/test_inputs/offer_ord_overlapping_to_offer.json b/dlc-manager/test_inputs/offer_ord_overlapping_to_offer.json new file mode 100644 index 00000000..c8da3f26 --- /dev/null +++ b/dlc-manager/test_inputs/offer_ord_overlapping_to_offer.json @@ -0,0 +1,192 @@ +{ + "id": [ + 9, + 164, + 248, + 98, + 221, + 224, + 186, + 237, + 180, + 155, + 198, + 201, + 213, + 182, + 7, + 99, + 192, + 179, + 34, + 214, + 206, + 92, + 177, + 1, + 209, + 63, + 226, + 247, + 31, + 128, + 33, + 115 + ], + "isOfferParty": false, + "contractInfo": [ + { + "contractDescriptor": { + "ord": { + "ordinalSatPoint": { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "offset": 0 + }, + "refundOffer": true, + "ordinalTx": { + "txid": "3bab4655158e9bcb00af8addb5c58c3427d3b32fe0df535ee1546f2994e31eee", + "version": 2, + "lock_time": 0, + "input": [], + "output": [] + }, + "outcomeDescriptor": { + "numerical": { + "toOfferRanges": [ + [ + 10, + 12 + ], + [ + 12, + 13 + ] + ], + "descriptor": { + "payoutFunction": { + "payoutFunctionPieces": [ + { + "polynomialPayoutCurvePiece": { + "payoutPoints": [ + { + "eventOutcome": 0, + "outcomePayout": 0, + "extraPrecision": 0 + }, + { + "eventOutcome": 5, + "outcomePayout": 200000000, + "extraPrecision": 0 + } + ] + } + }, + { + "polynomialPayoutCurvePiece": { + "payoutPoints": [ + { + "eventOutcome": 5, + "outcomePayout": 200000000, + "extraPrecision": 0 + }, + { + "eventOutcome": 1023, + "outcomePayout": 200000000, + "extraPrecision": 0 + } + ] + } + } + ] + }, + "roundingIntervals": { + "intervals": [ + { + "beginInterval": 0, + "roundingMod": 1 + } + ] + }, + "differenceParams": null, + "oracleNumericInfos": { + "base": 2, + "nbDigits": [ + 10 + ] + } + } + } + } + } + }, + "oracleAnnouncements": [ + { + "announcementSignature": "706e97a76e5e4c25e2f1c180f7f6b5596304ae1c84c602cb3cb97c5b878ede942e6e4e9cabadb9f3b7ec5eb0370d92b2e8f0b3df05530b111cc69a633bf16908", + "oraclePublicKey": "8a629938a0b7700ae7357c5d4447453aa502d4b644f8c62baad5d406d58b7f6f", + "oracleEvent": { + "oracleNonces": [ + "7bc4eae76b8fa69d241b812e681f535dc93ba171c6d752813ac7710cb401b81b", + "d01ea767509b40360e7a2b3ac1e4caf7c114760ab5e2091de426a6942a55fffc", + "021d1d3b4e33876fc37fe106354a4109dcc064f24aae36d752e303f54ee0436d", + "f7cedc1b24d697098484210110e07ee891569ee678c6616eb70990807e8a38ba", + "b2d587c50e7dbcb0c975595835d966141f396ae849f8745f19cc877099842899", + "9973eafc9436c8fe772e6ba86d35a189a0da03d0d05ae6299d1322ff755932b7", + "d6d2333c3be484672b0ac3c8b4b407db08aa6a2c634240d77fbb39191e7f4658", + "bd65cb669dd9eb467fd748a84f097165210e587a97677fb925be0f45d8d9e142", + "b04bb0e368aaae1657f8ec7b50a9411dd96fdd763fb6732a83df52a95848b2cc", + "51ada014e7194a596efab67568da0d0f622950517f3807a6ec555fcbebfceecd" + ], + "eventMaturityEpoch": 1623133104, + "eventDescriptor": { + "digitDecompositionEvent": { + "base": 2, + "isSigned": false, + "unit": "sats/sec", + "precision": 0, + "nbDigits": 10 + } + }, + "eventId": "Test" + } + } + ], + "threshold": 1 + } + ], + "counterParty": "0218845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + "offerParams": { + "fundPubkey": "02d962a5e200e3c4cd9d425212d87cf92924d875126f9f6168b3757c6cb2ec419b", + "changeScriptPubkey": "001479ee50e61e88a21baa2b5124b881c188bdf63c37", + "changeSerialId": 13956474821580554639, + "payoutScriptPubkey": "0014ed9c3ca30e6ff4b93c86ab4c7ea5a8efde31b874", + "payoutSerialId": 14595850945083503669, + "inputs": [ + { + "outpoint": "cac38ae578ed4ce3f32f60b25fa44768bb29c200beb2be80854b56802fbdc10f:0", + "maxWitnessLen": 107, + "redeemScript": "", + "serialId": 4408916189615191417 + } + ], + "inputAmount": 5000000000, + "collateral": 100000000 + }, + "totalCollateral": 200000000, + "fundingInputsInfo": [ + { + "fundingInput": { + "inputSerialId": 4408916189615191417, + "prevTx": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a01000000160014b586157864b9427a2083a765b2b3a927ade49e170000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "prevTxVout": 0, + "sequence": 4294967295, + "maxWitnessLen": 107, + "redeemScript": "" + }, + "address": null + } + ], + "fundOutputSerialId": 16475280753107887427, + "feeRatePerVb": 2, + "cetLocktime": 1623133104, + "refundLocktime": 1623737904 +} diff --git a/dlc-manager/tests/logo-bw.svg b/dlc-manager/tests/logo-bw.svg new file mode 100644 index 00000000..60bb6a34 --- /dev/null +++ b/dlc-manager/tests/logo-bw.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/dlc-manager/tests/ord b/dlc-manager/tests/ord new file mode 100755 index 00000000..c94101df --- /dev/null +++ b/dlc-manager/tests/ord @@ -0,0 +1,3 @@ +#!/bin/sh + +docker run ghcr.io/p2pderivatives/ord:0.10 "$@" diff --git a/dlc-manager/tests/ordinals_tests.rs b/dlc-manager/tests/ordinals_tests.rs new file mode 100644 index 00000000..41045c94 --- /dev/null +++ b/dlc-manager/tests/ordinals_tests.rs @@ -0,0 +1,537 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::{env::current_dir, path::PathBuf, process::Command}; + +use bitcoin::{OutPoint, Transaction}; +use bitcoin_rpc_provider::BitcoinCoreProvider; +use bitcoin_test_utils::rpc_helpers::{init_clients, ACCEPT_PARTY, OFFER_PARTY, SINK}; +use bitcoincore_rpc::{Client, RpcApi}; +use dlc::ord::SatPoint; +use dlc::{EnumerationPayout, Payout}; +use dlc_manager::contract::contract_input::{ContractInput, ContractInputInfo, OracleInput}; +use dlc_manager::contract::enum_descriptor::EnumDescriptor; +use dlc_manager::contract::ord_descriptor::{ + OrdDescriptor, OrdEnumDescriptor, OrdNumericalDescriptor, OrdOutcomeDescriptor, +}; +use dlc_manager::contract::ContractDescriptor; +use dlc_manager::manager::Manager; +use dlc_manager::Oracle; +use dlc_messages::Message; +use dlc_trie::digit_decomposition::decompose_value; +use mocks::memory_storage_provider::MemoryStorage; +use mocks::mock_oracle_provider::MockOracle; +use mocks::mock_time::MockTime; +use secp256k1_zkp::rand::distributions::Uniform; +use secp256k1_zkp::rand::{thread_rng, Rng, RngCore}; +use test_utils::{ + enum_outcomes, get_digit_decomposition_oracle, get_enum_oracles, + get_numerical_contract_descriptor, get_polynomial_payout_curve_pieces, + get_same_num_digits_oracle_numeric_infos, TestParams, ACCEPT_COLLATERAL, EVENT_ID, NB_DIGITS, + OFFER_COLLATERAL, TOTAL_COLLATERAL, +}; + +use crate::test_utils::{max_value, BASE, EVENT_MATURITY}; + +type TestManager = Manager< + Arc, + Arc, + Arc, + Arc, + Arc, + Arc, +>; + +#[derive(Debug, serde::Deserialize, PartialEq, Eq)] +struct InscriptionsOutput { + inscription: String, + location: String, + explorer: String, + postage: u64, +} + +#[macro_use] +#[allow(dead_code)] +mod test_utils; + +const DEFAULT_POSTAGE: u64 = 10000; + +fn ord_binary_path() -> PathBuf { + let mut dir = current_dir().unwrap(); + dir.push("tests"); + dir.push("ord"); + dir +} + +fn ord_command_base() -> Command { + let ord_bin_path = ord_binary_path(); + let mut command = Command::new(&ord_bin_path); + command.arg("-r"); + command.arg("--bitcoin-rpc-user"); + command.arg("testuser"); + command.arg("--bitcoin-rpc-pass"); + command.arg("lq6zequb-gYTdF2_ZEUtr8ywTXzLYtknzWU4nV8uVoo="); + command.arg("--data-dir"); + command.arg("./tests/orddata"); + command +} + +fn ord_command_wallet(name: &str) -> Command { + let mut command = ord_command_base(); + command.arg("--wallet"); + command.arg(name); + command.arg("wallet"); + command +} + +fn create_ordinal_wallet(name: &str) { + let mut command = ord_command_wallet(name); + command.arg("create"); + let mut handle = command.spawn().expect("not to fail"); + handle.wait().unwrap(); +} + +fn inscribe_logo(wallet_name: &str) { + let mut command = ord_command_wallet(wallet_name); + command.arg("inscribe"); + command.arg("--fee-rate"); + command.arg("1"); + command.arg("./tests/logo-bw.svg"); + let mut handle = command.spawn().expect("not to fail"); + handle.wait().unwrap(); +} + +fn get_inscriptions(wallet_name: &str) -> Vec { + let mut command = ord_command_wallet(wallet_name); + command.arg("inscriptions"); + let res = command.output().expect("not to fail"); + serde_json::from_str(&String::from_utf8(res.stdout).unwrap()).unwrap() +} + +fn generate_blocks(sink_rpc: &Client, nb_blocks: u64) { + let prev_blockchain_height = sink_rpc.get_block_count().unwrap(); + + let sink_address = sink_rpc + .call("getnewaddress", &["".into(), "bech32m".into()]) + .expect("RPC Error"); + sink_rpc + .generate_to_address(nb_blocks, &sink_address) + .expect("RPC Error"); + + // Make sure all blocks have been generated + let mut cur_blockchain_height = prev_blockchain_height; + while cur_blockchain_height < prev_blockchain_height + nb_blocks { + std::thread::sleep(std::time::Duration::from_millis(200)); + cur_blockchain_height = sink_rpc.get_block_count().unwrap(); + } +} + +fn get_enum_ord_outcome_descriptor(win_both: bool) -> OrdOutcomeDescriptor { + let outcome_payouts: Vec<_> = enum_outcomes() + .iter() + .enumerate() + .map(|(i, x)| { + let payout = if i % 2 == 0 { + Payout { + offer: TOTAL_COLLATERAL, + accept: 0, + } + } else { + Payout { + offer: 0, + accept: TOTAL_COLLATERAL, + } + }; + EnumerationPayout { + outcome: x.to_owned(), + payout, + } + }) + .collect(); + OrdOutcomeDescriptor::Enum(OrdEnumDescriptor { + to_offer_payouts: outcome_payouts + .iter() + .enumerate() + .map(|(i, _)| i % 2 == (!win_both as usize)) + .collect(), + descriptor: EnumDescriptor { outcome_payouts }, + }) +} + +fn get_numerical_ord_outcome_descriptor() -> OrdOutcomeDescriptor { + let oracle_numeric_infos = get_same_num_digits_oracle_numeric_infos(1); + if let ContractDescriptor::Numerical(numerical) = get_numerical_contract_descriptor( + oracle_numeric_infos.clone(), + get_polynomial_payout_curve_pieces(NB_DIGITS as usize), + None, + ) { + let mut rng = thread_rng(); + let distribution = Uniform::new(0, 2_u64.pow(NB_DIGITS) - 1); + let nb_ranges = rng.gen_range(0..30); + + let mut bounds: Vec<_> = (0..nb_ranges * 2) + .map(|_| rng.sample(distribution)) + .collect(); + + bounds.sort(); + bounds.dedup(); + if bounds.len() % 2 == 1 { + bounds.remove(0); + } + let mut to_offer_ranges = Vec::new(); + + for i in (0..bounds.len()).step_by(2) { + to_offer_ranges.push((bounds[i], bounds[i + 1])); + } + + OrdOutcomeDescriptor::Numerical(OrdNumericalDescriptor { + descriptor: numerical, + to_offer_ranges, + }) + } else { + panic!() + } +} + +fn get_ord_contract_descriptor( + ordinal_sat_point: SatPoint, + ordinal_tx: &Transaction, + outcome_descriptor: &OrdOutcomeDescriptor, +) -> ContractDescriptor { + let ord_descriptor = OrdDescriptor { + ordinal_sat_point, + ordinal_tx: ordinal_tx.clone(), + refund_offer: true, + outcome_descriptor: outcome_descriptor.clone(), + }; + + ContractDescriptor::Ord(ord_descriptor) +} + +fn get_ord_test_params( + nb_oracles: usize, + threshold: usize, + ordinal_sat_point: SatPoint, + ordinal_tx: &Transaction, + outcome_descriptor: &OrdOutcomeDescriptor, +) -> TestParams { + let oracles = match outcome_descriptor { + OrdOutcomeDescriptor::Enum(_) => get_enum_oracles(nb_oracles, threshold), + OrdOutcomeDescriptor::Numerical(_) => { + let mut oracle = get_digit_decomposition_oracle(NB_DIGITS as u16); + let outcome_value = (thread_rng().next_u32() % max_value()) as usize; + let outcomes: Vec<_> = + decompose_value(outcome_value, BASE as usize, NB_DIGITS as usize) + .iter() + .map(|x| x.to_string()) + .collect(); + + oracle.add_attestation(EVENT_ID, &outcomes); + + vec![oracle] + } + }; + let contract_descriptor = + get_ord_contract_descriptor(ordinal_sat_point, ordinal_tx, outcome_descriptor); + let contract_info = ContractInputInfo { + contract_descriptor, + oracles: OracleInput { + public_keys: oracles.iter().map(|x| x.get_public_key()).collect(), + event_id: EVENT_ID.to_owned(), + threshold: threshold as u16, + }, + }; + + let contract_input = ContractInput { + offer_collateral: OFFER_COLLATERAL, + accept_collateral: ACCEPT_COLLATERAL, + fee_rate: 2, + contract_infos: vec![contract_info], + }; + + TestParams { + oracles, + contract_input, + } +} + +#[test] +#[ignore] +fn ordinal_enum_test() { + let outcome_descriptor = get_enum_ord_outcome_descriptor(false); + let (mut alice_manager, mut bob_manager, sink_rpc, inscription, test_params) = + init(&outcome_descriptor); + + execute_contract( + &mut alice_manager, + &mut bob_manager, + &sink_rpc, + &test_params, + ); + + mocks::mock_time::set_time((EVENT_MATURITY as u64) + 1); + + alice_manager.periodic_check().unwrap(); + + generate_blocks(&sink_rpc, 1); + + let outcomes = enum_outcomes(); + let outcome_pos = outcomes + .iter() + .position(|x| { + test_params.oracles[0] + .get_attestation(EVENT_ID) + .unwrap() + .outcomes[0] + == *x + }) + .unwrap(); + + let winner = if outcome_pos % 2 == 1 { + OFFER_PARTY + } else { + ACCEPT_PARTY + }; + + let win_inscriptions = get_inscriptions(winner); + + let inscription = win_inscriptions + .iter() + .find(|x| x.inscription == inscription.inscription) + .expect("To find the inscription"); + let outpoint = location_string_to_outpoint(&inscription.location); + let tx = sink_rpc.get_raw_transaction(&outpoint.txid, None).unwrap(); + assert_eq!(DEFAULT_POSTAGE, tx.output[0].value); + assert_eq!(TOTAL_COLLATERAL, tx.output[1].value); +} + +#[test] +#[ignore] +fn ordinal_enum_win_both_test() { + let outcome_descriptor = get_enum_ord_outcome_descriptor(true); + let (mut alice_manager, mut bob_manager, sink_rpc, inscription, test_params) = + init(&outcome_descriptor); + + execute_contract( + &mut alice_manager, + &mut bob_manager, + &sink_rpc, + &test_params, + ); + + mocks::mock_time::set_time((EVENT_MATURITY as u64) + 1); + + alice_manager.periodic_check().unwrap(); + + generate_blocks(&sink_rpc, 1); + + let outcomes = enum_outcomes(); + let outcome_pos = outcomes + .iter() + .position(|x| { + test_params.oracles[0] + .get_attestation(EVENT_ID) + .unwrap() + .outcomes[0] + == *x + }) + .unwrap(); + + let winner = if outcome_pos % 2 == 0 { + OFFER_PARTY + } else { + ACCEPT_PARTY + }; + + let win_inscriptions = get_inscriptions(winner); + + let inscription = win_inscriptions + .iter() + .find(|x| x.inscription == inscription.inscription) + .expect("To find the inscription"); + let outpoint = location_string_to_outpoint(&inscription.location); + let tx = sink_rpc.get_raw_transaction(&outpoint.txid, None).unwrap(); + assert_eq!(DEFAULT_POSTAGE + TOTAL_COLLATERAL, tx.output[0].value); + assert_eq!(1, tx.output.len()); +} + +#[test] +#[ignore] +fn ordinal_numerical_test() { + let outcome_descriptor = get_numerical_ord_outcome_descriptor(); + let (mut alice_manager, mut bob_manager, sink_rpc, inscription, test_params) = + init(&outcome_descriptor); + + execute_contract( + &mut alice_manager, + &mut bob_manager, + &sink_rpc, + &test_params, + ); + + mocks::mock_time::set_time((EVENT_MATURITY as u64) + 1); + + alice_manager.periodic_check().unwrap(); + + generate_blocks(&sink_rpc, 1); + + let outcome = u64::from_str_radix( + &test_params.oracles[0] + .get_attestation(EVENT_ID) + .unwrap() + .outcomes + .join(""), + 2, + ) + .unwrap(); + + let winner = if let OrdOutcomeDescriptor::Numerical(n) = &outcome_descriptor { + if n.to_offer_ranges + .iter() + .any(|(x, y)| outcome >= *x && outcome <= *y) + { + OFFER_PARTY + } else { + ACCEPT_PARTY + } + } else { + unreachable!(); + }; + + let win_inscriptions = get_inscriptions(winner); + + let _ = win_inscriptions + .iter() + .find(|x| x.inscription == inscription.inscription) + .expect("To find the inscription"); + // let outpoint = location_string_to_outpoint(&inscription.location); + // let tx = sink_rpc.get_raw_transaction(&outpoint.txid, None).unwrap(); + // assert_eq!(DEFAULT_POSTAGE + TOTAL_COLLATERAL, tx.output[0].value); + // assert_eq!(1, tx.output.len()); +} + +fn init( + outcome_descriptor: &OrdOutcomeDescriptor, +) -> ( + TestManager, + TestManager, + Client, + InscriptionsOutput, + TestParams, +) { + create_ordinal_wallet(OFFER_PARTY); + create_ordinal_wallet(ACCEPT_PARTY); + create_ordinal_wallet(SINK); + let (alice_rpc, bob_rpc, sink_rpc) = init_clients(); + inscribe_logo(OFFER_PARTY); + generate_blocks(&sink_rpc, 1); + let mut inscriptions = get_inscriptions(OFFER_PARTY); + let mut inscriptions_outpoints = inscriptions + .iter() + .map(|x| location_string_to_outpoint(&x.location)) + .collect::>(); + inscriptions_outpoints.sort(); + inscriptions_outpoints.dedup(); + + let inscription = inscriptions.remove(0); + + let sat_point_data = inscription.location.split(":").collect::>(); + let sat_point = SatPoint { + outpoint: OutPoint { + txid: sat_point_data[0].parse().unwrap(), + vout: sat_point_data[1].parse().unwrap(), + }, + offset: sat_point_data[2].parse().unwrap(), + }; + + let ordinal_tx = sink_rpc + .get_raw_transaction(&sat_point.outpoint.txid, None) + .unwrap(); + + let alice_store = Arc::new(mocks::memory_storage_provider::MemoryStorage::new()); + let bob_store = Arc::new(mocks::memory_storage_provider::MemoryStorage::new()); + let mock_time = Arc::new(mocks::mock_time::MockTime {}); + mocks::mock_time::set_time((EVENT_MATURITY as u64) - 1); + + let mut alice_oracles = HashMap::with_capacity(1); + let mut bob_oracles = HashMap::with_capacity(1); + + let test_params = get_ord_test_params(1, 1, sat_point, &ordinal_tx, outcome_descriptor); + + for oracle in &test_params.oracles { + let oracle = Arc::new(oracle.clone()); + alice_oracles.insert(oracle.get_public_key(), Arc::clone(&oracle)); + bob_oracles.insert(oracle.get_public_key(), Arc::clone(&oracle)); + } + + let alice_core_client = Arc::new(BitcoinCoreProvider::new_from_rpc_client(alice_rpc).unwrap()); + let bob_core_client = Arc::new(BitcoinCoreProvider::new_from_rpc_client(bob_rpc).unwrap()); + + let alice_manager = Manager::new( + Arc::clone(&alice_core_client), + Arc::clone(&alice_core_client), + alice_store, + alice_oracles, + Arc::clone(&mock_time), + Arc::clone(&alice_core_client), + ) + .unwrap(); + + let bob_manager = Manager::new( + Arc::clone(&bob_core_client), + Arc::clone(&bob_core_client), + bob_store, + bob_oracles, + Arc::clone(&mock_time), + Arc::clone(&bob_core_client), + ) + .unwrap(); + + ( + alice_manager, + bob_manager, + sink_rpc, + inscription, + test_params, + ) +} + +fn execute_contract( + alice_manager: &mut TestManager, + bob_manager: &mut TestManager, + sink_rpc: &Client, + test_params: &TestParams, +) { + let dummy_pubkey = "0218845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166" + .parse() + .unwrap(); + + let offer = alice_manager + .send_offer(&test_params.contract_input, dummy_pubkey) + .unwrap(); + let temporary_contract_id = offer.temporary_contract_id; + + bob_manager + .on_dlc_message(&Message::Offer(offer), dummy_pubkey) + .unwrap(); + + let (_contract_id, _, accept_msg) = bob_manager + .accept_contract_offer(&temporary_contract_id) + .expect("Error accepting contract offer"); + + let sign = alice_manager + .on_dlc_message(&Message::Accept(accept_msg), dummy_pubkey) + .unwrap() + .unwrap(); + + bob_manager.on_dlc_message(&sign, dummy_pubkey).unwrap(); + + generate_blocks(&sink_rpc, 10); +} + +fn location_string_to_outpoint(location: &str) -> OutPoint { + let split_location: Vec<_> = location.split(":").collect(); + OutPoint { + txid: split_location[0].parse().unwrap(), + vout: split_location[1].parse().unwrap(), + } +} diff --git a/dlc-manager/tests/test_utils.rs b/dlc-manager/tests/test_utils.rs index cf90edd1..aef7c4a8 100644 --- a/dlc-manager/tests/test_utils.rs +++ b/dlc-manager/tests/test_utils.rs @@ -255,11 +255,15 @@ pub fn get_enum_oracle() -> MockOracle { } pub fn get_enum_oracles(nb_oracles: usize, threshold: usize) -> Vec { + get_enum_oracles_with_outcome(nb_oracles, threshold, (thread_rng().next_u32() as usize) % 4) +} + +pub fn get_enum_oracles_with_outcome(nb_oracles: usize, threshold: usize, outcome_pos: usize) -> Vec { let mut oracles: Vec<_> = (0..nb_oracles).map(|_| get_enum_oracle()).collect(); let active_oracles = select_active_oracles(nb_oracles, threshold); let outcomes = enum_outcomes(); - let outcome = outcomes[(thread_rng().next_u32() as usize) % outcomes.len()].clone(); + let outcome = outcomes[outcome_pos].clone(); for index in active_oracles { oracles .get_mut(index) diff --git a/dlc-messages/src/channel.rs b/dlc-messages/src/channel.rs index b6a39422..bc211baf 100644 --- a/dlc-messages/src/channel.rs +++ b/dlc-messages/src/channel.rs @@ -134,6 +134,7 @@ impl OfferChannel { c.oracle_info.validate(secp)?; } } + ContractInfo::OrdContractInfo(_) => return Err(Error::InvalidArgument), } Ok(()) diff --git a/dlc-messages/src/contract_msgs.rs b/dlc-messages/src/contract_msgs.rs index 33c8a276..6e6e1eb2 100644 --- a/dlc-messages/src/contract_msgs.rs +++ b/dlc-messages/src/contract_msgs.rs @@ -1,9 +1,13 @@ //! Structure containing information about contract details. +use bitcoin::Transaction; +use dlc::ord::SatPoint; use lightning::ln::msgs::DecodeError; use lightning::util::ser::{Readable, Writeable, Writer}; use oracle_msgs::OracleInfo; +use crate::ser_impls::sat_point; + #[derive(Clone, PartialEq, Debug, Eq)] #[cfg_attr( any(test, feature = "serde"), @@ -33,10 +37,12 @@ pub enum ContractInfo { SingleContractInfo(SingleContractInfo), /// A contract that is based on multiple events. DisjointContractInfo(DisjointContractInfo), + /// A contract which has an ordinal as part of the collateral. + OrdContractInfo(OrdContractInfo), } impl_dlc_writeable_enum!(ContractInfo, - (0, SingleContractInfo), (1, DisjointContractInfo);;; + (0, SingleContractInfo), (1, DisjointContractInfo), (2, OrdContractInfo);;; ); impl ContractInfo { @@ -45,6 +51,7 @@ impl ContractInfo { match self { ContractInfo::SingleContractInfo(v0) => v0.total_collateral, ContractInfo::DisjointContractInfo(v1) => v1.total_collateral, + ContractInfo::OrdContractInfo(v2) => v2.total_collateral, } } @@ -61,6 +68,7 @@ impl ContractInfo { .map(|x| x.oracle_info.get_closest_maturity_date()) .min() .expect("to have at least one element"), + ContractInfo::OrdContractInfo(o) => o.oracle_info.get_closest_maturity_date(), } } } @@ -98,6 +106,87 @@ pub struct DisjointContractInfo { impl_dlc_writeable!(DisjointContractInfo, { (total_collateral, writeable), (contract_infos, vec)}); +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +/// Information for a contract based on a multiple events. +pub struct OrdContractInfo { + /// The total collateral locked in the contract, excluding the ordinal output. + pub total_collateral: u64, + /// The descriptor for the event on which the contract is based. + pub contract_descriptor: OrdContractDescriptor, + /// The current location of the ordinal. + pub ordinal_sat_point: SatPoint, + /// The transaction that includes the outpoint in which the ordinal is currently located. + pub ordinal_tx: Transaction, + /// Whether the offer party should receive the ordinal in case of a contract time out. + pub refund_offer: bool, + /// Oracle information for the contract. + pub oracle_info: OracleInfo, +} + +impl_dlc_writeable!(OrdContractInfo, { + (total_collateral, writeable), + (contract_descriptor, writeable), + (ordinal_sat_point, {cb_writeable, sat_point::write, sat_point::read}), + (ordinal_tx, writeable), + (refund_offer, writeable), + (oracle_info, writeable) +}); + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +/// Contains information about the event on which an ordinal contract is based. +pub enum OrdContractDescriptor { + /// For enumerated events. + Enum(OrdEnumContractDescriptor), + /// For numerical events. + Numerical(OrdNumericalContractDescriptor), +} + +impl_dlc_writeable_enum!(OrdContractDescriptor, (0, Enum), (1, Numerical);;;); + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +/// Contains information about the payouts and ordinal assignment for a contract based on an +/// enumerated event. +pub struct OrdEnumContractDescriptor { + /// The outcomes for which the ordinal is given to the offer party. + pub ord_payouts: Vec, + /// The description of the outcomes and associated payouts. + pub descriptor: EnumeratedContractDescriptor, +} + +impl_dlc_writeable!(OrdEnumContractDescriptor, { (ord_payouts, vec), (descriptor, writeable) }); + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +/// Contains information about the payouts and ordinal assignment for a contract based on a +/// numerical event. +pub struct OrdNumericalContractDescriptor { + /// The ranges within which the ordinal is given to the offer party. + pub to_offer_payouts: Vec<(u64, u64)>, + /// The description of the outcomes and associated payouts. + pub descriptor: NumericOutcomeContractDescriptor, +} + +impl_dlc_writeable!(OrdNumericalContractDescriptor, { (to_offer_payouts, vec), (descriptor, writeable) }); + #[derive(Clone, Debug, PartialEq)] #[cfg_attr( feature = "serde", diff --git a/dlc-messages/src/lib.rs b/dlc-messages/src/lib.rs index 1ba20a1c..2693640a 100644 --- a/dlc-messages/src/lib.rs +++ b/dlc-messages/src/lib.rs @@ -351,6 +351,7 @@ impl OfferDlc { match &self.contract_info { ContractInfo::SingleContractInfo(single) => single.total_collateral, ContractInfo::DisjointContractInfo(disjoint) => disjoint.total_collateral, + ContractInfo::OrdContractInfo(o) => o.total_collateral, } } @@ -372,6 +373,8 @@ impl OfferDlc { c.oracle_info.validate(secp)?; } } + //todo(tibo): validate properly + ContractInfo::OrdContractInfo(o) => o.oracle_info.validate(secp)?, } let closest_maturity_date = self.contract_info.get_closest_maturity_date(); diff --git a/dlc-messages/src/ser_impls.rs b/dlc-messages/src/ser_impls.rs index 52273c07..cf9a014e 100644 --- a/dlc-messages/src/ser_impls.rs +++ b/dlc-messages/src/ser_impls.rs @@ -2,7 +2,8 @@ use bitcoin::network::constants::Network; use bitcoin::Address; -use dlc::{EnumerationPayout, PartyParams, Payout, TxInputInfo}; +use dlc::ord::{OrdinalUtxo, SatPoint}; +use dlc::{ord::OrdPayout, EnumerationPayout, PartyParams, Payout, TxInputInfo}; use lightning::ln::msgs::DecodeError; use lightning::ln::wire::Type; use lightning::util::ser::{Readable, Writeable, Writer}; @@ -627,3 +628,7 @@ impl_dlc_writeable_external!(PartyParams, party_params, { (input_amount, writeable), (collateral, writeable) }); + +impl_dlc_writeable_external!(OrdPayout, ord_payout, { (to_offer, writeable), (offer, writeable), (accept, writeable) }); +impl_dlc_writeable_external!(SatPoint, sat_point, { (outpoint, writeable), (offset, writeable) }); +impl_dlc_writeable_external!(OrdinalUtxo, ordinal_utxo, { (sat_point, {cb_writeable, sat_point::write, sat_point::read}), (value, writeable) }); diff --git a/dlc-trie/src/digit_decomposition.rs b/dlc-trie/src/digit_decomposition.rs index eb1b0060..00b59c7f 100644 --- a/dlc-trie/src/digit_decomposition.rs +++ b/dlc-trie/src/digit_decomposition.rs @@ -77,7 +77,7 @@ pub fn pad_range_payouts( fn take_prefix(start: &mut Vec, end: &mut Vec) -> Vec { if start == end { end.clear(); - return start.drain(0..).collect(); + return std::mem::take(start); } let mut i = 0; while start[i] == end[i] { diff --git a/dlc-trie/src/multi_oracle.rs b/dlc-trie/src/multi_oracle.rs index f3de47c9..66dc4293 100644 --- a/dlc-trie/src/multi_oracle.rs +++ b/dlc-trie/src/multi_oracle.rs @@ -70,7 +70,7 @@ fn compute_min_support_covering_prefix( left_bound .into_iter() - .zip(right_bound.into_iter()) + .zip(right_bound) .take_while(|(x, y)| x == y) .map(|(x, _)| x) .collect() diff --git a/dlc/Cargo.toml b/dlc/Cargo.toml index e2039838..cf7ea7c9 100644 --- a/dlc/Cargo.toml +++ b/dlc/Cargo.toml @@ -6,6 +6,7 @@ license-file = "../LICENSE" name = "dlc" repository = "https://github.com/p2pderivatives/rust-dlc/tree/master/dlc" version = "0.4.0" +edition = "2018" [dependencies] bitcoin = {version = "0.29.2"} diff --git a/dlc/src/lib.rs b/dlc/src/lib.rs index 1d184ef8..eb77a678 100644 --- a/dlc/src/lib.rs +++ b/dlc/src/lib.rs @@ -39,6 +39,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; pub mod channel; +pub mod ord; pub mod secp_utils; pub mod util; @@ -79,10 +80,12 @@ macro_rules! checked_add { }; } +pub(crate) use checked_add; + /// Represents the payouts for a unique contract outcome. Offer party represents /// the initiator of the contract while accept party represents the party /// accepting the contract. -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Eq, PartialEq, Debug, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Payout { /// Payout for the offering party @@ -91,7 +94,7 @@ pub struct Payout { pub accept: u64, } -#[derive(Eq, PartialEq, Debug, Clone)] +#[derive(Eq, PartialEq, Debug, Clone, Copy)] /// Representation of a set of contiguous outcomes that share a single payout. pub struct RangePayout { /// The start of the range @@ -1182,7 +1185,7 @@ mod tests { .script_pubkey() } - fn get_party_params( + pub(crate) fn get_party_params( input_amount: u64, collateral: u64, serial_id: Option, diff --git a/dlc/src/ord.rs b/dlc/src/ord.rs new file mode 100644 index 00000000..8b8c12ee --- /dev/null +++ b/dlc/src/ord.rs @@ -0,0 +1,364 @@ +//! # +//! + +use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::{ + checked_add, make_funding_redeemscript, DlcTransactions, Error, PartyParams, TX_VERSION, +}; + +/// Payout information including ordinal assignment. +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct OrdPayout { + /// Whether it should be the offer party that should receive the ordinal. + pub to_offer: bool, + /// The payout to the offer party. + pub offer: u64, + /// The payout to the accept party. + pub accept: u64, +} + +/// Information about the location of an ordinal in the blockchain. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct SatPoint { + /// The outpoint where the ordinal is located. + pub outpoint: OutPoint, + /// The offset of the ordinal within the output. + pub offset: u64, +} + +/// Description of the location and value of the UTXO containing an ordinal. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct OrdinalUtxo { + /// The location of the ordinal. + pub sat_point: SatPoint, + /// The value of the UTXO. + pub value: u64, +} + +/// Create the set of transactions that make up the DLC contract for a contract including an +/// ordinal as collateral. +pub fn create_dlc_transactions( + offer_params: &PartyParams, + accept_params: &PartyParams, + payouts: &[OrdPayout], + ordinal_utxo: &OrdinalUtxo, + refund_lock_time: u32, + fee_rate_per_vb: u64, + fund_lock_time: u32, + cet_lock_time: u32, + refund_offer: bool, + extra_fee: u64, +) -> Result { + let fund_sequence = crate::util::get_sequence(fund_lock_time); + + let (offer_tx_ins, offer_inputs_serial_ids) = + offer_params.get_unsigned_tx_inputs_and_serial_ids(fund_sequence); + let (accept_tx_ins, accept_inputs_serial_ids) = + accept_params.get_unsigned_tx_inputs_and_serial_ids(fund_sequence); + + let total_collateral = checked_add!(offer_params.collateral, accept_params.collateral)?; + + let (offer_change_output, offer_fund_fee, offer_cet_fee) = + offer_params.get_change_output_and_fees(fee_rate_per_vb, extra_fee)?; + let (accept_change_output, accept_fund_fee, accept_cet_fee) = + accept_params.get_change_output_and_fees(fee_rate_per_vb, extra_fee)?; + + let funding_script_pubkey = + make_funding_redeemscript(&offer_params.fund_pubkey, &accept_params.fund_pubkey); + let fund_output_value = checked_add!( + offer_params.input_amount, + accept_params.input_amount, + ordinal_utxo.value + )? - offer_change_output.value + - accept_change_output.value + - offer_fund_fee + - accept_fund_fee + - extra_fee; + + assert_eq!( + total_collateral + offer_cet_fee + accept_cet_fee + extra_fee + ordinal_utxo.value, + fund_output_value + ); + + assert_eq!( + offer_params.input_amount + accept_params.input_amount + ordinal_utxo.value, + fund_output_value + + offer_change_output.value + + accept_change_output.value + + offer_fund_fee + + accept_fund_fee + + extra_fee + ); + + // The sort for outputs is stable so we don't need to make sure that the change serial ids of + // offer and accept are non zero. + // TODO(tibo): test the case where they are zero. + let mut fund_tx = super::create_funding_transaction( + &funding_script_pubkey, + fund_output_value, + &offer_tx_ins, + &offer_inputs_serial_ids, + &accept_tx_ins, + &accept_inputs_serial_ids, + offer_change_output, + offer_params.change_serial_id, + accept_change_output, + accept_params.change_serial_id, + 0, + fund_lock_time, + ); + + fund_tx.input.insert( + 0, + TxIn { + previous_output: ordinal_utxo.sat_point.outpoint, + script_sig: Script::default(), + sequence: fund_sequence, + witness: Witness::default(), + }, + ); + + let fund_outpoint = OutPoint { + txid: fund_tx.txid(), + vout: crate::util::get_output_for_script_pubkey( + &fund_tx, + &funding_script_pubkey.to_v0_p2wsh(), + ) + .expect("to find the funding script pubkey") + .0 as u32, + }; + let (cets, refund_tx) = create_cets_and_refund_tx( + offer_params, + accept_params, + fund_outpoint, + payouts, + ordinal_utxo.value, + refund_offer, + refund_lock_time, + cet_lock_time, + None, + )?; + + assert_eq!( + ordinal_utxo.sat_point.outpoint, + fund_tx.input[0].previous_output + ); + + Ok(DlcTransactions { + fund: fund_tx, + cets, + refund: refund_tx, + funding_script_pubkey, + }) +} + +/// Generates CETs and redung transactions for a contract including an ordinal as part of the +/// collateral. Fails if the outcomes are not well formed. +pub fn create_cets_and_refund_tx( + offer_params: &PartyParams, + accept_params: &PartyParams, + prev_outpoint: OutPoint, + payouts: &[OrdPayout], + postage: u64, + refund_offer: bool, + refund_lock_time: u32, + cet_lock_time: u32, + cet_nsequence: Option, +) -> Result<(Vec, Transaction), Error> { + let total_collateral = crate::checked_add!(offer_params.collateral, accept_params.collateral)?; + + let cet_input = TxIn { + previous_output: prev_outpoint, + witness: Witness::default(), + script_sig: Script::default(), + sequence: cet_nsequence.unwrap_or_else(|| crate::util::get_sequence(cet_lock_time)), + }; + let has_proper_outcomes = payouts.iter().all(|o| { + let total = o.offer.checked_add(o.accept); + if let Some(total) = total { + total == total_collateral + } else { + false + } + }); + + if !has_proper_outcomes { + return Err(Error::InvalidArgument); + } + + let cets = create_cets( + &offer_params.payout_script_pubkey, + &accept_params.payout_script_pubkey, + payouts, + postage, + &cet_input, + cet_lock_time, + ); + + let offer_refund_output = TxOut { + value: offer_params.collateral, + script_pubkey: offer_params.payout_script_pubkey.clone(), + }; + + let accept_refund_output = TxOut { + value: accept_params.collateral, + script_pubkey: accept_params.payout_script_pubkey.clone(), + }; + + let refund_input = TxIn { + previous_output: prev_outpoint, + witness: Witness::default(), + script_sig: Script::default(), + sequence: Sequence::ENABLE_LOCKTIME_NO_RBF, + }; + + let mut output = if refund_offer { + vec![offer_refund_output, accept_refund_output] + } else { + vec![accept_refund_output, offer_refund_output] + }; + output = crate::util::discard_dust(output, crate::DUST_LIMIT); + let refund_tx = Transaction { + version: TX_VERSION, + lock_time: PackedLockTime(refund_lock_time), + input: vec![refund_input], + output, + }; + Ok((cets, refund_tx)) +} + +/// Generates the CETs for a contract including an ordinal as part of the collateral. +pub fn create_cets( + offer_payout_script_pubkey: &Script, + accept_payout_script_pubkey: &Script, + payouts: &[OrdPayout], + postage: u64, + cet_input: &TxIn, + cet_lock_time: u32, +) -> Vec { + let mut cets: Vec = Vec::new(); + for payout in payouts { + let (offer_payout, accept_payout) = if payout.to_offer { + (payout.offer + postage, payout.accept) + } else { + (payout.offer, payout.accept + postage) + }; + let offer_output = TxOut { + value: offer_payout, + script_pubkey: offer_payout_script_pubkey.clone(), + }; + let accept_output = TxOut { + value: accept_payout, + script_pubkey: accept_payout_script_pubkey.clone(), + }; + + // We use the `to_offer` boolean to order the outputs. If true, we want the offer + // payout to be first, so convert `!to_offer` to u64 will give 0, and inversely. + let tx = crate::create_cet( + offer_output, + (!payout.to_offer) as u64, + accept_output, + payout.to_offer as u64, + cet_input, + cet_lock_time, + ); + + // We make sure that the ordinal cannot be spent as fee. + assert!(tx.output[0].value >= postage); + cets.push(tx); + } + + cets +} + +#[cfg(test)] +mod tests { + use bitcoin::{hashes::hex::FromHex, OutPoint, Txid}; + + use crate::DlcTransactions; + + use super::{create_dlc_transactions, OrdPayout, OrdinalUtxo, SatPoint}; + + const TOTAL_COLLATERAL: u64 = 20000; + + fn get_ordinal_utxo(value: u64, offset: u64) -> OrdinalUtxo { + OrdinalUtxo { + sat_point: SatPoint { + outpoint: OutPoint { + txid: Txid::from_hex( + "6df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456", + ) + .unwrap(), + vout: 0, + }, + offset, + }, + value, + } + } + + fn get_dlc_transactions( + postage: u64, + offset: u64, + serial_ids: Option<(u64, u64)>, + ) -> DlcTransactions { + let ordinal_utxo = get_ordinal_utxo(postage, offset); + let (serial_id1, serial_id2) = serial_ids.map_or((None, None), |(x, y)| (Some(x), Some(y))); + let (offer_params, _) = crate::tests::get_party_params(100000, 10000, serial_id1); + let (accept_params, _) = crate::tests::get_party_params(100000, 10000, serial_id2); + + let payouts = vec![OrdPayout { + to_offer: false, + offer: TOTAL_COLLATERAL, + accept: 0, + }]; + create_dlc_transactions( + &offer_params, + &accept_params, + &payouts, + &ordinal_utxo, + 0, + 2, + 0, + 0, + true, + 0, + ) + .expect("To be able to build transactions") + } + + #[test] + fn create_ord_dlc_transactions_test() { + let dlc_transactions = get_dlc_transactions(10000, 0, None); + assert_eq!(0, dlc_transactions.get_fund_output_index()); + assert_eq!(10000, dlc_transactions.cets[0].output[0].value); + } + + #[test] + fn create_ord_dlc_transactions_with_different_postage_test() { + let dlc_transactions = get_dlc_transactions(25000, 0, None); + assert_eq!(0, dlc_transactions.get_fund_output_index()); + assert_eq!(25000, dlc_transactions.cets[0].output[0].value); + } + + #[test] + fn create_ord_dlc_transactions_with_all_zero_serial_ids_test() { + let dlc_transactions = get_dlc_transactions(10000, 0, Some((0, 0))); + assert_eq!(0, dlc_transactions.get_fund_output_index()); + assert_eq!(10000, dlc_transactions.cets[0].output[0].value); + } +} diff --git a/dlc/src/util.rs b/dlc/src/util.rs index b04806c3..1cd3a1cb 100644 --- a/dlc/src/util.rs +++ b/dlc/src/util.rs @@ -233,9 +233,9 @@ pub(crate) fn discard_dust(txs: Vec, dust_limit: u64) -> Vec { pub(crate) fn get_sequence(lock_time: u32) -> Sequence { if lock_time == 0 { - DISABLE_LOCKTIME + Sequence::MAX } else { - ENABLE_LOCKTIME + Sequence::ENABLE_LOCKTIME_NO_RBF } } diff --git a/docker-compose.yml b/docker-compose.yml index df1bcf87..a7e9beba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,12 +2,13 @@ version: "3.4" services: ### BITCOIND bitcoind: - image: ruimarinho/bitcoin-core:0.20.0 + image: ruimarinho/bitcoin-core:24.0.1 container_name: bitcoin-node command: | -conf=/config/default.conf -printtoconsole -regtest + -txindex ports: # regtest ports - 18443:18443 diff --git a/docs/Ordinal.md b/docs/Ordinal.md new file mode 100644 index 00000000..9ed1238b --- /dev/null +++ b/docs/Ordinal.md @@ -0,0 +1,33 @@ +# Ordinal Support + +It is possible to use an ordinal as part of the collateral of a DLC. +To do so, an [`OrdDescriptor`](../dlc-manager/src/contract/ord_descriptor.rs) must be used. +This descriptor contains information about the ordinal being included in the DLC (location in the blockchain and the transaction that includes is), as well as information about the event upon which the DLC is based. +Event outcomes can be, as for regular contracts, enumerated or numerical. + +## Enumerated events + +Ordinal DLCs based on enumerated events include, in addition to the regular enumerated event information, an array of boolean indicating for each possible outcome whether the ordinal should be given to the offer party (note this means that this array *must* be have the same number of elements as there are possible outcomes). + +## Numerical events + +Ordinal DLCs based on numerical events include, in addition to the regular numerical event information, an array or intervals. +These intervals indicate the ranges of outcomes for which the ordinal should be given to the offer party. + +# Limitations + +## Changing postage + +It is currently not possible to change the postage of the ordinal. +This means that if an ordinal is contained in a 1BTC UTXO, the entire 1BTC will be included in the DLC and given to the ordinal winner. +Changing the postage should thus be done prior to including the ordinal in a DLC. + +In addition, the postage of the ordinal will be merged with the payout of the winner. +This means that if a party is given an ordinal with a postage of 1BTC in addition to a payout of 1BTC, the output of the CET including the ordinal will have a value of 2BTC. + +# How it works + +In order to ensure that the ordinal does not get lost to fee, the DLC transactions for DLCs including an ordinal are created in the following way: +* The ordinal input is always set as the first one in the funding transaction, +* The funding output is always set as the first one in the funding transaction, +* The party getting the ordinal will always have its CET output in the first position. diff --git a/scripts/generate_blocks.sh b/scripts/generate_blocks.sh index 3e6a8a01..ac248b5d 100755 --- a/scripts/generate_blocks.sh +++ b/scripts/generate_blocks.sh @@ -4,4 +4,5 @@ bitcoincli=$(command -v bitcoin-cli) opts=( -rpcuser="testuser" -rpcpassword="lq6zequb-gYTdF2_ZEUtr8ywTXzLYtknzWU4nV8uVoo=" -regtest ) newaddress=$($bitcoincli "${opts[@]}" -rpcwallet=alice getnewaddress bec32) +# newaddress="bcrt1pvt4gqpkzxctz28zssmp8fjawn5nqa73r9809djmyypvyvg5zwnkqfd4mj3" $bitcoincli "${opts[@]}" generatetoaddress 10 ${newaddress} &> /dev/null diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh index 682841b3..dccd2ee0 100755 --- a/scripts/run_integration_tests.sh +++ b/scripts/run_integration_tests.sh @@ -16,8 +16,9 @@ for TEST_NAME in $LIST do if [ ! -z $TEST_NAME ] then - bash ${PWD}/scripts/start_node.sh + docker compose up -d + scripts/wait_for_electrs cargo test -- $TEST_NAME --ignored --exact --nocapture - bash ${PWD}/scripts/stop_node.sh + docker compose down -v fi -done \ No newline at end of file +done