add market maker server demo prototype (incomplete)

This commit is contained in:
conduition
2024-03-18 01:37:47 +00:00
parent fc29bd27b6
commit 57dadf452b
14 changed files with 1291 additions and 0 deletions

25
Cargo.lock generated
View File

@@ -100,6 +100,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "common"
version = "0.1.0"
dependencies = [
"bitcoin",
"dlctix",
"secp",
"serde",
]
[[package]]
name = "cpufeatures"
version = "0.2.12"
@@ -238,6 +248,21 @@ version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "mm_server"
version = "0.1.0"
dependencies = [
"bitcoin",
"common",
"dlctix",
"hex",
"rand",
"serde",
"serde_cbor",
"serde_json",
"serdect",
]
[[package]]
name = "musig2"
version = "0.0.8"

View File

@@ -1,3 +1,6 @@
[workspace]
members = ["demo/mm_server"]
[package]
name = "dlctix"
version = "0.0.4"

1
demo/common/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

12
demo/common/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bitcoin = "0.31.1"
dlctix = { version = "0.0.4", path = "../.." }
secp = { version = "0.2.3", features = ["serde"] }
serde = "1.0.197"

51
demo/common/src/lib.rs Normal file
View File

@@ -0,0 +1,51 @@
use bitcoin::Amount;
use dlctix::{ContractParameters, EventAnnouncement, Outcome};
use secp::Point;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub type PlayerID = u128;
/// Each value in the map is a weighted likelihood. Higher -> more likely to occur.
pub type OutcomeOdds = BTreeMap<Outcome, u64>;
#[derive(Serialize, Deserialize)]
pub struct ClientHello {
pub player_pubkey: Point,
pub payout_hash: [u8; 32],
}
#[derive(Serialize, Deserialize)]
pub struct ServerHello {
pub player_id: PlayerID,
pub ticket_hash: [u8; 32],
pub market_maker_pubkey: Point,
pub event: EventAnnouncement,
pub odds: OutcomeOdds,
}
// TODO rename: ClientIntent
#[derive(Serialize, Deserialize)]
pub struct Intent {
pub outcome: Outcome,
pub budget: bitcoin::Amount,
}
#[derive(Serialize, Deserialize)]
pub struct ServerOffer {
pub contract_parameters: ContractParameters,
pub deposit_amount: Amount,
}
#[derive(Serialize, Deserialize)]
pub enum ClientOfferAck {
Accept,
Reject,
}
#[derive(Serialize, Deserialize)]
pub enum ServerOfferAck {
Ok,
Retry,
}

430
demo/mm_server/Cargo.lock generated Normal file
View File

@@ -0,0 +1,430 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "bech32"
version = "0.10.0-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
[[package]]
name = "bitcoin"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c"
dependencies = [
"bech32",
"bitcoin-internals",
"bitcoin_hashes",
"hex-conservative",
"hex_lit",
"secp256k1",
"serde",
]
[[package]]
name = "bitcoin-internals"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
dependencies = [
"serde",
]
[[package]]
name = "bitcoin_hashes"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
dependencies = [
"bitcoin-internals",
"hex-conservative",
"serde",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cc"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "common"
version = "0.1.0"
dependencies = [
"bitcoin",
"dlctix",
"secp",
"serde",
]
[[package]]
name = "cpufeatures"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "dlctix"
version = "0.0.4"
dependencies = [
"bitcoin",
"hex",
"musig2",
"rand",
"secp",
"secp256k1",
"serde",
"serdect",
"sha2",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-conservative"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
[[package]]
name = "hex_lit"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "mm_server"
version = "0.1.0"
dependencies = [
"bitcoin",
"common",
"dlctix",
"hex",
"rand",
"serde",
"serde_cbor",
"serde_json",
"serdect",
]
[[package]]
name = "musig2"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08219fa88b8daa343b8b9b53bb9d1a131f8035fd5080e4d7780bb71487287404"
dependencies = [
"base16ct",
"hmac",
"once_cell",
"rand",
"secp",
"secp256k1",
"serde",
"serdect",
"sha2",
"subtle",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "secp"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1507279bb0404bb566f85523e48fcf37a158daa5380577ee0d93f3ef4df39ccc"
dependencies = [
"base16ct",
"once_cell",
"rand",
"secp256k1",
"serde",
"serdect",
"subtle",
]
[[package]]
name = "secp256k1"
version = "0.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
dependencies = [
"bitcoin_hashes",
"rand",
"secp256k1-sys",
"serde",
]
[[package]]
name = "secp256k1-sys"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb"
dependencies = [
"cc",
]
[[package]]
name = "serde"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "2.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

17
demo/mm_server/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "mm_server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dlctix = { version = "0.0.4", path = "../.." }
serde_cbor = "0.11.2"
common = { path = "../common" }
bitcoin = "0.31.1"
rand = "0.8.5"
hex = "0.4.3"
serdect = "0.2.0"
serde = "1.0.197"
serde_json = "1.0.114"

View File

@@ -0,0 +1,29 @@
use crate::global_state::Stage;
use std::{
error::Error,
fmt::{self, Debug, Display, Formatter},
};
#[derive(Debug)]
pub(crate) struct WrongStageError(pub(crate) Stage);
impl Display for WrongStageError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"server is in {:?} stage, cannot accept new players",
self.0
)
}
}
impl Error for WrongStageError {}
#[derive(Debug)]
pub(crate) struct InvalidInputError<T: Display + Debug>(pub(crate) T);
impl<T: Display + Debug> Display for InvalidInputError<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "invalid input from client: {}", self.0)
}
}
impl<T: Display + Debug> Error for InvalidInputError<T> {}

