diff --git a/Cargo.lock b/Cargo.lock index a69d3c4..63bfda4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,10 @@ dependencies = [ "rand", "secp", "secp256k1", + "serde", + "serde_cbor", + "serde_json", + "serdect", "sha2", ] @@ -172,6 +176,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hex" version = "0.4.3" @@ -240,6 +250,8 @@ dependencies = [ "rand", "secp", "secp256k1", + "serde", + "serdect", "sha2", "subtle", ] @@ -312,14 +324,16 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "secp" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bc1eb87a1e5b16339a614823bcdf91855ad21bfecc0f8384dedd53b838f5b8" +checksum = "1507279bb0404bb566f85523e48fcf37a158daa5380577ee0d93f3ef4df39ccc" dependencies = [ "base16ct", "once_cell", "rand", "secp256k1", + "serde", + "serdect", "subtle", ] @@ -353,6 +367,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.197" @@ -375,6 +399,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" diff --git a/Cargo.toml b/Cargo.toml index d1d708a..a27d5f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,17 +11,22 @@ keywords = ["dlc", "smart", "contract", "ticket", "auction"] exclude = ["/img"] [dependencies] -bitcoin = { version = "0.31.1", default-features = false, features = ["std"] } -hex = { version = "0.4.3", default-features = false } -musig2 = { version = "0.0.8", default-features = false, features = ["secp256k1", "rand"] } +bitcoin = { version = "0.31.1", default-features = false, features = ["std", "serde"] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +musig2 = { version = "0.0.8", default-features = false, features = ["secp256k1", "rand", "serde"] } rand = { version = "0.8.5", default-features = false } -secp = { version = "0.2.1", default-features = false } +secp = { version = "0.2.3", default-features = false, features = ["serde"] } secp256k1 = { version = "0.28.2", default-features = false, features = ["global-context"] } +serde = { version = "1.0.197", default-features = false, features = ["derive"] } +serdect = { version = "0.2.0", default-features = false, features = ["alloc"] } sha2 = { version = "0.10.8", default-features = false } [dev-dependencies] bitcoincore-rpc = "0.18.0" dotenv = "0.15.0" +serde = { version = "1.0.197", default-features = false, features = ["derive"] } +serde_cbor = { version = "0.11.2", default-features = false, features = ["std"] } +serde_json = { version = "1.0.114", default-features = false, features = [] } [package.metadata.docs.rs] all-features = true diff --git a/src/contract/mod.rs b/src/contract/mod.rs index 9a3c60b..6f58a14 100644 --- a/src/contract/mod.rs +++ b/src/contract/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod split; use bitcoin::{transaction::InputWeightPrediction, Amount, FeeRate, TxOut}; use secp::Point; +use serde::{Deserialize, Serialize}; use crate::{ consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE}, @@ -42,7 +43,7 @@ pub type PayoutWeights = BTreeMap; /// If all players use the same [`ContractParameters`], they should be able to /// construct identical sets of outcome and split transactions, and exchange musig2 /// signatures thereupon. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ContractParameters { /// The market maker who provides capital for the DLC ticketing process. pub market_maker: MarketMaker, @@ -345,9 +346,7 @@ impl ContractParameters { /// Represents a mapping of different signature requirements to some arbitrary type T. /// This can be used to efficiently look up signatures, nonces, etc, for each /// outcome transaction, and for different [`WinCondition`]s within each split transaction. -/// -/// TODO serde serialization -#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] pub struct SigMap { pub by_outcome: BTreeMap, pub by_win_condition: BTreeMap, diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs index 40ef899..dd91326 100644 --- a/src/contract/outcome.rs +++ b/src/contract/outcome.rs @@ -16,7 +16,7 @@ use crate::{ /// Represents the output of building the set of outcome transactions. /// This contains cached data used for constructing further transactions, /// or signing the outcome transactions themselves. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub(crate) struct OutcomeTransactionBuildOutput { outcome_txs: BTreeMap, outcome_spend_infos: BTreeMap, diff --git a/src/contract/split.rs b/src/contract/split.rs index 440ffc9..4ae56b1 100644 --- a/src/contract/split.rs +++ b/src/contract/split.rs @@ -17,7 +17,7 @@ use std::collections::{BTreeMap, BTreeSet}; /// Represents the output of building the set of split transactions. /// This contains cached data used for constructing further transactions, /// or signing the split transactions themselves. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub(crate) struct SplitTransactionBuildOutput { split_txs: BTreeMap, split_spend_infos: BTreeMap, diff --git a/src/lib.rs b/src/lib.rs index ebdf1ae..40c8e84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub(crate) mod contract; pub(crate) mod errors; pub(crate) mod oracles; pub(crate) mod parties; +pub(crate) mod serialization; pub(crate) mod spend_info; pub mod hashlock; @@ -25,6 +26,7 @@ use bitcoin::{ }; use musig2::{AdaptorSignature, AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; use secp::{MaybeScalar, Point, Scalar}; +use serde::{Deserialize, Serialize}; use std::{ borrow::Borrow, @@ -52,7 +54,7 @@ pub use parties::{MarketMaker, Player}; /// in real-world environments a [`TicketedDLC`] could easily encapsulate many thousands of /// transactions involved, consuming megabytes of memory. Cloning it would be extremely /// inefficient and potentially dangerous. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub struct TicketedDLC { params: ContractParameters, funding_outpoint: OutPoint, @@ -104,6 +106,15 @@ impl TicketedDLC { } } +impl std::fmt::Debug for TicketedDLC { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TicketedDLC") + .field("params", self.params()) + .field("funding_outpoint", &self.funding_outpoint) + .finish() + } +} + /// A marker trait used to constrain the API of [`SigningSession`]. pub trait SigningSessionState {} @@ -437,7 +448,7 @@ fn validate_sigmaps_completeness( /// Only some players care about certain outcomes, and a player only enforce one /// specific split TX unlock condition - the one corresponding to their ticket /// hash. We can save bandwidth and -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct ContractSignatures { /// A complete signature on the expiry transaction. Set to `None` if the /// [`ContractParameters::outcome_payouts`] field did not contain an @@ -456,6 +467,7 @@ pub struct ContractSignatures { /// Represents a fully signed and enforceable [`TicketedDLC`], created /// by running a [`SigningSession`]. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct SignedContract { signatures: ContractSignatures, dlc: TicketedDLC, diff --git a/src/oracles.rs b/src/oracles.rs index b07c3ac..3dfab8f 100644 --- a/src/oracles.rs +++ b/src/oracles.rs @@ -1,9 +1,10 @@ use secp::{MaybePoint, MaybeScalar, Point, Scalar}; +use serde::{Deserialize, Serialize}; -use crate::OutcomeIndex; +use crate::{serialization, OutcomeIndex}; /// An oracle's announcement of a future event. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EventAnnouncement { /// The signing oracle's pubkey pub oracle_pubkey: Point, @@ -12,6 +13,7 @@ pub struct EventAnnouncement { pub nonce_point: Point, /// Naive but easy. + #[serde(with = "serialization::vec_of_byte_vecs")] pub outcome_messages: Vec>, /// The unix timestamp beyond which the oracle is considered to have gone AWOL. diff --git a/src/parties.rs b/src/parties.rs index 5a7cf2f..e838a9e 100644 --- a/src/parties.rs +++ b/src/parties.rs @@ -1,9 +1,12 @@ use secp::Point; +use serde::{Deserialize, Serialize}; + +use crate::serialization; /// The agent who provides the on-chain capital to facilitate the ticketed DLC. /// Could be one of the players in the DLC, or could be a neutral 3rd party /// who wishes to profit by leveraging their capital. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct MarketMaker { pub pubkey: Point, } @@ -17,7 +20,7 @@ pub struct MarketMaker { /// ticket hashes, so players might share common pubkeys. However, for the /// economics of the contract to work, every player should be allocated /// their own completely unique ticket hash. -#[derive(Debug, Clone, Ord, PartialOrd, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Ord, PartialOrd, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct Player { /// An ephemeral public key controlled by the player. /// @@ -29,11 +32,13 @@ pub struct Player { /// The ticket hashes used for HTLCs. To buy into the DLC, players must /// purchase the preimages of these hashes. + #[serde(with = "serialization::byte_array")] pub ticket_hash: [u8; 32], /// A hash used for unlocking the split TX output early. To allow winning /// players to receive off-chain payouts, they must provide this `payout_hash`, /// for which they know the preimage. By selling the preimage to the market maker, /// they allow the market maker to reclaim the on-chain funds. + #[serde(with = "serialization::byte_array")] pub payout_hash: [u8; 32], } diff --git a/src/serialization.rs b/src/serialization.rs new file mode 100644 index 0000000..24862ba --- /dev/null +++ b/src/serialization.rs @@ -0,0 +1,328 @@ +use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ContractParameters, Error, Outcome, PlayerIndex, TicketedDLC, WinCondition}; + +use std::borrow::Borrow; + +impl std::fmt::Display for Outcome { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Outcome::Attestation(i) => write!(f, "att{}", i), + Outcome::Expiry => write!(f, "exp"), + } + } +} + +impl std::str::FromStr for Outcome { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "exp" => Ok(Outcome::Expiry), + s => { + let index_str = s.strip_prefix("att").ok_or(Error)?; + let outcome_index = index_str.parse().map_err(|_| Error)?; + Ok(Outcome::Attestation(outcome_index)) + } + } + } +} + +impl Serialize for Outcome { + fn serialize(&self, ser: S) -> Result { + if ser.is_human_readable() { + self.to_string().serialize(ser) + } else { + match self { + &Outcome::Attestation(i) => (i as i64).serialize(ser), + Outcome::Expiry => (-1i64).serialize(ser), + } + } + } +} + +impl<'de> Deserialize<'de> for Outcome { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + s.parse().map_err(|_| { + D::Error::invalid_value( + serde::de::Unexpected::Str(&s), + &"an attestation or expiry outcome string", + ) + }) + } else { + let index = i64::deserialize(deserializer)?; + if index < 0 { + Ok(Outcome::Expiry) + } else { + Ok(Outcome::Attestation(index as usize)) + } + } + } +} + +impl std::fmt::Display for WinCondition { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}:p{}", self.outcome, self.player_index) + } +} + +impl std::str::FromStr for WinCondition { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (prefix, suffix) = s.split_once(":").ok_or(Error)?; + let outcome: Outcome = prefix.parse()?; + let player_index_str = suffix.strip_prefix("p").ok_or(Error)?; + let player_index = player_index_str.parse().map_err(|_| Error)?; + Ok(WinCondition { + outcome, + player_index, + }) + } +} + +impl Serialize for WinCondition { + fn serialize(&self, ser: S) -> Result { + if ser.is_human_readable() { + self.to_string().serialize(ser) + } else { + (self.outcome, self.player_index).serialize(ser) + } + } +} + +impl<'de> Deserialize<'de> for WinCondition { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + s.parse().map_err(|_| { + D::Error::invalid_value(serde::de::Unexpected::Str(&s), &"a win condition string") + }) + } else { + let (outcome, player_index) = <(Outcome, PlayerIndex)>::deserialize(deserializer)?; + Ok(WinCondition { + outcome, + player_index, + }) + } + } +} + +/// Ticketed DLCs can be perfectly reconstructed from their `ContractParameters` +/// and funding outpoint, so to avoid consuming excess bandwidth, we store only +/// these two fields. +#[derive(Serialize, Deserialize)] +struct CompactTicketedDLC> { + params: T, + funding_outpoint: bitcoin::OutPoint, +} + +impl Serialize for TicketedDLC { + fn serialize(&self, ser: S) -> Result { + (CompactTicketedDLC { + params: self.params(), + funding_outpoint: self.funding_outpoint, + }) + .serialize(ser) + } +} + +impl<'de> Deserialize<'de> for TicketedDLC { + fn deserialize>(deserializer: D) -> Result { + let dlc = CompactTicketedDLC::::deserialize(deserializer)?; + TicketedDLC::new(dlc.params, dlc.funding_outpoint).map_err(|err| { + D::Error::custom(format!( + "failed to build transactions from deserialized ContractParameters: {}", + err + )) + }) + } +} + +pub(crate) mod byte_array { + use serde::{Deserializer, Serializer}; + + pub(crate) fn serialize(value: &[u8; 32], ser: S) -> Result { + serdect::array::serialize_hex_lower_or_bin(value, ser) + } + + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result<[u8; 32], D::Error> { + let mut bytes = [0u8; 32]; + serdect::array::deserialize_hex_or_bin(&mut bytes, deserializer)?; + Ok(bytes) + } +} + +pub(crate) mod vec_of_byte_vecs { + use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer}; + use serdect::slice::HexOrBin; + + pub(crate) fn serialize(vecs: &Vec>, ser: S) -> Result { + if !ser.is_human_readable() { + return vecs.serialize(ser); + } + let mut seq = ser.serialize_seq(Some(vecs.len()))?; + for vec in vecs { + let slice: &[u8] = vec.as_ref(); + seq.serialize_element(&hex::encode(slice))?; + } + seq.end() + } + + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result>, D::Error> { + Ok( + Vec::>::deserialize(deserializer)? + .into_iter() + .map(|HexOrBin(vec)| vec) + .collect(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EventAnnouncement, MarketMaker, PayoutWeights, Player}; + + use bitcoin::{Amount, FeeRate}; + use hex::ToHex; + use std::collections::{BTreeMap, BTreeSet}; + + #[test] + fn player_serialization() { + let player = Player { + pubkey: secp::Scalar::try_from(10).unwrap() * secp::G, + ticket_hash: [10; 32], + payout_hash: [20; 32], + }; + + let json_serialized = serde_json::to_string(&player).unwrap(); + assert_eq!( + &json_serialized, + "{\"pubkey\":\"03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7\",\ + \"ticket_hash\":\"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a\",\ + \"payout_hash\":\"1414141414141414141414141414141414141414141414141414141414141414\"}", + ); + + let cbor_serialized_hex: String = serde_cbor::to_vec(&player).unwrap().encode_hex(); + assert_eq!( + &cbor_serialized_hex, + "a3667075626b657998210318a01843184d189e184718f318c8186218351847187c187b181a18\ + e618ae185d1834184218d4189b1819184318c218b7185218a6188e182a184718e2184718c76b\ + 7469636b65745f6861736898200a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a\ + 0a0a0a0a0a0a0a6b7061796f75745f6861736898201414141414141414141414141414141414\ + 141414141414141414141414141414" + ); + } + + #[test] + fn contract_parameters_serialization() { + let params = ContractParameters { + market_maker: MarketMaker { + pubkey: "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7" + .parse() + .unwrap(), + }, + players: BTreeSet::from([ + Player { + pubkey: secp::Scalar::try_from(10).unwrap() * secp::G, + ticket_hash: [10; 32], + payout_hash: [20; 32], + }, + Player { + pubkey: secp::Scalar::try_from(11).unwrap() * secp::G, + ticket_hash: [30; 32], + payout_hash: [40; 32], + }, + ]), + event: EventAnnouncement { + oracle_pubkey: "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7" + .parse() + .unwrap(), + nonce_point: "0317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc5" + .parse() + .unwrap(), + outcome_messages: vec![ + Vec::from(b"option 1"), + Vec::from(b"option 2"), + Vec::from(b"option 3"), + ], + expiry: u32::MAX, + }, + outcome_payouts: BTreeMap::from([ + (Outcome::Attestation(0), PayoutWeights::from([(0, 1)])), + (Outcome::Attestation(1), PayoutWeights::from([(1, 1)])), + ( + Outcome::Attestation(2), + PayoutWeights::from([(0, 1), (1, 1)]), + ), + (Outcome::Expiry, PayoutWeights::from([(0, 1), (1, 1)])), + ]), + fee_rate: FeeRate::from_sat_per_vb_unchecked(100), + funding_value: Amount::from_sat(300_000), + relative_locktime_block_delta: 25, + }; + + let json_serialized = + serde_json::to_string_pretty(¶ms).expect("failed to serialize ContractParameters"); + + let json_expected = r#"{ + "market_maker": { + "pubkey": "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7" + }, + "players": [ + { + "pubkey": "03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb", + "ticket_hash": "1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e", + "payout_hash": "2828282828282828282828282828282828282828282828282828282828282828" + }, + { + "pubkey": "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7", + "ticket_hash": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a", + "payout_hash": "1414141414141414141414141414141414141414141414141414141414141414" + } + ], + "event": { + "oracle_pubkey": "03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7", + "nonce_point": "0317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc5", + "outcome_messages": [ + "6f7074696f6e2031", + "6f7074696f6e2032", + "6f7074696f6e2033" + ], + "expiry": 4294967295 + }, + "outcome_payouts": { + "att0": { + "0": 1 + }, + "att1": { + "1": 1 + }, + "att2": { + "0": 1, + "1": 1 + }, + "exp": { + "0": 1, + "1": 1 + } + }, + "fee_rate": 25000, + "funding_value": 300000, + "relative_locktime_block_delta": 25 +}"#; + + assert_eq!(&json_serialized, json_expected); + + let decoded_params: ContractParameters = serde_json::from_str(&json_serialized) + .expect("failed to deserialize ContractParameters"); + assert_eq!(decoded_params, params); + } +} diff --git a/src/spend_info/funding.rs b/src/spend_info/funding.rs index 0770e35..1a2fe23 100644 --- a/src/spend_info/funding.rs +++ b/src/spend_info/funding.rs @@ -10,7 +10,7 @@ use crate::{ parties::{MarketMaker, Player}, }; -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub(crate) struct FundingSpendInfo { key_agg_ctx: KeyAggContext, funding_value: Amount, diff --git a/src/spend_info/outcome.rs b/src/spend_info/outcome.rs index a4b8416..83ba4d7 100644 --- a/src/spend_info/outcome.rs +++ b/src/spend_info/outcome.rs @@ -36,7 +36,7 @@ use std::{borrow::Borrow, collections::BTreeMap}; /// Once PTLCs are available, we can instead sign the split transaction once /// and distribute adaptor-signatures to each player, encrypted under the /// player's ticket point. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub(crate) struct OutcomeSpendInfo { untweaked_ctx: KeyAggContext, tweaked_ctx: KeyAggContext, diff --git a/src/spend_info/split.rs b/src/spend_info/split.rs index 170139e..3ecf368 100644 --- a/src/spend_info/split.rs +++ b/src/spend_info/split.rs @@ -27,7 +27,7 @@ use crate::{ /// /// 3. A hash-lock which pays to the market maker immediately if they learn the // payout preimage from the player. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] pub(crate) struct SplitSpendInfo { tweaked_ctx: KeyAggContext, spend_info: TaprootSpendInfo, diff --git a/tests/regtest.rs b/tests/regtest.rs index e72f889..76a603e 100644 --- a/tests/regtest.rs +++ b/tests/regtest.rs @@ -52,9 +52,44 @@ fn new_rpc_client() -> BitcoinClient { BitcoinClient::new(&bitcoind_rpc_url, auth).expect("failed to create bitcoind RPC client") } +const FUNDING_VALUE: Amount = Amount::from_sat(200_000); + +/// Make sure we're on the regtest network and we have enough bitcoins +/// in the regtest node wallet, otherwise the actual test will not work. +fn check_regtest_wallet(rpc_client: &BitcoinClient, min_balance: Amount) { + let info = rpc_client + .get_mining_info() + .expect("failed to get network info from remote node"); + + assert_eq!( + info.chain, + bitcoin::Network::Regtest, + "node should be running in regtest mode, found {} instead", + info.chain + ); + + let mut wallet_info = rpc_client.get_wallet_info().unwrap_or_else(|_| { + if let Some(wallet_name) = rpc_client.list_wallet_dir().unwrap().into_iter().next() { + rpc_client.load_wallet(&wallet_name).unwrap(); + } else { + rpc_client + .create_wallet("dlctix_market_maker", None, None, None, None) + .unwrap(); + } + rpc_client.get_wallet_info().unwrap() + }); + + while wallet_info.balance < min_balance { + mine_blocks(&rpc_client, 101).expect("error mining blocks"); + wallet_info = rpc_client.get_wallet_info().unwrap(); + } +} + /// Take some money from the regtest node and deposit it into the given address. /// Return the outpoint and prevout. fn take_usable_utxo(rpc: &BitcoinClient, address: &Address, amount: Amount) -> (OutPoint, TxOut) { + check_regtest_wallet(rpc, amount + Amount::from_sat(50_000)); + let txid: bitcoin::Txid = rpc .call( "sendtoaddress", @@ -177,7 +212,14 @@ fn musig_sign_ticketed_dlc( let pubnonces_by_sender: BTreeMap> = signing_sessions .iter() - .map(|(&sender_pubkey, session)| (sender_pubkey, session.our_public_nonces().clone())) + .map(|(&sender_pubkey, session)| { + // Simulate serialization, as pubnonces are usually sent over a transport channel. + let serialized_nonces = serde_json::to_string(session.our_public_nonces()) + .expect("error serializing pubnonces"); + let received_pubnonces = + serde_json::from_str(&serialized_nonces).expect("error deserializing pubnonces"); + (sender_pubkey, received_pubnonces) + }) .collect(); let signing_sessions: BTreeMap> = @@ -193,7 +235,13 @@ fn musig_sign_ticketed_dlc( let partial_sigs_by_sender: BTreeMap> = signing_sessions .iter() - .map(|(&sender_pubkey, session)| (sender_pubkey, session.our_partial_signatures().clone())) + .map(|(&sender_pubkey, session)| { + let serialized_sigs = serde_json::to_string(session.our_partial_signatures()) + .expect("error serializing partial signatures"); + let received_sigs = serde_json::from_str(&serialized_sigs) + .expect("error deserializing partial signatures"); + (sender_pubkey, received_sigs) + }) .collect(); // Everyone's signatures can be verified by everyone else. @@ -223,42 +271,18 @@ fn musig_sign_ticketed_dlc( } let (_, contract) = signed_contracts.pop_first().unwrap(); - contract -} - -const FUNDING_VALUE: Amount = Amount::from_sat(200_000); - -/// Make sure we're on the regtest network and we have enough bitcoins -/// in the regtest node wallet, otherwise the actual test will not work. -#[test] -fn check_regtest_wallet() { - let rpc_client = new_rpc_client(); - let info = rpc_client - .get_mining_info() - .expect("failed to get network info from remote node"); + // SignedContract should be able to be stored and retrieved via serde serialization. + let decoded_contract = serde_json::from_str( + &serde_json::to_string(&contract).expect("error serializing SignedContract"), + ) + .expect("error deserializing SignedContract"); assert_eq!( - info.chain, - bitcoin::Network::Regtest, - "node should be running in regtest mode, found {} instead", - info.chain + contract, decoded_contract, + "deserialized SignedContract does not match original" ); - let mut wallet_info = rpc_client.get_wallet_info().unwrap_or_else(|_| { - if let Some(wallet_name) = rpc_client.list_wallet_dir().unwrap().into_iter().next() { - rpc_client.load_wallet(&wallet_name).unwrap(); - } else { - rpc_client - .create_wallet("dlctix_market_maker", None, None, None, None) - .unwrap(); - } - rpc_client.get_wallet_info().unwrap() - }); - - while wallet_info.balance < FUNDING_VALUE + Amount::from_sat(100_000) { - mine_blocks(&rpc_client, 101).expect("error mining blocks"); - wallet_info = rpc_client.get_wallet_info().unwrap(); - } + contract } #[test] @@ -338,7 +362,6 @@ fn simple_ticketed_dlc_simulation() { }; // Fund the market maker - let (mm_utxo_outpoint, mm_utxo_prevout) = take_usable_utxo( &rpc, &market_maker_address,