zero_trust_rps/common/
message.rs

1use serde::{Deserialize, Serialize};
2pub use string::FixedStr20 as RpsData;
3use uuid::Uuid;
4
5use super::{
6    blake3::{B3Hash, B3Key, Blake3Hash},
7    hex::OwnedHexStr,
8    utils::hash_users,
9};
10
11type Version = u32;
12// INCREASE on BREAKING CHANGE
13pub const PROTOCOL_VERSION: Version = 0;
14
15pub type CommitHash = OwnedHexStr<32, 64>;
16
17pub type RoomId = u64;
18
19#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Hash {
21    B3sum(B3Hash),
22}
23
24impl Hash {
25    #[allow(unused)]
26    pub fn get_algorithm(&self) -> &'static str {
27        match self {
28            Hash::B3sum(_) => "blake3",
29        }
30    }
31}
32
33impl AsRef<str> for Hash {
34    fn as_ref(&self) -> &str {
35        match self {
36            Hash::B3sum(owned_hex_str) => owned_hex_str.as_ref(),
37        }
38    }
39}
40
41mod string {
42    #![expect(clippy::partialeq_ne_impl)]
43    use fixed_len_str::fixed_len_str;
44
45    fixed_len_str!(20);
46
47    impl FixedStr20 {
48        #[allow(unused)]
49        pub const LEN: usize = 20;
50    }
51}
52
53impl Blake3Hash for RpsData {
54    fn hash_keyed(&self, key: &B3Key, round: &Round) -> B3Hash {
55        blake3::Hasher::new_keyed(key)
56            .update(&round.round_id.into_original_bytes())
57            .update(self.trim_end_matches('\0').as_bytes())
58            .finalize()
59            .into()
60    }
61}
62
63#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
64pub enum HashWithData {
65    B3sum {
66        data: RpsData,
67        hash: B3Hash,
68        key: B3Key,
69    },
70}
71
72#[allow(dead_code)]
73impl HashWithData {
74    pub fn verify(&self, round: &Round) -> Result<(), String> {
75        match self {
76            HashWithData::B3sum { hash, key, data } => {
77                let real_hash = data.hash_keyed(key, round);
78
79                if &real_hash != hash {
80                    Err(format!("Verifying {hash} failed. Got {real_hash}"))
81                } else {
82                    Ok(())
83                }
84            }
85        }
86    }
87
88    pub fn get_data(&self) -> &str {
89        match self {
90            HashWithData::B3sum {
91                hash: _,
92                key: _,
93                data,
94            } => data.as_ref().trim_end_matches('\0'),
95        }
96    }
97
98    pub fn get_hash(&self) -> String {
99        match self {
100            HashWithData::B3sum {
101                hash,
102                key: _,
103                data: _,
104            } => format!("{hash}"),
105        }
106    }
107
108    pub fn as_hash(&self) -> Hash {
109        match self {
110            HashWithData::B3sum {
111                data: _,
112                hash,
113                key: _,
114            } => Hash::B3sum(*hash),
115        }
116    }
117}
118
119#[derive(Serialize, Deserialize, Debug)]
120pub enum ClientMessage {
121    Ping { c: u8 },
122    // Pong { s: u8 },
123    Join { room_id: RoomId, user: Uuid },
124    Play { value: Hash, round: Round }, // client can send really big messages with this
125    ConfirmPlay(HashWithData),
126    RoundFinished { round_id: RoundId },
127}
128
129#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
130pub struct UserMove {
131    pub user: Uuid,
132    pub data: HashWithData,
133}
134
135#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
136pub enum UserState {
137    InRoom,
138    Played(Hash),
139    Confirmed(HashWithData),
140}
141
142#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
143pub struct User {
144    pub id: Uuid,
145    pub state: UserState,
146}
147
148pub type RoundId = B3Hash;
149
150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
151pub struct Round {
152    pub round_id: RoundId,
153    pub users: Box<[Uuid]>,
154}
155
156impl Round {
157    pub fn validate(&self) -> Result<(), String> {
158        if !self.users.is_sorted() {
159            return Err("Invalid round: users are not sorted".into());
160        }
161        let hash = hash_users(self.users.iter().copied());
162        if hash == self.round_id {
163            Ok(())
164        } else {
165            Err(format!(
166                "Invalid round: got id {}, expected {hash}",
167                self.round_id
168            ))
169        }
170    }
171}
172
173#[derive(Serialize, Deserialize, Debug, Clone)]
174pub struct RoomState {
175    pub id: RoomId,
176    pub users: Box<[User]>,
177    pub round: Option<Round>,
178}
179
180#[derive(Serialize, Deserialize, Debug)]
181pub enum ServerMessage {
182    Hello(Version, String, Option<CommitHash>),
183    // Ping { s: u8 },
184    Pong { c: u8 },
185    RoomUpdate { new_state: RoomState },
186    Error(String),
187}