Skip to main content

futu_core/
server_time.rs

1//! C++ FTLogin server-time anchoring and `ProtoDelay` calibration rules.
2//!
3//! Ref:
4//! - `f3c/FTlogin/Src/ftlogin/login/logger.cpp:558-568,1623-1633`
5//! - `f3c/FTlogin/Src/ftlogin/login/logger.cpp:2708-2719`
6//! - `FutuOpenD/Src/NNProtoCenter/Other/NNBiz_SvrTime.cpp:41-78`
7
8use std::time::{Duration, Instant};
9
10use parking_lot::RwLock;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ServerTimeSampleFacts {
14    pub server_time_secs: Option<u64>,
15    pub server_time_usec: Option<u64>,
16    pub round_trip_micros: u64,
17    pub local_receive_unix_micros: Option<u64>,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ServerTimeProjection {
22    pub server_at_receive_unix_micros: i64,
23    pub offset_micros: i64,
24}
25
26/// One indivisible precise-anchor update captured at the transport boundary.
27///
28/// Keeping the projection and monotonic capture point together prevents a
29/// caller from pairing an old server sample with a later `Instant` after
30/// decryption or protobuf parsing has consumed additional time.
31#[derive(Debug, Clone, Copy)]
32pub struct ServerTimeAnchorUpdate {
33    pub projection: ServerTimeProjection,
34    pub captured_at: Instant,
35}
36
37/// Project one complete Platform login/heartbeat timestamp like C++ FTLogin.
38///
39/// C++ updates the anchor only when both server fields exist and adds half the
40/// measured request RTT to the microsecond component. Values outside Rust's
41/// signed Unix-microsecond range are rejected so callers retain the old anchor.
42#[must_use]
43pub fn project_server_time_sample_like_cpp(
44    facts: ServerTimeSampleFacts,
45) -> Option<ServerTimeProjection> {
46    let server_secs = i128::from(facts.server_time_secs?);
47    let server_usec = i128::from(facts.server_time_usec?);
48    let local_receive = i128::from(facts.local_receive_unix_micros?);
49    let half_rtt = i128::from(facts.round_trip_micros / 2);
50    let server_at_receive = server_secs
51        .checked_mul(1_000_000)?
52        .checked_add(server_usec)?
53        .checked_add(half_rtt)?;
54    let offset = server_at_receive.checked_sub(local_receive)?;
55
56    Some(ServerTimeProjection {
57        server_at_receive_unix_micros: i64::try_from(server_at_receive).ok()?,
58        offset_micros: i64::try_from(offset).ok()?,
59    })
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct DelayCalibrationFacts {
64    pub server_time_offset_micros: i64,
65    pub icmp_round_trip_micros: u64,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct DelayCalibrationProjection {
70    pub s2c_time_diff_us: i64,
71    pub net_delay_us: i64,
72}
73
74/// Build the values written by C++ `SetS2CTimeDiffAndNetDelay`.
75#[must_use]
76pub fn project_delay_calibration_like_cpp(
77    facts: DelayCalibrationFacts,
78) -> Option<DelayCalibrationProjection> {
79    Some(DelayCalibrationProjection {
80        s2c_time_diff_us: facts.server_time_offset_micros,
81        net_delay_us: i64::try_from(facts.icmp_round_trip_micros / 2).ok()?,
82    })
83}
84
85#[derive(Debug, Clone, Copy)]
86enum AnchorState {
87    OffsetOnly {
88        offset_micros: i64,
89    },
90    Precise {
91        server_at_capture_unix_micros: i64,
92        captured_at: Instant,
93    },
94}
95
96/// Shared server-time source.
97///
98/// HTTP auth supplies the initial offset-only fallback. A complete Platform
99/// TCP login or heartbeat replaces it with a precise server timestamp plus a
100/// monotonic capture point, matching C++ `server_time_ + system_uptime_`.
101#[derive(Debug)]
102pub struct ServerTimeAnchorStore {
103    state: RwLock<AnchorState>,
104}
105
106impl ServerTimeAnchorStore {
107    #[must_use]
108    pub fn new() -> Self {
109        Self {
110            state: RwLock::new(AnchorState::OffsetOnly { offset_micros: 0 }),
111        }
112    }
113
114    pub fn store_offset_micros(&self, offset_micros: i64) {
115        *self.state.write() = AnchorState::OffsetOnly { offset_micros };
116    }
117
118    pub fn store_projection(&self, projection: ServerTimeProjection, captured_at: Instant) {
119        *self.state.write() = AnchorState::Precise {
120            server_at_capture_unix_micros: projection.server_at_receive_unix_micros,
121            captured_at,
122        };
123    }
124
125    pub fn store_update(&self, update: ServerTimeAnchorUpdate) {
126        self.store_projection(update.projection, update.captured_at);
127    }
128
129    #[must_use]
130    pub fn now_unix_micros_at(&self, local_now_unix_micros: i64, now: Instant) -> i64 {
131        match *self.state.read() {
132            AnchorState::OffsetOnly { offset_micros } => {
133                local_now_unix_micros.saturating_add(offset_micros)
134            }
135            AnchorState::Precise {
136                server_at_capture_unix_micros,
137                captured_at,
138            } => server_at_capture_unix_micros.saturating_add(duration_micros_i64(
139                now.saturating_duration_since(captured_at),
140            )),
141        }
142    }
143
144    #[must_use]
145    pub fn offset_micros_at(&self, local_now_unix_micros: i64, now: Instant) -> i64 {
146        self.now_unix_micros_at(local_now_unix_micros, now)
147            .saturating_sub(local_now_unix_micros)
148    }
149}
150
151impl Default for ServerTimeAnchorStore {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157fn duration_micros_i64(duration: Duration) -> i64 {
158    duration.as_micros().min(i64::MAX as u128) as i64
159}