View File

@@ -0,0 +1,102 @@
use bitcoin::{Amount, FeeRate};
use crate::payouts::compute_deposit_and_payout_weights;
use common::{Intent, OutcomeOdds, PlayerID, ServerOffer};
use dlctix::secp::{Point, Scalar};
use dlctix::{hashlock, ContractParameters, EventAnnouncement, MarketMaker, Player, PlayerIndex};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
net,
};
const RELATIVE_LOCKTIME_BLOCK_DELTA: u16 = 60;
/// TODO: this should be dynamic
const FEE_RATE: FeeRate = FeeRate::from_sat_per_vb_unchecked(80);
#[derive(serde::Serialize)]
pub(crate) struct PlayerRegistration {
#[serde(serialize_with = "serdect::array::serialize_hex_lower_or_bin")]
pub(crate) ticket_preimage: hashlock::Preimage,
pub(crate) player: Player,
pub(crate) intent: Intent,
#[serde(skip)]
pub(crate) connection: net::TcpStream,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Stage {
IntentRegistry,
OfferAndAck,
}
pub(crate) struct GlobalState {
pub(crate) event: EventAnnouncement,
pub(crate) odds: OutcomeOdds,
pub(crate) market_maker_seckey: Scalar,
pub(crate) market_maker_pubkey: Point,
pub(crate) registrations: HashMap<PlayerID, PlayerRegistration>,
pub(crate) stage: Stage,
}
impl GlobalState {
pub(crate) fn construct_offers(&self) -> BTreeMap<PlayerID, ServerOffer> {
// Sort players and map them to their IDs.
let player_ids: BTreeMap<&Player, PlayerID> = self
.registrations
.iter()
.map(|(&id, reg)| (&reg.player, id))
.collect();
// Map player identifiers to player indexes.
let player_indexes: HashMap<PlayerID, PlayerIndex> = player_ids
.values()
.enumerate()
.map(|(index, &id)| (id, index))
.collect();
let player_intents: BTreeMap<PlayerIndex, &Intent> = self
.registrations
.iter()
.map(|(id, reg)| (player_indexes[id], &reg.intent))
.collect();
// Compute the total amount of bitcoin which all players wish to wager.
let (deposit_amounts, outcome_payouts) = compute_deposit_and_payout_weights(
&player_intents,
&self.odds,
self.event.all_outcomes(),
);
let funding_value: Amount = deposit_amounts.values().copied().sum();
let players: BTreeSet<Player> = self
.registrations
.values()
.map(|reg| reg.player.clone())
.collect();
let contract_parameters = ContractParameters {
market_maker: MarketMaker {
pubkey: self.market_maker_pubkey,
},
players,
event: self.event.clone(),
funding_value,
outcome_payouts,
fee_rate: FEE_RATE,
relative_locktime_block_delta: RELATIVE_LOCKTIME_BLOCK_DELTA,
};
player_indexes
.into_iter()
.map(|(player_id, player_index)| {
let offer = ServerOffer {
contract_parameters: contract_parameters.clone(),
deposit_amount: deposit_amounts[&player_index],
};
(player_id, offer)
})
.collect()
}
}

View File

@@ -0,0 +1,49 @@
mod errors;
mod global_state;
mod payouts;
mod server;
use crate::global_state::{GlobalState, Stage};
use common::OutcomeOdds;
use dlctix::secp::Scalar;
use dlctix::EventAnnouncement;
use std::{
collections::HashMap,
env,
error::Error,
sync::{Arc, RwLock},
};
fn run_server() -> Result<(), Box<dyn Error>> {
let bind_addr =
env::var("MM_SERVER_BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0:1420".to_string());
let market_maker_seckey: Scalar = env::var("MM_SECRET_KEY")?.parse()?;
let market_maker_pubkey = market_maker_seckey.base_point_mul();
let event: EventAnnouncement =
serde_cbor::from_slice(&hex::decode(env::var("DLC_EVENT_ANNOUNCEMENT_CBOR")?)?)?;
let odds: OutcomeOdds =
serde_cbor::from_slice(&hex::decode(env::var("DLC_EVENT_ODDS_CBOR")?)?)?;
let global_state = Arc::new(RwLock::new(GlobalState {
event,
odds,
market_maker_seckey,
market_maker_pubkey,
registrations: HashMap::new(),
stage: Stage::IntentRegistry,
}));
server::serve(bind_addr, global_state)
}
fn main() {
if let Err(e) = run_server() {
eprintln!("fatal error: {}", e);
std::process::exit(1);
}
println!("exiting OK");
}

View File

@@ -0,0 +1,303 @@
use bitcoin::Amount;
use common::{Intent, OutcomeOdds};
use dlctix::{Outcome, PayoutWeights, PlayerIndex};
use std::collections::BTreeMap;
pub(crate) fn compute_deposit_and_payout_weights<'i>(
intents: &BTreeMap<PlayerIndex, &'i Intent>,
odds: &OutcomeOdds,
all_outcomes: impl IntoIterator<Item = Outcome>,
) -> (
BTreeMap<PlayerIndex, Amount>,
BTreeMap<Outcome, PayoutWeights>,
) {
let mut outcome_payouts = BTreeMap::<Outcome, PayoutWeights>::new();
// All players for a given outcome are paid out equally.
for (&player_index, intent) in intents {
outcome_payouts
.entry(intent.outcome)
.or_default()
.insert(player_index, 1);
}
// Count only outcomes which people have wagered on.
let total_odds_weight: u64 = odds
.iter()
.filter_map(|(outcome, &weight)| outcome_payouts.get(outcome).map(|_| weight))
.sum();
// Compute the relative weights for each player to deposit.
let deposit_weights: PayoutWeights = intents
.iter()
.map(|(&player_index, intent)| {
let n_winners = outcome_payouts[&intent.outcome].len() as u64;
let outcome_odds = odds[&intent.outcome];
let weight = 100_000_000 * outcome_odds / total_odds_weight / n_winners;
(player_index, weight)
})
.collect();
// For any outcomes which nobody wagered on, all deposits will be refunded.
for outcome in all_outcomes {
if !outcome_payouts.contains_key(&outcome) {
outcome_payouts.insert(outcome, deposit_weights.clone());
}
}
// Start with the maximum possible pot by summing all players' budgets.
let mut pot_total: Amount = intents.values().map(|intent| intent.budget).sum();
let total_deposit_weight: u64 = deposit_weights.values().sum();
for (player_index, intent) in intents {
let weight = deposit_weights[player_index];
let suggested_deposit_amount = pot_total * weight / total_deposit_weight;
// If this player's deposit amount exceeds their budget, scale the pot down until
if suggested_deposit_amount > intent.budget {
pot_total = pot_total * intent.budget.to_sat() / suggested_deposit_amount.to_sat()
}
}
let deposit_amounts = deposit_weights
.into_iter()
.map(|(player_index, weight)| {
let deposit_amount = pot_total * weight / total_deposit_weight;
(player_index, deposit_amount)
})
.collect();
(deposit_amounts, outcome_payouts)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_weight_computations_two_players() {
let intents = BTreeMap::from([
(
0,
Intent {
outcome: Outcome::Attestation(0),
budget: Amount::from_sat(100_000),
},
),
(
1,
Intent {
outcome: Outcome::Attestation(1),
budget: Amount::from_sat(300_000),
},
),
]);
let odds = OutcomeOdds::from([
(Outcome::Attestation(0), 1),
(Outcome::Attestation(1), 2),
(Outcome::Expiry, 1),
]);
let all_outcomes = [
Outcome::Attestation(0),
Outcome::Attestation(1),
Outcome::Attestation(2),
Outcome::Expiry,
];
let expected_deposit_amounts = BTreeMap::from([
(0, Amount::from_sat(100_000)),
(1, Amount::from_sat(200_000)),
]);
let expected_outcome_payouts = BTreeMap::from([
(Outcome::Attestation(0), PayoutWeights::from([(0, 1)])),
(Outcome::Attestation(1), PayoutWeights::from([(1, 1)])),
(
Outcome::Attestation(2),
PayoutWeights::from([(0, 33333333), (1, 66666666)]),
),
(
Outcome::Expiry,
PayoutWeights::from([(0, 33333333), (1, 66666666)]),
),
]);
let (deposit_amounts, outcome_payouts) =
compute_deposit_and_payout_weights(&intents, &odds, all_outcomes);
assert_eq!(deposit_amounts, expected_deposit_amounts);
assert_eq!(outcome_payouts, expected_outcome_payouts);
}
#[test]
fn test_weight_computations_three_players_with_equal_buy_in() {
let intents = BTreeMap::from([
(
0,
Intent {
outcome: Outcome::Attestation(0),
budget: Amount::from_sat(100_000),
},
),
(
1,
Intent {
outcome: Outcome::Attestation(1),
budget: Amount::from_sat(300_000),
},
),
(
2,
Intent {
outcome: Outcome::Attestation(1),
budget: Amount::from_sat(150_000),
},
),
]);
let odds = OutcomeOdds::from([
(Outcome::Attestation(0), 1),
(Outcome::Attestation(1), 2),
(Outcome::Expiry, 1),
]);
let all_outcomes = [
Outcome::Attestation(0),
Outcome::Attestation(1),
Outcome::Attestation(2),
Outcome::Expiry,
];
let expected_deposit_amounts = BTreeMap::from([
(0, Amount::from_sat(100_000)),
(1, Amount::from_sat(100_000)),
(2, Amount::from_sat(100_000)),
]);
let expected_outcome_payouts = BTreeMap::from([
// Player 0 wins alone
(Outcome::Attestation(0), PayoutWeights::from([(0, 1)])),
// Player 1 and 2 win together, splitting the prize
(
Outcome::Attestation(1),
PayoutWeights::from([(1, 1), (2, 1)]),
),
// Unexpected outcome; all players refunded
(
Outcome::Attestation(2),
PayoutWeights::from([(0, 33333333), (1, 33333333), (2, 33333333)]),
),
(
Outcome::Expiry,
PayoutWeights::from([(0, 33333333), (1, 33333333), (2, 33333333)]),
),
]);
let (deposit_amounts, outcome_payouts) =
compute_deposit_and_payout_weights(&intents, &odds, all_outcomes);
assert_eq!(deposit_amounts, expected_deposit_amounts);
assert_eq!(outcome_payouts, expected_outcome_payouts);
}
#[test]
fn test_weight_computations_five_players() {
let intents = BTreeMap::from([
(
0,
Intent {
outcome: Outcome::Attestation(0),
budget: Amount::from_sat(100_000),
},
),
(
1,
Intent {
outcome: Outcome::Attestation(0),
budget: Amount::from_sat(200_000),
},
),
(
2,
Intent {
outcome: Outcome::Attestation(1),
budget: Amount::from_sat(150_000),
},
),
(
3,
Intent {
outcome: Outcome::Attestation(1),
budget: Amount::from_sat(300_000),
},
),
(
4,
Intent {
outcome: Outcome::Attestation(2),
budget: Amount::from_sat(500_000),
},
),
]);
// Odds weight sum: 8
let odds = OutcomeOdds::from([
(Outcome::Attestation(0), 1),
(Outcome::Attestation(1), 2),
(Outcome::Attestation(2), 5),
]);
let all_outcomes = [
Outcome::Attestation(0),
Outcome::Attestation(1),
Outcome::Attestation(2),
Outcome::Expiry,
];
// Pot total: 800k
let expected_deposit_amounts = BTreeMap::from([
// Players 0 and 1 deposit 50k, receiving 400k each on victory (1:8 odds)
(0, Amount::from_sat(50_000)),
(1, Amount::from_sat(50_000)),
// Players 2 and 3 deposit 100k, receiving 400k each on victory (1:4 odds)
(2, Amount::from_sat(100_000)),
(3, Amount::from_sat(100_000)),
// Player 4 deposits 500k, receiving 800k on victory (8:5 odds)
(4, Amount::from_sat(500_000)),
]);
let expected_outcome_payouts = BTreeMap::from([
// Player 0 and 1 and win together, splitting the prize
(
Outcome::Attestation(0),
PayoutWeights::from([(0, 1), (1, 1)]),
),
// Player 2 and 3 win together, splitting the prize
(
Outcome::Attestation(1),
PayoutWeights::from([(2, 1), (3, 1)]),
),
// Player 4 wins alone
(Outcome::Attestation(2), PayoutWeights::from([(4, 1)])),
// Unexpected outcome; all players refunded
(
Outcome::Expiry,
PayoutWeights::from([
(0, 6_250_000),
(1, 6_250_000),
(2, 12_500_000),
(3, 12_500_000),
(4, 62_500_000),
]),
),
]);
let (deposit_amounts, outcome_payouts) =
compute_deposit_and_payout_weights(&intents, &odds, all_outcomes);
assert_eq!(deposit_amounts, expected_deposit_amounts);
assert_eq!(outcome_payouts, expected_outcome_payouts);
}
}

