mirror of
https://github.com/conduition/dlctix.git
synced 2026-02-23 08:54:28 +01:00
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.
This commit is contained in:
38
Cargo.lock
generated
38
Cargo.lock
generated
@@ -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"
|
||||
|
||||
13
Cargo.toml
13
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
|
||||
|
||||
@@ -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<PlayerIndex, u64>;
|
||||
/// 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<T> {
|
||||
pub by_outcome: BTreeMap<Outcome, T>,
|
||||
pub by_win_condition: BTreeMap<WinCondition, T>,
|
||||
|
||||
@@ -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, Transaction>,
|
||||
outcome_spend_infos: BTreeMap<Outcome, OutcomeSpendInfo>,
|
||||
|
||||
@@ -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<Outcome, Transaction>,
|
||||
split_spend_infos: BTreeMap<WinCondition, SplitSpendInfo>,
|
||||
|
||||
16
src/lib.rs
16
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<T>(
|
||||
/// 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,
|
||||
|
||||
@@ -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<Vec<u8>>,
|
||||
|
||||
/// The unix timestamp beyond which the oracle is considered to have gone AWOL.
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
328
src/serialization.rs
Normal file
328
src/serialization.rs
Normal file
@@ -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<Self, Self::Err> {
|
||||
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<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
|
||||
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<D: Deserializer<'de>>(deserializer: D) -> Result<Outcome, D::Error> {
|
||||
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<Self, Self::Err> {
|
||||
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<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
|
||||
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<D: Deserializer<'de>>(deserializer: D) -> Result<WinCondition, D::Error> {
|
||||
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<T: Borrow<ContractParameters>> {
|
||||
params: T,
|
||||
funding_outpoint: bitcoin::OutPoint,
|
||||
}
|
||||
|
||||
impl Serialize for TicketedDLC {
|
||||
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
|
||||
(CompactTicketedDLC {
|
||||
params: self.params(),
|
||||
funding_outpoint: self.funding_outpoint,
|
||||
})
|
||||
.serialize(ser)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TicketedDLC {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<TicketedDLC, D::Error> {
|
||||
let dlc = CompactTicketedDLC::<ContractParameters>::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<S: Serializer>(value: &[u8; 32], ser: S) -> Result<S::Ok, S::Error> {
|
||||
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<S: Serializer>(vecs: &Vec<Vec<u8>>, ser: S) -> Result<S::Ok, S::Error> {
|
||||
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<Vec<Vec<u8>>, D::Error> {
|
||||
Ok(
|
||||
Vec::<serdect::slice::HexOrBin<false>>::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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<R: RngCore + CryptoRng>(
|
||||
|
||||
let pubnonces_by_sender: BTreeMap<Point, SigMap<PubNonce>> = 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<Point, SigningSession<PartialSignatureSharingRound>> =
|
||||
@@ -193,7 +235,13 @@ fn musig_sign_ticketed_dlc<R: RngCore + CryptoRng>(
|
||||
|
||||
let partial_sigs_by_sender: BTreeMap<Point, SigMap<PartialSignature>> = 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<R: RngCore + CryptoRng>(
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user