actordb_protocol/write_concern/
evidence.rs1use super::{
3 invalid, meta, varint, WriteConcernPolicyError, WriteConcernReceipt, MAX_WRITE_CONCERN_REPLICAS,
4};
5use crate::RecordVersion;
6use prost::Message;
7use sha2::{Digest, Sha256};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
11#[non_exhaustive]
12#[repr(i32)]
13pub enum WriteDurability {
14 #[default]
15 MemoryApplied = 0,
16 StoreCommitted = 1,
17 StableCommitted = 2,
18}
19impl WriteDurability {
20 pub(super) fn decode(value: i32) -> Result<Self, WriteConcernPolicyError> {
21 match value {
22 0 => Ok(Self::MemoryApplied),
23 1 => Ok(Self::StoreCommitted),
24 2 => Ok(Self::StableCommitted),
25 _ => Err(invalid("unknown durability evidence")),
26 }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct WriteOperationEvidence {
34 wire: meta::WriteOperationEvidence,
35}
36impl WriteOperationEvidence {
37 pub fn application_write_digests(table: &str, key: &str, value: &[u8]) -> ([u8; 32], [u8; 32]) {
39 let mut digest = Sha256::new();
40 digest.update(b"\x1eactordb.table.v1:");
41 const HEX: &[u8; 16] = b"0123456789abcdef";
42 for byte in table.bytes().map(|byte| byte.to_ascii_uppercase()) {
43 digest.update([HEX[(byte >> 4) as usize], HEX[(byte & 15) as usize]]);
44 }
45 digest.update(b":");
46 for byte in key.as_bytes() {
47 digest.update([HEX[(byte >> 4) as usize], HEX[(byte & 15) as usize]]);
48 }
49 (digest.finalize().into(), Sha256::digest(value).into())
50 }
51 pub fn validate_application_digests(
52 &self,
53 key_digest: &[u8],
54 value_digest: &[u8],
55 is_tombstone: bool,
56 ) -> Result<(), WriteConcernPolicyError> {
57 if self.wire.key_digest != key_digest
58 || self.wire.value_digest != value_digest
59 || self.wire.is_tombstone != is_tombstone
60 {
61 return Err(invalid("receipt certifies a different application write"));
62 }
63 Ok(())
64 }
65 #[allow(clippy::too_many_arguments)]
66 pub fn new(
67 fragment: u32,
68 key: &str,
69 value: &[u8],
70 version: RecordVersion,
71 node_id: String,
72 is_tombstone: bool,
73 expires_at_ms: Option<u64>,
74 ) -> Result<Self, WriteConcernPolicyError> {
75 let mut wire = meta::WriteOperationEvidence {
76 fragment,
77 key_digest: Sha256::digest(key.as_bytes()).to_vec(),
78 value_digest: Sha256::digest(value).to_vec(),
79 timestamp_ms: version.timestamp_ms,
80 logical_counter: version.logical_counter,
81 node_id,
82 is_tombstone,
83 expires_at_ms,
84 operation_digest: Vec::new(),
85 };
86 wire.operation_digest = Sha256::digest(wire.encode_to_vec()).to_vec();
87 let evidence = Self { wire };
88 evidence.validate()?;
89 Ok(evidence)
90 }
91 pub fn fragment(&self) -> u32 {
92 self.wire.fragment
93 }
94 pub fn version(&self) -> RecordVersion {
95 RecordVersion::new(self.wire.timestamp_ms, self.wire.logical_counter)
96 }
97 pub fn originating_node(&self) -> &str {
98 &self.wire.node_id
99 }
100 pub fn is_tombstone(&self) -> bool {
101 self.wire.is_tombstone
102 }
103 pub fn expires_at_ms(&self) -> Option<u64> {
104 self.wire.expires_at_ms
105 }
106 pub fn key_digest(&self) -> &[u8] {
107 &self.wire.key_digest
108 }
109 pub fn value_digest(&self) -> &[u8] {
110 &self.wire.value_digest
111 }
112 pub fn operation_digest(&self) -> &[u8] {
113 &self.wire.operation_digest
114 }
115 pub fn validate_application_write(
119 &self,
120 table: &str,
121 key: &str,
122 value: &[u8],
123 is_tombstone: bool,
124 ) -> Result<(), WriteConcernPolicyError> {
125 let (key_digest, value_digest) = Self::application_write_digests(table, key, value);
126 self.validate_application_digests(&key_digest, &value_digest, is_tombstone)
127 }
128 fn validate(&self) -> Result<(), WriteConcernPolicyError> {
129 let mut unsigned = self.wire.clone();
130 unsigned.operation_digest.clear();
131 if self.wire.node_id.is_empty()
132 || self.wire.node_id.len() > 255
133 || self.wire.key_digest.len() != 32
134 || self.wire.value_digest.len() != 32
135 || self.wire.operation_digest != Sha256::digest(unsigned.encode_to_vec()).as_slice()
136 || self.wire.expires_at_ms == Some(0)
137 {
138 return Err(invalid("invalid operation certificate"));
139 }
140 Ok(())
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct WriteAcknowledgement {
146 node: String,
147 durability: WriteDurability,
148}
149impl WriteAcknowledgement {
150 pub fn new(node: String, durability: WriteDurability) -> Self {
151 Self { node, durability }
152 }
153 pub fn node(&self) -> &str {
154 &self.node
155 }
156 pub fn durability(&self) -> WriteDurability {
157 self.durability
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct WriteConcernEvidence {
165 operation: WriteOperationEvidence,
166 eligible_nodes: Vec<String>,
167 replica_set_digest: Vec<u8>,
168 acknowledgements: Vec<WriteAcknowledgement>,
169}
170fn set_digest(nodes: &[String]) -> Vec<u8> {
171 let mut hash = Sha256::new();
172 hash.update(b"actordb.write-concern.replica-set.v1");
173 hash.update((nodes.len() as u64).to_be_bytes());
174 for node in nodes {
175 hash.update((node.len() as u64).to_be_bytes());
176 hash.update(node.as_bytes());
177 }
178 hash.finalize().to_vec()
179}
180impl WriteConcernEvidence {
181 pub fn new(
182 operation: WriteOperationEvidence,
183 mut eligible_nodes: Vec<String>,
184 acknowledgements: Vec<WriteAcknowledgement>,
185 ) -> Result<Self, WriteConcernPolicyError> {
186 eligible_nodes.sort();
187 let evidence = Self {
188 operation,
189 replica_set_digest: set_digest(&eligible_nodes),
190 eligible_nodes,
191 acknowledgements,
192 };
193 evidence.validate()?;
194 Ok(evidence)
195 }
196 pub fn operation(&self) -> &WriteOperationEvidence {
197 &self.operation
198 }
199 pub fn eligible_nodes(&self) -> &[String] {
200 &self.eligible_nodes
201 }
202 pub fn replica_set_digest(&self) -> &[u8] {
203 &self.replica_set_digest
204 }
205 pub fn acknowledgements(&self) -> &[WriteAcknowledgement] {
206 &self.acknowledgements
207 }
208 pub fn durability_at(&self, node: &str) -> Option<WriteDurability> {
209 self.acknowledgements
210 .iter()
211 .find(|ack| ack.node == node)
212 .map(|ack| ack.durability)
213 }
214 fn validate(&self) -> Result<(), WriteConcernPolicyError> {
215 self.operation.validate()?;
216 if self.eligible_nodes.is_empty()
217 || self.eligible_nodes.len() > MAX_WRITE_CONCERN_REPLICAS
218 || self
219 .eligible_nodes
220 .iter()
221 .any(|node| node.is_empty() || node.len() > 255)
222 || self
223 .eligible_nodes
224 .windows(2)
225 .any(|pair| pair[0] >= pair[1])
226 || self.replica_set_digest != set_digest(&self.eligible_nodes)
227 || self.acknowledgements.len() > self.eligible_nodes.len()
228 {
229 return Err(invalid("invalid replica-set evidence"));
230 }
231 let mut seen = std::collections::HashSet::new();
232 for ack in &self.acknowledgements {
233 if self.eligible_nodes.binary_search(&ack.node).is_err() || !seen.insert(&ack.node) {
234 return Err(invalid("ineligible or duplicate durability evidence"));
235 }
236 }
237 Ok(())
238 }
239 pub(super) fn validate_receipt(
240 &self,
241 receipt: &WriteConcernReceipt,
242 ) -> Result<(), WriteConcernPolicyError> {
243 self.validate()?;
244 let count = receipt.replica_count().map_or(1, |n| n as usize);
245 if count != self.eligible_nodes.len()
246 || self.acknowledgements.len() != receipt.acknowledged_nodes().len()
247 || receipt
248 .acknowledged_nodes()
249 .iter()
250 .any(|node| self.durability_at(node).is_none())
251 {
252 return Err(invalid("evidence disagrees with acknowledgement progress"));
253 }
254 Ok(())
255 }
256 pub(super) fn encode(&self) -> meta::WriteConcernEvidence {
257 meta::WriteConcernEvidence {
258 operation: Some(self.operation.wire.clone()),
259 eligible_nodes: self.eligible_nodes.clone(),
260 replica_set_digest: self.replica_set_digest.clone(),
261 acknowledgements: self
262 .acknowledgements
263 .iter()
264 .map(|ack| meta::WriteAcknowledgement {
265 replica_index: self.eligible_nodes.binary_search(&ack.node).unwrap() as u32,
266 durability: ack.durability as i32,
267 })
268 .collect(),
269 }
270 }
271 pub(super) fn decode(
272 wire: meta::WriteConcernEvidence,
273 ) -> Result<Self, WriteConcernPolicyError> {
274 let acknowledgements = wire
275 .acknowledgements
276 .iter()
277 .map(|ack| {
278 let node = wire
279 .eligible_nodes
280 .get(ack.replica_index as usize)
281 .ok_or_else(|| invalid("invalid replica evidence index"))?
282 .clone();
283 Ok(WriteAcknowledgement::new(
284 node,
285 WriteDurability::decode(ack.durability)?,
286 ))
287 })
288 .collect::<Result<Vec<_>, WriteConcernPolicyError>>()?;
289 let evidence = Self {
290 operation: WriteOperationEvidence {
291 wire: wire
292 .operation
293 .ok_or_else(|| invalid("missing operation certificate"))?,
294 },
295 eligible_nodes: wire.eligible_nodes,
296 replica_set_digest: wire.replica_set_digest,
297 acknowledgements,
298 };
299 evidence.validate()?;
300 Ok(evidence)
301 }
302}
303
304pub(super) fn preflight(bytes: &[u8]) -> Result<(), WriteConcernPolicyError> {
306 let mut pos = 0;
307 let mut counts = [0usize; 5];
308 while pos < bytes.len() {
309 let tag = varint(bytes, &mut pos)?;
310 let field = (tag >> 3) as usize;
311 if !(1..=4).contains(&field) || tag & 7 != 2 {
312 return Err(invalid("invalid evidence field"));
313 }
314 counts[field] += 1;
315 if counts[field]
316 > if field == 2 || field == 4 {
317 MAX_WRITE_CONCERN_REPLICAS
318 } else {
319 1
320 }
321 {
322 return Err(invalid("duplicate or excessive evidence field"));
323 }
324 let len = usize::try_from(varint(bytes, &mut pos)?)
325 .map_err(|_| invalid("evidence length overflow"))?;
326 let limit = match field {
327 1 => 1024,
328 2 => 255,
329 3 => 32,
330 _ => 16,
331 };
332 if len > limit {
333 return Err(invalid("evidence field exceeds limit"));
334 }
335 pos = pos
336 .checked_add(len)
337 .filter(|pos| *pos <= bytes.len())
338 .ok_or_else(|| invalid("truncated evidence"))?;
339 }
340 Ok(())
341}