View File

@@ -0,0 +1,96 @@
use bitcoin::Amount;
use super::SOCKET_DEFAULT_READ_TIMEOUT;
use crate::errors::InvalidInputError;
use crate::global_state::{GlobalState, PlayerRegistration};
use common::{ClientHello, Intent, ServerHello};
use dlctix::{hashlock, Player};
use std::{error::Error, net, sync::RwLock, time::Duration};
const MIN_BUDGET: Amount = Amount::from_sat(70_000);
pub(crate) fn handshake_player_register(
state: &RwLock<GlobalState>,
conn: net::TcpStream,
) -> Result<(), Box<dyn Error>> {
let client_hello: ClientHello = serde_cbor::from_reader(&conn)?;
// Sample a random ticket preimage for this player.
let mut rng = rand::thread_rng();
let ticket_preimage = hashlock::preimage_random(&mut rng);
let ticket_hash = hashlock::sha256(&ticket_preimage);
// The player ID is simply the first 16 bytes of the ticket hash.
let player_id = u128::from_be_bytes({
let mut buf = [0u8; 16];
buf.copy_from_slice(&ticket_hash[..16]);
buf
});
let player = Player {
pubkey: client_hello.player_pubkey,
ticket_hash,
payout_hash: client_hello.payout_hash,
};
let server_hello = {
let state_rlock = state.read().unwrap();
ServerHello {
player_id,
ticket_hash,
market_maker_pubkey: state_rlock.market_maker_pubkey,
event: state_rlock.event.clone(),
odds: state_rlock.odds.clone(),
}
};
serde_cbor::to_writer(&conn, &server_hello)?;
// Give the client 2 minutes to compose an Intent.
conn.set_read_timeout(Some(Duration::from_secs(120)))?;
let intent: Intent = serde_cbor::from_reader(&conn)?;
conn.set_read_timeout(Some(SOCKET_DEFAULT_READ_TIMEOUT))?;
{
// validate intent outcome is acceptable
let state_rlock = state.read().unwrap();
if !state_rlock.event.is_valid_outcome(&intent.outcome) {
return Err(InvalidInputError(
"player's intended outcome is not valid for this event",
))?;
}
if !state_rlock.odds.contains_key(&intent.outcome) {
return Err(InvalidInputError(format!(
"we have no odds for the player's requested outcome '{}'",
intent.outcome
)))?;
}
if intent.budget < MIN_BUDGET {
return Err(InvalidInputError(format!(
"budget must be at least {}",
MIN_BUDGET
)))?;
}
}
let registration = PlayerRegistration {
ticket_preimage,
player,
intent,
connection: conn,
};
println!(
"registering new player: {}",
serde_json::to_string_pretty(&registration).unwrap()
);
state
.write()
.unwrap()
.registrations
.insert(player_id, registration);
Ok(())
}

