Skip to main content

aven_core/sync/encrypted_tail/
batch.rs

1//! Compact framing belongs only to the batch transport; record bytes are unchanged.
2use super::*;
3
4pub mod fixed {
5    use serde::{Deserializer, Serializer, de::Error};
6    pub fn serialize<S: Serializer>(
7        value: &[u8; 32],
8        serializer: S,
9    ) -> std::result::Result<S::Ok, S::Error> {
10        crate::sync::base64_bytes::serialize(value, serializer)
11    }
12    pub fn deserialize<'de, D: Deserializer<'de>>(
13        deserializer: D,
14    ) -> std::result::Result<[u8; 32], D::Error> {
15        crate::sync::base64_bytes::bounded::<D, 32>(deserializer)?
16            .try_into()
17            .map_err(|_| D::Error::custom("expected 32 bytes"))
18    }
19}
20
21#[derive(Serialize, Deserialize)]
22#[serde(remote = "Context", deny_unknown_fields)]
23struct CompactContext {
24    #[serde(with = "fixed")]
25    vault: [u8; 32],
26    #[serde(with = "fixed")]
27    genesis: [u8; 32],
28    #[serde(with = "fixed")]
29    device: [u8; 32],
30    #[serde(with = "fixed")]
31    head: [u8; 32],
32    #[serde(with = "fixed")]
33    stream: [u8; 32],
34    #[serde(with = "fixed")]
35    descriptor: [u8; 32],
36}
37
38#[derive(Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct Envelope<T> {
41    #[serde(with = "CompactContext")]
42    pub context: Context,
43    #[serde(with = "fixed")]
44    pub correlation: [u8; 32],
45    pub operation: T,
46}
47
48#[derive(Clone, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct CompactMapping {
51    pub operation_id: String,
52    pub sequence: i64,
53    #[serde(with = "fixed")]
54    pub commitment: [u8; 32],
55}
56impl From<Mapping> for CompactMapping {
57    fn from(value: Mapping) -> Self {
58        Self {
59            operation_id: value.operation_id,
60            sequence: value.sequence,
61            commitment: value.commitment,
62        }
63    }
64}
65impl From<CompactMapping> for Mapping {
66    fn from(value: CompactMapping) -> Self {
67        Self {
68            operation_id: value.operation_id,
69            sequence: value.sequence,
70            commitment: value.commitment,
71        }
72    }
73}
74
75const _: () = assert!(BATCH_APPEND_LIMIT <= attachments::HTTP_LIMIT);
76
77/// Bounds collection allocation even when an input contains many tiny elements.
78pub fn bounded_items<'de, D, T>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
79where
80    D: serde::Deserializer<'de>,
81    T: serde::Deserialize<'de>,
82{
83    struct Items<T>(std::marker::PhantomData<T>);
84    impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for Items<T> {
85        type Value = Vec<T>;
86        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
87            f.write_str("at most 128 batch items")
88        }
89        fn visit_seq<A: serde::de::SeqAccess<'de>>(
90            self,
91            mut seq: A,
92        ) -> std::result::Result<Self::Value, A::Error> {
93            use serde::de::Error;
94            let mut items = Vec::new();
95            while let Some(item) = seq.next_element()? {
96                if items.len() == BATCH_COUNT {
97                    return Err(A::Error::custom("batch count limit"));
98                }
99                items.push(item);
100            }
101            Ok(items)
102        }
103    }
104    deserializer.deserialize_seq(Items(std::marker::PhantomData))
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use sqlx::{Connection, Executor};
111
112    #[tokio::test]
113    async fn migration_preserves_frozen_observed_and_blocked_ownership() {
114        let mut conn = sqlx::SqliteConnection::connect("sqlite::memory:")
115            .await
116            .unwrap();
117        conn.execute("PRAGMA foreign_keys=ON").await.unwrap();
118        conn.execute("CREATE TABLE changes(change_id TEXT PRIMARY KEY,client_id TEXT,local_seq INTEGER,entity_type TEXT,entity_id TEXT,field TEXT,op_type TEXT,payload TEXT,base_version TEXT,created_at TEXT)").await.unwrap();
119        conn.execute("INSERT INTO changes(change_id) VALUES ('frozen'),('accepted')")
120            .await
121            .unwrap();
122        let schema = include_str!("../../../migrations/20260924091148_encrypted_sync.sql");
123        let start = schema.find("CREATE TABLE local_e2ee_outbox (").unwrap();
124        let end = schema.find("-- Local association state").unwrap();
125        // Both bounds select literal schema source, never user input.
126        sqlx::raw_sql(sqlx::AssertSqlSafe(&schema[start..end]))
127            .execute(&mut conn)
128            .await
129            .unwrap();
130        sqlx::query("INSERT INTO local_e2ee_outbox(singleton,operation_id,association,sync_generation,record,observed_sequence,observed_commitment,blocked) VALUES(1,'frozen','association',7,?,8,?,1)")
131            .bind(vec![1u8; 100]).bind(vec![2u8; 32]).execute(&mut conn).await.unwrap();
132        sqlx::query("INSERT INTO local_e2ee_accepted(operation_id,sequence,commitment,record) VALUES('accepted',9,?,?)")
133            .bind(vec![3u8;32]).bind(vec![4u8;100]).execute(&mut conn).await.unwrap();
134        let mut tx = conn.begin().await.unwrap();
135        sqlx::raw_sql(include_str!(
136            "../../../migrations/20260928163800_ordered_e2ee_outbox.sql"
137        ))
138        .execute(&mut *tx)
139        .await
140        .unwrap();
141        tx.commit().await.unwrap();
142        let row: (i64, String, i64, Vec<u8>, i64, Vec<u8>, bool) = sqlx::query_as("SELECT position,association,sync_generation,record,observed_sequence,observed_commitment,blocked FROM local_e2ee_outbox")
143            .fetch_one(&mut conn).await.unwrap();
144        assert_eq!(
145            row,
146            (
147                1,
148                "association".into(),
149                7,
150                vec![1; 100],
151                8,
152                vec![2; 32],
153                true
154            )
155        );
156        assert!(
157            conn.execute("DELETE FROM changes WHERE change_id='frozen'")
158                .await
159                .is_err()
160        );
161        assert!(
162            conn.execute("UPDATE changes SET payload='changed' WHERE change_id='accepted'")
163                .await
164                .is_err()
165        );
166        sqlx::query("UPDATE local_e2ee_outbox SET record=?")
167            .bind(vec![1u8; RECORD_LIMIT])
168            .execute(&mut conn)
169            .await
170            .unwrap();
171        sqlx::query("UPDATE local_e2ee_accepted SET record=?")
172            .bind(vec![1u8; RECORD_LIMIT])
173            .execute(&mut conn)
174            .await
175            .unwrap();
176        assert!(
177            sqlx::query("UPDATE local_e2ee_outbox SET record=?")
178                .bind(vec![1u8; RECORD_LIMIT + 1])
179                .execute(&mut conn)
180                .await
181                .is_err()
182        );
183    }
184}