1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
//! Shared crash game math and wire state.
//!
//! The multiplier is a pure function of elapsed time, so the client can draw it
//! locally. The crash point is drawn up front and never leaves the server until
//! the round ends; the payout at cashout is always computed from the server's
//! clock, never from anything the client sends.
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::Duration;
/// `exp(GROWTH_RATE * t)` doubles the multiplier every five seconds.
pub const GROWTH_RATE: f64 = 0.138_629_436_111_989_07; // ln(2) / 5
/// House edge baked into the crash-point distribution.
pub const HOUSE_EDGE: f64 = 0.01;
/// Liability cap. At the cap `P(crash >= m)` stops following `edge / m`.
pub const MAX_MULTIPLIER: f64 = 1000.0;
/// Multiplier after `elapsed` seconds of a round.
pub fn multiplier_at(elapsed: Duration) -> f64 {
multiplier_at_secs(elapsed.as_secs_f64())
}
pub fn multiplier_at_secs(seconds: f64) -> f64 {
if !seconds.is_finite() || seconds <= 0.0 {
return 1.0;
}
(GROWTH_RATE * seconds).exp().clamp(1.0, MAX_MULTIPLIER)
}
/// How long a round runs before reaching `multiplier`.
pub fn time_for_multiplier(multiplier: f64) -> Duration {
let multiplier = multiplier.clamp(1.0, MAX_MULTIPLIER);
Duration::from_secs_f64(multiplier.ln() / GROWTH_RATE)
}
/// Crash point for a uniform draw in `[0, 1)`.
///
/// `crash = (1 - edge) / (1 - u)` gives `P(crash >= m) = edge_keep / m` for
/// every `m` in `[1, MAX_MULTIPLIER]`, so a fixed-target cashout returns
/// `(1 - edge)` of the stake on average.
pub fn crash_point_from_uniform(u: f64) -> f64 {
let u = if u.is_finite() {
u.clamp(0.0, 0.999_999_999)
} else {
0.0
};
((1.0 - HOUSE_EDGE) / (1.0 - u)).clamp(1.0, MAX_MULTIPLIER)
}
/// How many bytes a round seed contains. The first eight bytes choose the
/// crash point; the rest are revealed and hashed with it.
pub const SEED_LEN: usize = 32;
/// Hex-encoded SHA-256 of a round seed, sent before the round starts.
pub fn commit_seed(seed: &[u8]) -> String {
let digest = Sha256::digest(seed);
format!("{digest:x}")
}
/// Uniform draw encoded by a seed: the first eight bytes, big-endian.
pub fn seed_to_uniform(seed: &[u8]) -> f64 {
let mut bytes = [0u8; 8];
let take = seed.len().min(8);
bytes[..take].copy_from_slice(&seed[..take]);
u64::from_be_bytes(bytes) as f64 / 18_446_744_073_709_551_616.0
}
/// The crash point a seed commits to. A client recomputes this after the
/// reveal and checks it against the published commit.
pub fn crash_point_from_seed(seed: &[u8]) -> f64 {
crash_point_from_uniform(seed_to_uniform(seed))
}
/// Decode a lowercase or uppercase hex seed. Returns `None` for odd lengths,
/// bad digits or a seed that is not exactly [`SEED_LEN`] bytes.
pub fn seed_from_hex(raw: &str) -> Option<Vec<u8>> {
if raw.len() != SEED_LEN * 2 {
return None;
}
let mut seed = Vec::with_capacity(SEED_LEN);
for pair in raw.as_bytes().chunks_exact(2) {
let high = (pair[0] as char).to_digit(16)?;
let low = (pair[1] as char).to_digit(16)?;
seed.push((high * 16 + low) as u8);
}
Some(seed)
}
/// Check a revealed seed against the commit published before the round.
pub fn verify_seed(seed_hex: &str, commit: &str) -> bool {
let Some(seed) = seed_from_hex(seed_hex) else {
return false;
};
commit_seed(&seed).eq_ignore_ascii_case(commit)
}
/// Whole-Kr cashout value. Balances are integers, so the payout is floored.
pub fn payout(bet: u64, multiplier: f64) -> u64 {
if bet == 0 || !multiplier.is_finite() || multiplier <= 0.0 {
return 0;
}
let value = bet as f64 * multiplier.min(MAX_MULTIPLIER);
if value >= u64::MAX as f64 {
u64::MAX
} else {
value.floor() as u64
}
}
/// Round state as sent to the client.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CrashState {
pub session_id: String,
/// `running`, `cashed`, `crashed` or `forfeit`.
pub phase: String,
pub bet: u64,
pub balance: i64,
/// Live multiplier while running, otherwise the value the round ended at.
pub multiplier: f64,
/// `floor(bet * multiplier)` for the value above.
pub potential: u64,
/// Server Unix time when the round started, milliseconds.
pub started_at: u64,
/// Hidden until the round ends.
pub crash_point: Option<f64>,
pub payout: u64,
pub result: String,
/// `sha256(seed)`, present for the whole round.
pub commit: Option<String>,
/// The seed itself, present only after the round ends.
pub seed: Option<String>,
}
impl CrashState {
pub fn is_running(&self) -> bool {
self.phase == "running"
}
}
|