Skip to main content

hashiverse_lib/tools/
server_id.rs

1//! # Server identity with proof-of-work birth certificate
2//!
3//! [`ServerId`] is the stable identity of a single server node. Unlike [`crate::tools::client_id::ClientId`],
4//! which costs nothing to produce, a `ServerId` is expensive: the server's 32-byte `Id`
5//! is the *reversed* bytes of a PoW hash computed over the server's keys, a random
6//! sponsor id, a timestamp, and a random content hash — with a minimum leading-zero-bit
7//! count (`SERVER_KEY_POW_MIN`) configured in [`crate::tools::config`].
8//!
9//! Reversing the PoW hash means the id naturally has a large number of **trailing**
10//! zero bits. The Kademlia DHT distributes responsibility by XOR distance, and the
11//! trailing-zero structure spreads servers evenly across the keyspace while making
12//! every id independently verifiable: anyone can recompute the PoW from the fields
13//! embedded in the `ServerId` and confirm the id is real.
14//!
15//! The PoW therefore acts as a "birth certificate" that gates server identities —
16//! you can't cheaply spin up thousands of sybil servers targeting a specific keyspace
17//! region because each id costs real CPU time.
18//!
19//! Beyond identity:
20//! - [`ServerId::to_peer`] wraps the identity into a [`crate::protocol::peer::Peer`]
21//!   for gossip on the DHT, signing the result with the server's private key.
22//! - [`ServerId::encode`] / [`ServerId::decode`] provide a compact fixed-length byte
23//!   representation for on-disk persistence.
24//! - [`ServerId::verify`] re-runs the PoW check and confirms the embedded id matches.
25
26use crate::protocol::peer::{Peer, PeerPow};
27use crate::tools::keys::Keys;
28use crate::tools::pow_generator::pow_generator::PowGenerator;
29use crate::tools::time::{TimeMillis, TimeMillisBytes, TIME_MILLIS_BYTES};
30use crate::tools::time_provider::time_provider::TimeProvider;
31use crate::tools::types::{Hash, Id, PQCommitmentBytes, Pow, Salt, Signature, SignatureKey, VerificationKey, VerificationKeyBytes, HASH_BYTES, ID_BYTES, PQ_COMMITMENT_BYTES, SALT_BYTES, SIGNATURE_KEY_BYTES, VERIFICATION_KEY_BYTES};
32use crate::tools::{config, pow, tools, types};
33use std::fmt;
34
35#[derive(Clone)]
36pub struct ServerId {
37    pub keys: Keys,
38
39    // Proof of initial work
40    pub sponsor_id: Id,
41    pub timestamp: TimeMillis,
42    pub hash: Hash,
43    pub salt: Salt,
44    pub pow: Pow,
45
46    pub id: Id,
47}
48
49impl ServerId {
50    pub fn id_hex(&self) -> String {
51        hex::encode(self.id)
52    }
53
54    pub fn server_pow_hash_to_id(hash: Hash) -> anyhow::Result<Id> {
55        if hash.len() != ID_BYTES {
56            anyhow::bail!("Invalid Hash length: expected {} bytes, got {} bytes", ID_BYTES, hash.len());
57        }
58
59        let id = Id(tools::reverse_bytes(hash.as_bytes()));
60        Ok(id)
61    }
62
63    // Generates the initial pow hash for a server.
64    #[allow(clippy::too_many_arguments)] // server-identity PoW takes the full identity bundle by reference
65    pub async fn pow_generate(
66        label: &str,
67        time_provider: &dyn TimeProvider,
68        pow_min: Pow,
69        sponsor_id: &Id,
70        verification_key: &VerificationKeyBytes,
71        pq_commitment_bytes: &PQCommitmentBytes,
72        content_hash: &Hash,
73        pow_generator: &dyn PowGenerator,
74    ) -> anyhow::Result<(TimeMillis, Salt, Pow, Hash)> {
75        let timestamp = time_provider.current_time_millis();
76        let timestamp_be = timestamp.encode_be();
77        let datas = [sponsor_id.as_ref(), verification_key.as_ref(), pq_commitment_bytes.as_ref(), timestamp_be.as_ref(), content_hash.as_ref()];
78        let data_hash = pow::pow_compute_data_hash(&datas);
79        let (salt, pow, pow_hash) = pow_generator.generate(label, pow_min, data_hash).await?;
80        Ok((timestamp, salt, pow, pow_hash))
81    }
82
83    pub fn pow_measure(sponsor_id: &Id, verification_key: &VerificationKeyBytes, pqcommitment_bytes: &PQCommitmentBytes, timestamp_be: &TimeMillisBytes, content_hash: &Hash, salt: &Salt) -> anyhow::Result<(Pow, Hash)> {
84        pow::pow_measure(&[sponsor_id.as_ref(), verification_key.as_ref(), pqcommitment_bytes.as_ref(), timestamp_be.as_ref(), content_hash.as_ref()], salt)
85    }
86
87    pub async fn new(label: &str, time_provider: &dyn TimeProvider, pow_min: Pow, skip_pq_commitment_bytes: bool, pow_generator: &dyn PowGenerator) -> anyhow::Result<Self> {
88        let sponsor_id = Id::random();
89        let keys = Keys::from_rnd(skip_pq_commitment_bytes)?;
90        let hash = Hash::random();
91        let (timestamp, salt, pow, pow_hash) = ServerId::pow_generate(label, time_provider, pow_min, &sponsor_id, &keys.verification_key_bytes, &keys.pq_commitment_bytes, &hash, pow_generator).await?;
92        let id = ServerId::server_pow_hash_to_id(pow_hash)?;
93
94        Ok(ServerId {
95            keys,
96            sponsor_id,
97            timestamp,
98            hash,
99            salt,
100            pow,
101            id,
102        })
103    }
104
105    pub fn to_peer(&self, time_provider: &dyn TimeProvider) -> anyhow::Result<Peer> {
106        let mut peer = Peer {
107            id: self.id,
108            verification_key_bytes: self.keys.verification_key_bytes,
109            pq_commitment_bytes: self.keys.pq_commitment_bytes,
110
111            pow_initial: PeerPow {
112                sponsor_id: self.sponsor_id,
113                timestamp: self.timestamp,
114                content_hash: self.hash,
115                salt: self.salt,
116                pow: self.pow,
117            },
118
119            pow_current_day: PeerPow {
120                sponsor_id: self.sponsor_id,
121                timestamp: self.timestamp,
122                content_hash: self.hash,
123                salt: self.salt,
124                pow: self.pow,
125            },
126
127            pow_current_month: PeerPow {
128                sponsor_id: self.sponsor_id,
129                timestamp: self.timestamp,
130                content_hash: self.hash,
131                salt: self.salt,
132                pow: self.pow,
133            },
134
135            address: "".to_string(),
136            version: env!("CARGO_PKG_VERSION").to_string(),
137
138            timestamp: TimeMillis::zero(),
139            signature: Signature::zero(),
140        };
141
142        // Sign the peer
143        peer.sign(time_provider, &self.keys.signature_key)?;
144
145        Ok(peer)
146    }
147
148    pub fn verify(&self) -> anyhow::Result<()> {
149        let (pow, pow_hash) = ServerId::pow_measure(&self.sponsor_id, &self.keys.verification_key_bytes, &self.keys.pq_commitment_bytes, &self.timestamp.encode_be(), &self.hash, &self.salt)?;
150        if pow != self.pow {
151            anyhow::bail!("ServerID pow does not verify");
152        }
153
154        if pow < config::SERVER_KEY_POW_MIN {
155            anyhow::bail!("ServerID pow is not sufficient");
156        }
157
158        let id = ServerId::server_pow_hash_to_id(pow_hash)?;
159
160        if id != self.id {
161            anyhow::bail!("ServerID id does not verify");
162        }
163
164        Ok(())
165    }
166
167    pub fn encode(&self) -> anyhow::Result<Vec<u8>> {
168        let mut bytes = Vec::new();
169        {
170            bytes.extend_from_slice(self.keys.signature_key.as_ref());
171            bytes.extend_from_slice(self.keys.verification_key.as_ref());
172            bytes.extend_from_slice(self.keys.pq_commitment_bytes.as_ref());
173            bytes.extend_from_slice(self.sponsor_id.as_ref());
174            bytes.extend_from_slice(self.timestamp.encode_be().as_ref());
175            bytes.extend_from_slice(self.hash.as_ref());
176            bytes.extend_from_slice(self.salt.as_ref());
177            bytes.push(self.pow.0);
178            bytes.extend_from_slice(self.id.as_ref());
179        }
180
181        // Sanity check
182        let expected_len = SIGNATURE_KEY_BYTES + VERIFICATION_KEY_BYTES + PQ_COMMITMENT_BYTES + ID_BYTES + TIME_MILLIS_BYTES + HASH_BYTES + SALT_BYTES + 1 + types::ID_BYTES;
183        if bytes.len() != expected_len {
184            anyhow::bail!("incorrect byte count: expected {}, got {}", expected_len, bytes.len());
185        }
186
187        Ok(bytes)
188    }
189
190    pub fn decode(bytes: &[u8]) -> anyhow::Result<Self> {
191        // Sanity check
192        let expected_len = SIGNATURE_KEY_BYTES + VERIFICATION_KEY_BYTES + PQ_COMMITMENT_BYTES + ID_BYTES + TIME_MILLIS_BYTES + HASH_BYTES + SALT_BYTES + 1 + types::ID_BYTES;
193        if bytes.len() != expected_len {
194            anyhow::bail!("incorrect byte count: expected {}, got {}", expected_len, bytes.len());
195        }
196
197        let mut pos = 0;
198
199        let signature_key_bytes = &bytes[pos..pos + SIGNATURE_KEY_BYTES];
200        pos += SIGNATURE_KEY_BYTES;
201        let verification_key_bytes = &bytes[pos..pos + VERIFICATION_KEY_BYTES];
202        pos += VERIFICATION_KEY_BYTES;
203        let pq_commitment_bytes = &bytes[pos..pos + PQ_COMMITMENT_BYTES];
204        pos += PQ_COMMITMENT_BYTES;
205        let sponsor_id = Id::from_slice(bytes[pos..pos + ID_BYTES].try_into()?)?;
206        pos += ID_BYTES;
207        let timestamp = TimeMillis::timestamp_decode_be(&TimeMillisBytes::from_bytes(&bytes[pos..pos + 8])?);
208        pos += TIME_MILLIS_BYTES;
209        let hash = Hash::from_slice(bytes[pos..pos + HASH_BYTES].try_into()?)?;
210        pos += HASH_BYTES;
211        let salt = Salt::from_slice(bytes[pos..pos + SALT_BYTES].try_into()?)?;
212        pos += SALT_BYTES;
213        let pow = Pow(bytes[pos]);
214        pos += 1;
215        let id_bytes = &bytes[pos..pos + types::ID_BYTES];
216
217        // Recreate keys
218        let signature_key_arr: &[u8; 32] = signature_key_bytes.try_into()?;
219        let verification_key_arr: &[u8; 32] = verification_key_bytes.try_into()?;
220        let signature_key = SignatureKey::from_bytes(signature_key_arr)?;
221        let verification_key = VerificationKey::from_bytes_raw(verification_key_arr)?;
222        let verification_key_bytes = verification_key.to_verification_key_bytes();
223        let pq_commitment = PQCommitmentBytes::from_slice(pq_commitment_bytes)?;
224
225        let keys = Keys {
226            signature_key,
227            verification_key,
228            verification_key_bytes,
229            pq_commitment_bytes: pq_commitment,
230        };
231
232        let id = Id::from_slice(id_bytes)?;
233
234        Ok(ServerId {
235            keys,
236            sponsor_id,
237            timestamp,
238            hash,
239            salt,
240            pow,
241            id,
242        })
243    }
244}
245
246impl fmt::Display for ServerId {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        write!(f, "[ id={} pow={} hash={} salt={} keys={} ]", self.id,  self.pow, self.hash, self.salt, self.keys)
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
256    use crate::tools::time_provider::time_provider::RealTimeProvider;
257
258    #[tokio::test]
259    async fn pow_test() -> anyhow::Result<()> {
260        let time_provider = RealTimeProvider;
261        let pow_generator = SingleThreadedPowGenerator::new();
262        const POW_MAX: u8 = 2 * 8;
263        for pow_min in 0..POW_MAX {
264            let server_id = ServerId::new("own_pow", &time_provider, Pow(pow_min), true, &pow_generator).await?;
265            assert!(server_id.pow >= Pow(pow_min));
266        }
267
268        Ok(())
269    }
270
271    #[tokio::test]
272    async fn server_id_encode_decode_verify() -> anyhow::Result<()> {
273        let time_provider = RealTimeProvider;
274        let pow_generator = SingleThreadedPowGenerator::new();
275        let server_id = ServerId::new("own_pow", &time_provider, Pow(8), false, &pow_generator).await?;
276        let encoded = server_id.encode()?;
277        let decoded = ServerId::decode(&encoded)?;
278        decoded.verify()?;
279        Ok(())
280    }
281
282    #[tokio::test]
283    async fn server_id_encode_decode_reversibility() -> anyhow::Result<()> {
284        let time_provider = RealTimeProvider;
285        let pow_generator = SingleThreadedPowGenerator::new();
286        let server_id = ServerId::new("own_pow", &time_provider, Pow(8), false, &pow_generator).await?;
287
288        let server_id_encoded = server_id.encode()?;
289        let server_id2 = ServerId::decode(&server_id_encoded)?;
290
291        // Thoroughly compare all fields
292        // If Keys/Id derive PartialEq, this is enough:
293        assert_eq!(server_id.keys.signature_key, server_id2.keys.signature_key, "Keys do not match after decode");
294        assert_eq!(server_id.keys.verification_key, server_id2.keys.verification_key, "Keys do not match after decode");
295        assert_eq!(server_id.keys.pq_commitment_bytes, server_id2.keys.pq_commitment_bytes, "Keys do not match after decode");
296        assert_eq!(server_id.timestamp, server_id2.timestamp, "Timestamps do not match");
297        assert_eq!(server_id.salt, server_id2.salt, "Salts do not match");
298        assert_eq!(server_id.pow, server_id2.pow, "PoW bits do not match");
299        assert_eq!(server_id.id, server_id2.id, "IDs do not match");
300
301        Ok(())
302    }
303}