From 3eb37c4d51afa377aa420a39f2655ad54902ee52 Mon Sep 17 00:00:00 2001 From: conduition Date: Sat, 16 Mar 2024 20:27:09 +0000 Subject: [PATCH] add serde serialization trait implementations This allows players and the market maker to transact network sockets, or other transport channels, by passing nonces, sigmaps, etc, back and forth to each other. It also allows parties to store their signed contract data locally, and enforce the contract even after a restart. --- Cargo.lock | 38 ++++- Cargo.toml | 13 +- src/contract/mod.rs | 7 +- src/contract/outcome.rs | 2 +- src/contract/split.rs | 2 +- src/lib.rs | 16 +- src/oracles.rs | 6 +- src/parties.rs | 9 +- src/serialization.rs | 328 ++++++++++++++++++++++++++++++++++++++ src/spend_info/funding.rs | 2 +- src/spend_info/outcome.rs | 2 +- src/spend_info/split.rs | 2 +- tests/regtest.rs | 93 +++++++---- 13 files changed, 464 insertions(+), 56 deletions(-) create mode 100644 src/serialization.rs 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,