Skip to main content

hashiverse_lib/protocol/
peer.rs

1//! # Peer records — identity, reputation, and PoW credentials for gossip
2//!
3//! Three types make up a peer's on-the-wire reputation:
4//!
5//! - [`Peer`] — the self-signed identity record gossiped through the Kademlia DHT. Carries
6//!   the peer's Ed25519 public key, post-quantum commitment, network address, and
7//!   **three** PoW tokens (see [`PeerPow`]): the initial "birth certificate", the best
8//!   PoW observed during the current rolling day, and during the current rolling month.
9//!   Rolling PoW means a peer that has continued to do work recently is weighted higher
10//!   than one that submitted a good initial PoW and went quiet, without every peer
11//!   having to redo heavy work on every gossip cycle.
12//! - [`PeerPow`] — a PoW token bound to a specific `content_hash` and timestamp. Because
13//!   the content_hash is part of the proof, these tokens can't be replayed to sign a
14//!   *different* message.
15//! - [`ClientPow`] — bundles a peer's public key + PQ commitment with its *hardest-ever*
16//!   PoW, used to gate trust-sensitive operations (healing, feedback amplification).
17//!
18//! Every field uses compact `#[serde(rename)]` wire keys so gossiped peer blobs stay
19//! small even at DHT scale.
20
21use crate::tools::server_id::ServerId;
22use crate::tools::time::{MILLIS_IN_DAY, MILLIS_IN_MONTH, TimeMillis, TimeMillisBytes};
23use crate::tools::time_provider::time_provider::TimeProvider;
24use crate::tools::types::{Hash, Id, PQCommitmentBytes, Pow, Salt, Signature, SignatureKey, VerificationKey, VerificationKeyBytes};
25use crate::tools::{config, hashing, signing, tools};
26use crate::{anyhow_assert_eq, anyhow_assert_ge};
27use serde::{Deserialize, Serialize};
28use std::fmt;
29
30/// A proof-of-work token sponsored by a specific peer and bound to a specific request body.
31///
32/// `PeerPow` is the standard "I did work, and I did it for *this* request" credential. It
33/// fixes four things into the PoW input: the sponsor's peer [`Id`], the time the work was
34/// performed, a `content_hash` over the request body, and a [`Salt`] that the worker varied
35/// to find a solution. Pinning the content hash is what prevents an attacker from replaying
36/// a valid PoW against a different request — re-using the same `salt` against a new body
37/// would produce a different hash and fail verification.
38///
39/// `PeerPow` is worn by [`Peer`] itself (three of them: the initial identity PoW plus
40/// rolling day/month "best-effort" PoWs) and by every authenticated RPC (see
41/// [`crate::protocol::rpc`]). Because its inputs are canonical and verification is cheap,
42/// receivers can re-derive and compare the PoW without keeping state.
43#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
44pub struct PeerPow {
45    #[serde(rename = "sp")]
46    pub sponsor_id: Id,
47    #[serde(rename = "t")]
48    pub timestamp: TimeMillis,
49    #[serde(rename = "ch")]
50    pub content_hash: Hash, // This is the hash of the content of the request that the pow is protecting.  Stops replay attacks with the same pow being used for different requests.
51    #[serde(rename = "s")]
52    pub salt: Salt,
53    #[serde(rename = "z")]
54    pub pow: Pow,
55}
56
57impl PeerPow {
58    pub fn new(sponsor_id: Id, verification_key: &VerificationKeyBytes, pq_commitment_bytes: &PQCommitmentBytes, timestamp: TimeMillis, content_hash: Hash, salt: Salt) -> anyhow::Result<PeerPow> {
59        let (pow, _) = Self::pow(&sponsor_id, verification_key, pq_commitment_bytes, &timestamp.encode_be(), &content_hash, &salt)?;
60        Ok(Self {
61            sponsor_id,
62            timestamp,
63            content_hash,
64            salt,
65            pow,
66        })
67    }
68
69    pub fn zero() -> Self {
70        PeerPow {
71            sponsor_id: Id::zero(),
72            timestamp: TimeMillis::zero(),
73            content_hash: Hash::zero(),
74            salt: Salt::zero(),
75            pow: Pow(0),
76        }
77    }
78
79    pub fn random() -> Self {
80        Self {
81            sponsor_id: Id::random(),
82            timestamp: TimeMillis::random(),
83            content_hash: Hash::random(),
84            salt: Salt::random(),
85            pow: Pow(tools::random_u8()),
86        }
87    }
88
89    pub fn verify(&self, verification_key: &VerificationKeyBytes, pq_commitment_bytes: &PQCommitmentBytes) -> anyhow::Result<()> {
90        let (pow, _) = Self::pow(&self.sponsor_id, verification_key, pq_commitment_bytes, &self.timestamp.encode_be(), &self.content_hash, &self.salt)?;
91        match pow == self.pow {
92            true => Ok(()),
93            false => Err(anyhow::anyhow!("pow does not match inputs: {} != {}", self.pow, pow)),
94        }
95    }
96
97    pub fn pow_decayed_day(&self, current_time_millis: TimeMillis) -> f64 {
98        let pow_halflife_millis = MILLIS_IN_DAY;
99        let elapsed_millis = current_time_millis - self.timestamp;
100        (self.pow.0 as f64) / 2.0f64.powf(elapsed_millis.0 as f64 / pow_halflife_millis.0 as f64)
101    }
102
103    pub fn pow_decayed_month(&self, current_time_millis: TimeMillis) -> f64 {
104        let pow_halflife_millis = MILLIS_IN_MONTH;
105        let elapsed_millis = current_time_millis - self.timestamp;
106        (self.pow.0 as f64) / 2.0f64.powf(elapsed_millis.0 as f64 / pow_halflife_millis.0 as f64)
107    }
108
109    pub fn pow(sponsor_id: &Id, verification_key: &VerificationKeyBytes, pq_commitment_bytes: &PQCommitmentBytes, timestamp: &TimeMillisBytes, hash: &Hash, salt: &Salt) -> anyhow::Result<(Pow, Hash)> {
110        ServerId::pow_measure(sponsor_id, verification_key, pq_commitment_bytes, timestamp, hash, salt)
111    }
112
113    pub fn own_pow(&self, verification_key: &VerificationKeyBytes, pq_commitment_bytes: &PQCommitmentBytes) -> anyhow::Result<(Pow, Hash)> {
114        Self::pow(&self.sponsor_id, verification_key, pq_commitment_bytes, &self.timestamp.encode_be(), &self.content_hash, &self.salt)
115    }
116}
117
118/// A client-side best-ever proof-of-work token, used as a reputation credential in the
119/// trust-sensitive parts of the protocol.
120///
121/// Unlike [`PeerPow`], which is scoped to a single request, `ClientPow` is the *hardest*
122/// PoW a particular client has ever produced against a canonical sponsor target. It is
123/// carried on feedback and healing requests — the operations that let peers influence what
124/// content is propagated or suppressed — so the recipient can weight the sender's opinion
125/// by how much work they have ever invested in an identity. This makes Sybil-style attacks
126/// expensive: voting with many fresh clients means doing all that work many times.
127///
128/// The struct bundles the peer's verification key, PQ commitment, and the underlying
129/// [`PeerPow`] so a verifier has everything it needs in one place.
130#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
131pub struct ClientPow {
132    pub peer_verification_key: VerificationKeyBytes,
133    pub peer_pq_commitment_bytes: PQCommitmentBytes,
134    pub peer_pow: PeerPow,
135}
136
137impl ClientPow {
138    pub fn measure(peer_verification_key: &VerificationKeyBytes, peer_pq_commitment_bytes: &PQCommitmentBytes, sponsor_id: &Id, timestamp: TimeMillis, content_hash: &Hash, salt: &Salt) -> anyhow::Result<Self> {
139        let (pow, _) = ServerId::pow_measure(sponsor_id, peer_verification_key, peer_pq_commitment_bytes, &timestamp.encode_be(), content_hash, salt)?;
140        Ok(Self {
141            peer_verification_key: *peer_verification_key,
142            peer_pq_commitment_bytes: *peer_pq_commitment_bytes,
143            peer_pow: PeerPow {
144                sponsor_id: *sponsor_id,
145                timestamp,
146                content_hash: *content_hash,
147                salt: *salt,
148                pow,
149            },
150        })
151    }
152
153    pub fn verify(&self) -> anyhow::Result<()> {
154        self.peer_pow.verify(&self.peer_verification_key, &self.peer_pq_commitment_bytes)
155    }
156}
157
158impl fmt::Display for PeerPow {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        write!(f, "timestamp={} hash={} salt={} pow={}", self.timestamp, hex::encode(self.content_hash), hex::encode(self.salt), self.pow,)
161    }
162}
163
164/// A single node's self-signed identity record, gossiped through the Kademlia DHT.
165///
166/// `Peer` is the unit of peer discovery. Each server publishes one: its [`Id`], its Ed25519
167/// verification key, its post-quantum commitment, its current network address, its build
168/// version, and three [`PeerPow`] tokens — one fixed "initial" PoW that was produced at
169/// startup and never changes (so it acts as a long-term identity credential), plus rolling
170/// "best seen this day" and "best seen this month" PoWs that keep the record fresh and let
171/// other peers weight trust by recent work. The whole thing is covered by a `signature`
172/// so tampering is detectable.
173///
174/// Other peers store, forward, and compare `Peer` records to decide who to talk to, which
175/// is why the PoW fields have time decay built in (`pow_decayed_day`, `pow_decayed_month`)
176/// — stale records age out naturally.
177#[derive(Serialize, Deserialize, PartialEq, Clone)]
178pub struct Peer {
179    pub id: Id,
180
181    #[serde(rename = "vkb")]
182    pub verification_key_bytes: VerificationKeyBytes,
183    #[serde(rename = "pqcb")]
184    pub pq_commitment_bytes: PQCommitmentBytes,
185
186    #[serde(rename = "pi")]
187    pub pow_initial: PeerPow, // The pow generated when the server first started up.  Never changes.
188    #[serde(rename = "pcd")]
189    pub pow_current_day: PeerPow, // The best pow seen this day (approximately)
190    #[serde(rename = "pcm")]
191    pub pow_current_month: PeerPow, // The best pow seen this month (approximately)
192
193    #[serde(rename = "ver")]
194    pub version: String, // The compile version of the Peer
195    #[serde(rename = "addr")]
196    pub address: String, // The last known public address at which this server knows itself.
197
198    #[serde(rename = "ts")]
199    pub timestamp: TimeMillis, // The last time this record was updated - used for other Peers to overwrite stale records.
200
201    #[serde(rename = "sig")]
202    pub signature: Signature, // sign( hash ( id, pow_initial.pow(signature_pub), pow_current.pow(signature_pub), address, signature_timestamp ) )
203}
204
205impl fmt::Debug for Peer {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        fmt::Display::fmt(self, f)
208    }
209}
210
211impl fmt::Display for Peer {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        write!(
214            f,
215            "[ id={} version={} address={} pows=[ {} {} {} ] timestamp={} ]",
216            self.id, self.version, self.address, self.pow_initial.pow, self.pow_current_day.pow, self.pow_current_month.pow, self.timestamp
217        )
218
219        // write!(
220        //     f,
221        //     "[ id={} signature_pub={} encryption_pub={} pow_initial={} pow_current={} address={} signature_timestamp={} signature={} ]",
222        //     hex::encode(self.id),
223        //     hex::encode(self.signature_pub),
224        //     hex::encode(self.encryption_pub),
225        //     self.pow_initial,
226        //     self.pow_current,
227        //     self.address,
228        //     self.signature_timestamp,
229        //     hex::encode(self.signature),
230        // )
231    }
232}
233
234impl Peer {
235    pub fn zero() -> Peer {
236        Peer {
237            id: Id::zero(),
238            verification_key_bytes: VerificationKeyBytes::zero(),
239            pq_commitment_bytes: PQCommitmentBytes::zero(),
240
241            pow_initial: PeerPow::zero(),
242            pow_current_day: PeerPow::zero(),
243            pow_current_month: PeerPow::zero(),
244
245            version: env!("CARGO_PKG_VERSION").to_string(),
246            address: String::new(),
247
248            timestamp: TimeMillis::zero(),
249
250            signature: Signature::zero(),
251        }
252    }
253
254    pub fn signature_hash_generate(&self) -> anyhow::Result<Hash> {
255        Ok(hashing::hash_multiple(&[
256            self.id.as_ref(),
257            self.verification_key_bytes.as_ref(),
258            self.pq_commitment_bytes.as_ref(),
259            self.pow_initial.own_pow(&self.verification_key_bytes, &self.pq_commitment_bytes)?.1.as_ref(),
260            self.pow_current_day.own_pow(&self.verification_key_bytes, &self.pq_commitment_bytes)?.1.as_ref(),
261            self.pow_current_month.own_pow(&self.verification_key_bytes, &self.pq_commitment_bytes)?.1.as_ref(),
262            self.address.as_bytes(),
263            self.timestamp.encode_be().as_ref(),
264        ]))
265    }
266
267    pub fn sign(&mut self, time_provider: &dyn TimeProvider, signature_key: &SignatureKey) -> anyhow::Result<()> {
268        self.timestamp = time_provider.current_time_millis();
269
270        let signature_hash = self.signature_hash_generate()?;
271        self.signature = signing::sign(signature_key, signature_hash.as_ref());
272        Ok(())
273    }
274
275    pub fn verify(&self) -> anyhow::Result<()> {
276        self.verify_signature()?;
277        self.verify_server_id()?;
278        self.verify_pows()?;
279        Ok(())
280    }
281
282    fn verify_signature(&self) -> anyhow::Result<()> {
283        let signature_hash = self.signature_hash_generate()?;
284        signing::verify(&VerificationKey::from_bytes(&self.verification_key_bytes)?, &self.signature, signature_hash.as_ref())?;
285
286        Ok(())
287    }
288
289    fn verify_server_id(&self) -> anyhow::Result<()> {
290        let (pow, pow_hash) = ServerId::pow_measure(
291            &self.pow_initial.sponsor_id,
292            &self.verification_key_bytes,
293            &self.pq_commitment_bytes,
294            &self.pow_initial.timestamp.encode_be(),
295            &self.pow_initial.content_hash,
296            &self.pow_initial.salt,
297        )?;
298        anyhow_assert_ge!(&pow, &config::SERVER_KEY_POW_MIN, "insufficient server_id pow");
299
300        let id = ServerId::server_pow_hash_to_id(pow_hash)?;
301
302        anyhow_assert_eq!(&self.id, &id, "served_id mismatch");
303
304        Ok(())
305    }
306
307    fn verify_pows(&self) -> anyhow::Result<()> {
308        self.pow_initial.verify(&self.verification_key_bytes, &self.pq_commitment_bytes)?;
309        self.pow_current_day.verify(&self.verification_key_bytes, &self.pq_commitment_bytes)?;
310        self.pow_current_month.verify(&self.verification_key_bytes, &self.pq_commitment_bytes)?;
311        Ok(())
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use crate::protocol::peer::{Peer, PeerPow};
318    use crate::tools::server_id::ServerId;
319    use crate::tools::time::TimeMillis;
320    use crate::tools::time_provider::time_provider::RealTimeProvider;
321    use crate::tools::parallel_pow_generator::StubParallelPowGenerator;
322    use crate::tools::tools;
323    use crate::tools::types::{Pow, VerificationKeyBytes};
324
325    async fn get_random_ingredients() -> anyhow::Result<(ServerId, Peer)> {
326        let time_provider = RealTimeProvider::default();
327        let pow_generator = StubParallelPowGenerator::new();
328        let server_id = ServerId::new(&time_provider, Pow(0), true, &pow_generator).await?;
329
330        let mut peer = server_id.to_peer(&time_provider)?;
331        peer.pow_current_day = PeerPow::random();
332        peer.pow_current_month = PeerPow::random();
333        peer.address = tools::random_base64(16);
334
335        // Sign again now that we have changed some of the data
336        peer.sign(&time_provider, &server_id.keys.signature_key)?;
337
338        Ok((server_id, peer))
339    }
340
341    #[tokio::test]
342    async fn signing_test() -> anyhow::Result<()> {
343        let (_, peer) = get_random_ingredients().await?;
344
345        // Now verify the signature
346        peer.verify_signature()?;
347        Ok(())
348    }
349
350    #[tokio::test]
351    async fn signing_fail_test() -> anyhow::Result<()> {
352        {
353            let (_, mut peer) = get_random_ingredients().await?;
354            peer.verification_key_bytes = VerificationKeyBytes::zero();
355            peer.verify_signature().unwrap_err();
356        }
357
358        {
359            let (_, mut peer) = get_random_ingredients().await?;
360            peer.pow_initial = PeerPow::random();
361            peer.verify_signature().unwrap_err();
362        }
363
364        {
365            let (_, mut peer) = get_random_ingredients().await?;
366            peer.pow_current_day = PeerPow::random();
367            peer.verify_signature().unwrap_err();
368        }
369
370        {
371            let (_, mut peer) = get_random_ingredients().await?;
372            peer.pow_current_month = PeerPow::random();
373            peer.verify_signature().unwrap_err();
374        }
375
376        {
377            let (_, mut peer) = get_random_ingredients().await?;
378            peer.address = tools::random_base64(16);
379            peer.verify_signature().unwrap_err();
380        }
381
382        {
383            let (_, mut peer) = get_random_ingredients().await?;
384            peer.timestamp = TimeMillis::random();
385            peer.verify_signature().unwrap_err();
386        }
387
388        Ok(())
389    }
390}