View File

@@ -0,0 +1,87 @@
mod handshake;
mod offer_and_ack;
use crate::errors::WrongStageError;
use crate::global_state::{GlobalState, Stage};
use std::{
error::Error,
net,
sync::{Arc, RwLock},
thread,
time::Duration,
};
pub(crate) const SOCKET_DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const SOCKET_WRITE_TIMEOUT: Duration = Duration::from_secs(8);
pub(crate) const MIN_PLAYERS: usize = 5;
pub(crate) const PLAYERS_THRESHOLD: usize = 10;
fn handle_tcp_conn(
state: Arc<RwLock<GlobalState>>,
conn: net::TcpStream,
) -> Result<(), Box<dyn Error>> {
conn.set_read_timeout(Some(SOCKET_DEFAULT_READ_TIMEOUT))?;
conn.set_write_timeout(Some(SOCKET_WRITE_TIMEOUT))?;
{
let state_rlock = state.read().unwrap();
if state_rlock.stage != Stage::IntentRegistry {
return Err(WrongStageError(state_rlock.stage))?;
}
}
handshake::handshake_player_register(&state, conn)?;
// Once we hit the minimum threshold, dispatch offers and then prompt for signatures.
if state.read().unwrap().registrations.len() >= PLAYERS_THRESHOLD {
{
let mut state_wlock = state.write().unwrap();
state_wlock.stage = Stage::OfferAndAck;
}
if let Some(accepted_players) = offer_and_ack::offer_and_ack_cycle(&state)? {
// TODO prompt all players for signatures
}
}
Ok(())
}
pub(crate) fn serve(
bind_addr: String,
global_state: Arc<RwLock<GlobalState>>,
) -> Result<(), Box<dyn Error>> {
println!("starting listener on {}", bind_addr);
let listener = net::TcpListener::bind(bind_addr)?;
// TODO use thread pool
println!("awaiting connections...");
for stream in listener.incoming() {
let conn = match stream {
Ok(conn) => conn,
Err(e) => {
eprintln!("error accepting TCP connection: {}", e);
continue;
}
};
match conn.peer_addr() {
Ok(peer_addr) => {
println!("received new TCP connection from {}", peer_addr);
}
Err(e) => {
eprintln!("new TCP connection; unable to get peer IP address: {}", e);
}
}
let state = Arc::clone(&global_state);
thread::spawn(move || {
if let Err(e) = handle_tcp_conn(state, conn) {
eprintln!("TCP connection handling failure: {}", e);
}
});
}
Ok(())
}

