mirror of
https://github.com/conduition/dlctix.git
synced 2026-01-29 20:55:10 +01:00
add market maker server demo prototype (incomplete)
This commit is contained in:
25
Cargo.lock
generated
25
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[workspace]
|
||||
members = ["demo/mm_server"]
|
||||
|
||||
[package]
|
||||
name = "dlctix"
|
||||
version = "0.0.4"
|
||||
|
||||
1
demo/common/.gitignore
vendored
Normal file
1
demo/common/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
12
demo/common/Cargo.toml
Normal file
12
demo/common/Cargo.toml
Normal 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
51
demo/common/src/lib.rs
Normal 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
430
demo/mm_server/Cargo.lock
generated
Normal 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
17
demo/mm_server/Cargo.toml
Normal 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"
|
||||
29
demo/mm_server/src/errors.rs
Normal file
29
demo/mm_server/src/errors.rs
Normal 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> {}
|
||||
102
demo/mm_server/src/global_state.rs
Normal file
102
demo/mm_server/src/global_state.rs
Normal 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)| (®.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], ®.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()
|
||||
}
|
||||
}
|
||||
49
demo/mm_server/src/main.rs
Normal file
49
demo/mm_server/src/main.rs
Normal 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");
|
||||
}
|
||||
303
demo/mm_server/src/payouts.rs
Normal file
303
demo/mm_server/src/payouts.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
96
demo/mm_server/src/server/handshake.rs
Normal file
96
demo/mm_server/src/server/handshake.rs
Normal 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(®istration).unwrap()
|
||||
);
|
||||
state
|
||||
.write()
|
||||
.unwrap()
|
||||
.registrations
|
||||
.insert(player_id, registration);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
87
demo/mm_server/src/server/mod.rs
Normal file
87
demo/mm_server/src/server/mod.rs
Normal 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(())
|
||||
}
|
||||
86
demo/mm_server/src/server/offer_and_ack.rs
Normal file
86
demo/mm_server/src/server/offer_and_ack.rs
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user