Skip to main content

hashiverse_lib/protocol/payload/
submit_post_v1.rs

1//! # `SubmitPostV1` — post submission envelope
2//!
3//! The wire format used to upload a new post from a client to a server responsible for
4//! the bucket it targets. [`SubmitPostV1`] pairs:
5//!
6//! - an Ed25519-signed, brotli-compressed JSON header ([`PostSigningAuthorityV1`]),
7//! - the encrypted post body.
8//!
9//! [`PostSigningAuthorityV1`] also covers ephemeral delegation: a temporary subkey with
10//! an explicit expiry and its own PoW salt may sign on the author's behalf, so sensitive
11//! identity keys don't need to be loaded into low-trust environments (browser Web
12//! Workers, headless automation) to post. The variant enum is open-ended so new
13//! signing mechanisms (PQ-only, multi-party, threshold) can be added without breaking
14//! existing clients.
15
16use bytes::Bytes;
17use crate::tools::types::{Salt, Signature, SignatureKey};
18use crate::tools::time::TimeMillis;
19use crate::tools::{compression, signing};
20use serde::{Serialize, Deserialize};
21
22#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
23pub struct PostSigningAuthorityDirectV1 {
24    pub payload_signature: Signature,
25}
26
27#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
28pub struct PostSigningAuthorityEphemeralV1 {
29    pub ephemeral_signature_pub: [u8; 32],
30    pub expires: TimeMillis,
31    pub salt: Salt,
32    pub signature: Signature, // sign(hash(ephemeral_signature_pub, expires, salt))
33}
34
35#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
36pub enum PostSigningAuthorityV1 {
37    PostSigningAuthorityDirectV1(PostSigningAuthorityDirectV1),
38    PostSigningAuthorityEphemeralV1(PostSigningAuthorityEphemeralV1),
39}
40
41#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
42pub struct SubmitPostHeaderV1 {
43    pub post_signing_authority: PostSigningAuthorityV1,
44}
45
46#[derive(Debug, PartialEq, Clone)]
47pub struct SubmitPostV1 {
48    pub submit_post_header_signature: Signature,
49    pub submit_post_header_bytes: Bytes, // SubmitPostHeaderV1
50    pub encrypted_post_signature: Signature,
51    pub encrypted_post_bytes: Bytes, // PostV1 - encrypted
52}
53
54impl SubmitPostV1 {
55    pub fn new(
56        submit_post_header: &SubmitPostHeaderV1,
57        encrypted_post_signature: Signature,
58        encrypted_post_bytes: Bytes,
59        signature_key: SignatureKey,
60    ) -> anyhow::Result<Self> {
61        let submit_post_header_json = serde_json::to_string(&submit_post_header)?;
62        let submit_post_header_bytes = compression::compress_for_speed(submit_post_header_json.as_bytes())?.to_bytes();
63        let submit_post_header_signature = signing::sign(&signature_key, &submit_post_header_bytes);
64
65        Ok(Self {
66            submit_post_header_signature,
67            submit_post_header_bytes,
68            encrypted_post_signature,
69            encrypted_post_bytes,
70        })
71    }
72
73    pub fn submit_post_header_get(&self) -> anyhow::Result<SubmitPostHeaderV1> {
74        let header_json_bytes = compression::decompress(&self.submit_post_header_bytes)?.to_bytes();
75        Ok(serde_json::from_slice(&header_json_bytes)?)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use crate::tools::keys::Keys;
82    use super::*;
83    use serde_json;
84    use crate::tools::tools;
85
86    #[tokio::test]
87    async fn test_to_from_json() -> anyhow::Result<()> {
88        let keys = Keys::from_phrase("this is a test phrase")?;
89        
90        let mut payload = [0u8; 1024];
91        tools::random_fill_bytes(&mut payload);
92        
93        let header = SubmitPostHeaderV1 {
94            post_signing_authority: PostSigningAuthorityV1::PostSigningAuthorityDirectV1(
95                PostSigningAuthorityDirectV1 {
96                    payload_signature: signing::sign(&keys.signature_key, &payload)
97                },
98            ),
99        };
100
101        let header_json = serde_json::to_string(&header)?;
102
103        log::info!("header_json={}", header_json);
104
105        let header_restored = serde_json::from_str(&header_json)?;
106        assert_eq!(header, header_restored);
107        
108        Ok(())
109    }
110}