View File

@@ -0,0 +1,86 @@
use super::{MIN_PLAYERS, SOCKET_DEFAULT_READ_TIMEOUT};
use crate::global_state::GlobalState;
use common::{ClientOfferAck, PlayerID, ServerOffer};
use std::{
collections::{BTreeMap, BTreeSet},
error::Error,
net,
sync::{mpsc, Arc, RwLock},
thread,
time::Duration,
};
fn send_offer_and_receive_ack(conn: &net::TcpStream, offer: &ServerOffer) -> ClientOfferAck {
let client_offer_ack: ClientOfferAck = serde_cbor::to_writer(conn, offer)
.and_then(|_| {
// Give the user 2 minutes to ACK the offer. Fall back to rejection.
conn.set_read_timeout(Some(Duration::from_secs(120)))
.unwrap();
serde_cbor::from_reader(conn)
})
.unwrap_or(ClientOfferAck::Reject);
// Reset the read timeout.
conn.set_read_timeout(Some(SOCKET_DEFAULT_READ_TIMEOUT))
.unwrap();
client_offer_ack
}
pub(crate) fn offer_and_ack_cycle(
state: &Arc<RwLock<GlobalState>>,
) -> Result<Option<BTreeMap<PlayerID, ServerOffer>>, Box<dyn Error>> {
let offers = state.read().unwrap().construct_offers();
let (ack_sender, ack_receiver) = mpsc::channel();
for (player_id, offer) in offers {
let state = Arc::clone(state);
let ack_sender = ack_sender.clone();
thread::spawn(move || {
let state_rlock = state.read().unwrap();
let conn = &state_rlock.registrations[&player_id].connection;
let client_offer_ack = send_offer_and_receive_ack(conn, &offer);
ack_sender
.send((player_id, offer, client_offer_ack))
.unwrap();
});
}
drop(ack_sender); // Otherwise the ack_receiver channel will stay open.
let mut accepted_players = BTreeMap::new();
let mut rejected_players = BTreeSet::new();
while let Ok((player_id, offer, client_offer_ack)) = ack_receiver.recv() {
match client_offer_ack {
ClientOfferAck::Accept => {
accepted_players.insert(player_id, offer);
}
ClientOfferAck::Reject => {
rejected_players.insert(player_id);
}
};
}
if rejected_players.len() > 0 {
// Disconnect rejecting players.
{
let mut state_wlock = state.write().unwrap();
for player_id in rejected_players {
state_wlock.registrations.remove(&player_id);
}
// We don't have enough accepting players to try again.
if state_wlock.registrations.len() < MIN_PLAYERS {
return Ok(None);
}
}
// Retry with accepting players.
return offer_and_ack_cycle(state);
}
Ok(Some(accepted_players))
}