aven_core/sync/encrypted_tail/
batch.rs1use 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
77pub 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 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}