Skip to main content

actordb_protocol/
write_concern.rs

1//! Acknowledgement thresholds do not establish a global mutation order.
2//!
3//! The receiving node freezes replica eligibility, region labels, and the
4//! effective count before apply. A processing acknowledgement means that the
5//! replica processed this mutation and retains its equal-or-newer LWW winner;
6//! it does not promise that the submitted value remains the winner. The local
7//! acknowledgement follows ordinary apply completion, which may only enqueue
8//! persistence and return before a store flush. Remote acknowledgements
9//! satisfy the stronger of the sender's configured fence and the per-write
10//! minimum. `minimum_durability` explicitly requests a store or stable fence.
11//! Evidence records the actual reached fence for each counted identity, the
12//! submitted operation/version floor and the exact frozen eligible set. Local
13//! default evidence certifies memory apply even if a background flush races it.
14
15use std::collections::HashSet;
16use std::fmt;
17use std::time::{Duration, Instant};
18
19use prost::Message;
20
21use crate::meta;
22
23pub const MAX_WRITE_CONCERN_REPLICAS: usize = 4096;
24pub const MAX_WRITE_CONCERN_POLICY_BYTES: usize = 1024;
25const MAX_RESULT_BYTES: usize = 3 * MAX_WRITE_CONCERN_REPLICAS * 280 + 8192;
26mod evidence;
27pub use evidence::{
28    WriteAcknowledgement, WriteConcernEvidence, WriteDurability, WriteOperationEvidence,
29};
30
31/// Total acknowledgements, including the receiving node.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33#[non_exhaustive]
34pub enum WriteConcern {
35    #[default]
36    Local,
37    Count(u32),
38    Majority,
39}
40
41impl WriteConcern {
42    pub fn required_acknowledgements(
43        self,
44        replica_count: usize,
45    ) -> Result<u32, WriteConcernPolicyError> {
46        let majority = replica_count / 2 + 1;
47        match self {
48            Self::Local | Self::Count(1) => Ok(1),
49            Self::Count(0) => Err(invalid("acknowledgement count must be positive")),
50            Self::Count(count) if replica_count != 0 && count as usize <= majority => Ok(count),
51            Self::Count(_) if replica_count == 0 => Err(invalid(
52                "nonlocal concern requires explicit replica placement",
53            )),
54            Self::Count(_) => Err(invalid("acknowledgement count exceeds majority")),
55            Self::Majority if replica_count == 0 => Err(invalid(
56                "majority concern requires explicit replica placement",
57            )),
58            Self::Majority => u32::try_from(majority)
59                .map_err(|_| invalid("replica count exceeds supported range")),
60        }
61    }
62}
63
64/// Per-write wait policy. Timeout starts when the receiving node applies the
65/// update. Transport and actor admission have their own deadlines.
66#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
67pub struct WriteConcernOptions {
68    #[builder(default)]
69    concern: WriteConcern,
70    #[builder(default = Duration::from_secs(5))]
71    timeout: Duration,
72    /// Additionally require a processed acknowledgement outside the receiver's
73    /// configured region, including for local and count-one policies.
74    #[builder(default)]
75    require_other_region: bool,
76    /// Reject before apply if an operational cap would reduce the requested
77    /// acknowledgement count. Defaults to allowing a disclosed reduction.
78    #[builder(default)]
79    strict_acknowledgements: bool,
80    /// Minimum actual fence required from each counted acknowledgement.
81    /// Store/stable policies invoke a local fence and require matching peer ACKs.
82    #[builder(default)]
83    minimum_durability: WriteDurability,
84    /// Require certified operation and frozen replica-set evidence from peers.
85    #[builder(default)]
86    require_evidence: bool,
87}
88
89impl Default for WriteConcernOptions {
90    fn default() -> Self {
91        Self::builder().build()
92    }
93}
94
95impl WriteConcernOptions {
96    pub fn minimum_durability(&self) -> WriteDurability {
97        self.minimum_durability
98    }
99    pub fn with_minimum_durability(mut self, durability: WriteDurability) -> Self {
100        self.minimum_durability = durability;
101        self
102    }
103    pub fn require_evidence(&self) -> bool {
104        self.require_evidence || self.minimum_durability != WriteDurability::MemoryApplied
105    }
106    pub fn with_require_evidence(mut self, required: bool) -> Self {
107        self.require_evidence = required;
108        self
109    }
110    pub fn concern(&self) -> WriteConcern {
111        self.concern
112    }
113    pub fn timeout(&self) -> Duration {
114        self.timeout
115    }
116    /// Whether success additionally requires a processed outside-region replica.
117    pub fn require_other_region(&self) -> bool {
118        self.require_other_region
119    }
120    /// Add or clear the independent region acknowledgement requirement.
121    pub fn with_require_other_region(mut self, required: bool) -> Self {
122        self.require_other_region = required;
123        self
124    }
125    /// Whether an operational cap must preserve the requested count. This does
126    /// not change the configured storage durability or LWW winner semantics.
127    pub fn strict_acknowledgements(&self) -> bool {
128        self.strict_acknowledgements
129    }
130    /// Require or relax preservation of the requested acknowledgement count.
131    /// Older peers reject an enabled constraint before applying the mutation.
132    pub fn with_strict_acknowledgements(mut self, strict: bool) -> Self {
133        self.strict_acknowledgements = strict;
134        self
135    }
136    pub fn validate(&self) -> Result<(), WriteConcernPolicyError> {
137        let ms = self.timeout.as_millis();
138        if ms == 0 || ms > u64::MAX as u128 || Instant::now().checked_add(self.timeout).is_none() {
139            return Err(invalid(
140                "write concern timeout must be representable and at least one millisecond",
141            ));
142        }
143        if self.concern == WriteConcern::Count(0) {
144            return Err(invalid("acknowledgement count must be positive"));
145        }
146        Ok(())
147    }
148
149    pub fn encode(&self) -> Result<Vec<u8>, WriteConcernPolicyError> {
150        self.validate()?;
151        let (level, acknowledgements) = match self.concern {
152            WriteConcern::Local => (meta::WriteConcernLevel::WriteConcernLocal, 0),
153            WriteConcern::Count(n) => (meta::WriteConcernLevel::WriteConcernCount, n),
154            WriteConcern::Majority => (meta::WriteConcernLevel::WriteConcernMajority, 0),
155        };
156        Ok(meta::WriteConcernPolicy {
157            level: level as i32,
158            acknowledgements,
159            timeout_ms: self.timeout.as_millis() as u64,
160            require_other_region: self.require_other_region,
161            strict_acknowledgements: self.strict_acknowledgements,
162            minimum_durability: self.minimum_durability as i32,
163            require_evidence: self.require_evidence,
164        }
165        .encode_to_vec())
166    }
167
168    /// Unknown policy fields are rejected, so an older implementation cannot
169    /// silently ignore a new required acknowledgement constraint.
170    pub fn decode(bytes: &[u8]) -> Result<Self, WriteConcernPolicyError> {
171        preflight_policy(bytes)?;
172        let policy = meta::WriteConcernPolicy::decode(bytes).map_err(|e| invalid(e.to_string()))?;
173        let concern = match meta::WriteConcernLevel::try_from(policy.level) {
174            Ok(meta::WriteConcernLevel::WriteConcernLocal) if policy.acknowledgements == 0 => {
175                WriteConcern::Local
176            }
177            Ok(meta::WriteConcernLevel::WriteConcernCount) => {
178                WriteConcern::Count(policy.acknowledgements)
179            }
180            Ok(meta::WriteConcernLevel::WriteConcernMajority) if policy.acknowledgements == 0 => {
181                WriteConcern::Majority
182            }
183            _ => return Err(invalid("invalid acknowledgement policy")),
184        };
185        let options = Self {
186            concern,
187            timeout: Duration::from_millis(policy.timeout_ms),
188            require_other_region: policy.require_other_region,
189            strict_acknowledgements: policy.strict_acknowledgements,
190            minimum_durability: WriteDurability::decode(policy.minimum_durability)?,
191            require_evidence: policy.require_evidence,
192        };
193        options.validate()?;
194        Ok(options)
195    }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199#[non_exhaustive]
200pub enum WriteConcernPolicyError {
201    Invalid(String),
202    Unsupported(String),
203}
204
205impl fmt::Display for WriteConcernPolicyError {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        match self {
208            Self::Invalid(s) | Self::Unsupported(s) => f.write_str(s),
209        }
210    }
211}
212impl std::error::Error for WriteConcernPolicyError {}
213fn invalid(s: impl Into<String>) -> WriteConcernPolicyError {
214    WriteConcernPolicyError::Invalid(s.into())
215}
216
217/// Actual processing acknowledgement progress. The receiving node always
218/// counts as one after ordinary apply completion. Certified evidence reports
219/// actual local and remote fences; a timed-out or failed-fence receipt may retain
220/// local progress below the requested durability. No failed write is rolled back.
221/// An unknown replica count is represented by `None` for local-only writes.
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct WriteConcernReceipt {
224    receiving_node: String,
225    replica_count: u32,
226    requested: u32,
227    required: u32,
228    acknowledged_nodes: Vec<String>,
229    receiving_region: Option<Box<str>>,
230    outside_region_acknowledged_nodes: Vec<String>,
231    evidence: Option<Box<WriteConcernEvidence>>,
232    requested_durability: WriteDurability,
233}
234
235impl WriteConcernReceipt {
236    pub fn requested_durability(&self) -> WriteDurability {
237        self.requested_durability
238    }
239    pub fn with_requested_durability(
240        mut self,
241        durability: WriteDurability,
242    ) -> Result<Self, WriteConcernPolicyError> {
243        self.requested_durability = durability;
244        self.validate()?;
245        Ok(self)
246    }
247    /// Certified floor and actual reached fences. Legacy responses can omit it
248    /// unless `require_evidence` or a store/stable fence was requested.
249    pub fn evidence(&self) -> Option<&WriteConcernEvidence> {
250        self.evidence.as_deref()
251    }
252    pub fn with_evidence(
253        mut self,
254        evidence: WriteConcernEvidence,
255    ) -> Result<Self, WriteConcernPolicyError> {
256        self.evidence = Some(Box::new(evidence));
257        self.validate()?;
258        Ok(self)
259    }
260    pub fn new(
261        receiving_node: String,
262        replica_count: u32,
263        requested: u32,
264        required: u32,
265        acknowledged_nodes: Vec<String>,
266    ) -> Result<Self, WriteConcernPolicyError> {
267        let receipt = Self {
268            receiving_node,
269            replica_count,
270            requested,
271            required,
272            acknowledged_nodes,
273            receiving_region: None,
274            outside_region_acknowledged_nodes: Vec::new(),
275            evidence: None,
276            requested_durability: WriteDurability::MemoryApplied,
277        };
278        receipt.validate()?;
279        Ok(receipt)
280    }
281    pub fn receiving_node(&self) -> &str {
282        &self.receiving_node
283    }
284    pub fn replica_count(&self) -> Option<u32> {
285        (self.replica_count != 0).then_some(self.replica_count)
286    }
287    pub fn requested_acknowledgements(&self) -> u32 {
288        self.requested
289    }
290    pub fn required_acknowledgements(&self) -> u32 {
291        self.required
292    }
293    pub fn acknowledged_nodes(&self) -> &[String] {
294        &self.acknowledged_nodes
295    }
296    pub fn was_overridden(&self) -> bool {
297        self.required < self.requested
298    }
299
300    /// Attach progress against the receiving node's frozen region requirement.
301    pub fn with_region_progress(
302        mut self,
303        receiving_region: String,
304        outside_region_acknowledged_nodes: Vec<String>,
305    ) -> Result<Self, WriteConcernPolicyError> {
306        self.receiving_region = Some(receiving_region.into_boxed_str());
307        self.outside_region_acknowledged_nodes = outside_region_acknowledged_nodes;
308        self.validate()?;
309        Ok(self)
310    }
311    /// Frozen receiving-region label, present when a region requirement was used.
312    pub fn receiving_region(&self) -> Option<&str> {
313        self.receiving_region.as_deref()
314    }
315    /// Eligible acknowledged identities classified outside the receiving region
316    /// by the receiving node's frozen configuration.
317    pub fn outside_region_acknowledged_nodes(&self) -> &[String] {
318        &self.outside_region_acknowledged_nodes
319    }
320    /// Whether an eligible outside-region processing acknowledgement was observed.
321    pub fn other_region_satisfied(&self) -> bool {
322        self.receiving_region.is_some() && !self.outside_region_acknowledged_nodes.is_empty()
323    }
324    /// Validate receipt correlation with the requested policy. A timed-out
325    /// receipt may lack the required progress but must preserve policy metadata.
326    pub fn validate_for(
327        &self,
328        options: &WriteConcernOptions,
329        satisfied: bool,
330    ) -> Result<(), WriteConcernPolicyError> {
331        self.validate()?;
332        let expected = options
333            .concern()
334            .required_acknowledgements(self.replica_count as usize)?;
335        if self.requested != expected {
336            return Err(invalid(
337                "write concern reply changed the requested threshold",
338            ));
339        }
340        if options.strict_acknowledgements() && self.required != self.requested {
341            return Err(invalid(
342                "write concern reply lowered a strict acknowledgement threshold",
343            ));
344        }
345        if options.require_other_region()
346            && (self.receiving_region.is_none() || satisfied && !self.other_region_satisfied())
347        {
348            return Err(invalid(
349                "write concern reply lacks required region progress",
350            ));
351        }
352        if satisfied && self.acknowledged_nodes.len() < self.required as usize {
353            return Err(invalid("receipt lacks required acknowledgements"));
354        }
355        match &self.evidence {
356            None if options.require_evidence() => {
357                return Err(invalid("write concern reply omitted required evidence"))
358            }
359            Some(evidence)
360                if satisfied
361                    && evidence
362                        .acknowledgements()
363                        .iter()
364                        .any(|ack| ack.durability() < options.minimum_durability()) =>
365            {
366                return Err(invalid("write concern reply lowered requested durability"));
367            }
368            _ => {}
369        }
370        if self.evidence.is_some() && self.requested_durability != options.minimum_durability() {
371            return Err(invalid("receipt changed requested durability"));
372        }
373        Ok(())
374    }
375
376    fn validate(&self) -> Result<(), WriteConcernPolicyError> {
377        if self.requested_durability != WriteDurability::MemoryApplied && self.evidence.is_none() {
378            return Err(invalid("durability policy lacks evidence"));
379        }
380        if let Some(evidence) = &self.evidence {
381            evidence.validate_receipt(self)?;
382        }
383        let valid_id = |id: &str| !id.is_empty() && id.len() <= 255;
384        let acknowledged: HashSet<_> = self.acknowledged_nodes.iter().collect();
385        let outside: HashSet<_> = self.outside_region_acknowledged_nodes.iter().collect();
386        if !valid_id(&self.receiving_node)
387            || self.requested == 0
388            || self.required == 0
389            || self.required > self.requested
390            || self.replica_count as usize > MAX_WRITE_CONCERN_REPLICAS
391            || (self.replica_count == 0 && self.requested != 1)
392            || (self.replica_count > 0 && self.requested > self.replica_count / 2 + 1)
393            || self.acknowledged_nodes.len() > MAX_WRITE_CONCERN_REPLICAS
394            || (self.replica_count == 0 && self.acknowledged_nodes.len() != 1)
395            || (self.replica_count > 0
396                && self.acknowledged_nodes.len() > self.replica_count as usize)
397            || self.acknowledged_nodes.iter().any(|id| !valid_id(id))
398            || acknowledged.len() != self.acknowledged_nodes.len()
399            || !self
400                .acknowledged_nodes
401                .iter()
402                .any(|id| id == &self.receiving_node)
403            || self
404                .receiving_region
405                .as_ref()
406                .is_some_and(|region| !valid_id(region))
407            || self.receiving_region.is_some() && self.replica_count == 0
408            || self.receiving_region.is_none() && !self.outside_region_acknowledged_nodes.is_empty()
409            || self.outside_region_acknowledged_nodes.len() > self.acknowledged_nodes.len()
410            || outside.len() != self.outside_region_acknowledged_nodes.len()
411            || self
412                .outside_region_acknowledged_nodes
413                .iter()
414                .any(|id| id == &self.receiving_node || !acknowledged.contains(id))
415        {
416            return Err(invalid("invalid write concern receipt"));
417        }
418        Ok(())
419    }
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
423#[non_exhaustive]
424pub enum WriteConcernOutcome {
425    Satisfied(WriteConcernReceipt),
426    TimedOut(WriteConcernReceipt),
427    DurabilityFailed {
428        receipt: WriteConcernReceipt,
429        error: String,
430    },
431    InvalidPolicy(String),
432    Unsupported(String),
433    Backpressure,
434    UnknownOutcome(String),
435}
436
437impl WriteConcernOutcome {
438    pub fn encode(&self) -> Vec<u8> {
439        let mut result = meta::WriteConcernResult::default();
440        let (status, receipt) = match self {
441            Self::Satisfied(receipt) => (
442                meta::WriteConcernStatus::WriteConcernSatisfied,
443                Some(receipt),
444            ),
445            Self::TimedOut(receipt) => (
446                meta::WriteConcernStatus::WriteConcernTimedOut,
447                Some(receipt),
448            ),
449            Self::DurabilityFailed { receipt, error } => {
450                result.error = error.clone();
451                (
452                    meta::WriteConcernStatus::WriteConcernDurabilityFailed,
453                    Some(receipt),
454                )
455            }
456            Self::InvalidPolicy(error) => {
457                result.error = error.clone();
458                (meta::WriteConcernStatus::WriteConcernInvalid, None)
459            }
460            Self::Unsupported(error) => {
461                result.error = error.clone();
462                (meta::WriteConcernStatus::WriteConcernUnsupported, None)
463            }
464            Self::Backpressure => (meta::WriteConcernStatus::WriteConcernBackpressure, None),
465            Self::UnknownOutcome(error) => {
466                result.error = error.clone();
467                (meta::WriteConcernStatus::WriteConcernUnknownOutcome, None)
468            }
469        };
470        result.status = status as i32;
471        if result.error.len() > 4096 {
472            let mut limit = 4096;
473            while !result.error.is_char_boundary(limit) {
474                limit -= 1;
475            }
476            result.error.truncate(limit);
477        }
478        if let Some(receipt) = receipt {
479            result.receiving_node = receipt.receiving_node.clone();
480            result.replica_count = receipt.replica_count;
481            result.requested_acknowledgements = receipt.requested;
482            result.required_acknowledgements = receipt.required;
483            result.acknowledged_nodes = receipt.acknowledged_nodes.clone();
484            result.receiving_region = receipt.receiving_region.as_deref().map(str::to_owned);
485            result.outside_region_acknowledged_nodes =
486                receipt.outside_region_acknowledged_nodes.clone();
487            result.evidence = receipt
488                .evidence
489                .as_deref()
490                .map(WriteConcernEvidence::encode);
491            result.requested_durability = receipt.requested_durability as i32;
492        }
493        result.encode_to_vec()
494    }
495    pub fn decode(bytes: &[u8]) -> Result<Self, WriteConcernPolicyError> {
496        preflight_result(bytes)?;
497        let result = meta::WriteConcernResult::decode(bytes).map_err(|e| invalid(e.to_string()))?;
498        match meta::WriteConcernStatus::try_from(result.status) {
499            Ok(
500                meta::WriteConcernStatus::WriteConcernSatisfied
501                | meta::WriteConcernStatus::WriteConcernTimedOut
502                | meta::WriteConcernStatus::WriteConcernDurabilityFailed,
503            ) => {
504                let mut receipt = WriteConcernReceipt::new(
505                    result.receiving_node,
506                    result.replica_count,
507                    result.requested_acknowledgements,
508                    result.required_acknowledgements,
509                    result.acknowledged_nodes,
510                )?;
511                if let Some(region) = result.receiving_region {
512                    receipt = receipt
513                        .with_region_progress(region, result.outside_region_acknowledged_nodes)?;
514                } else if !result.outside_region_acknowledged_nodes.is_empty() {
515                    return Err(invalid("region progress lacks receiving region"));
516                }
517                if let Some(evidence) = result.evidence {
518                    receipt = receipt.with_evidence(WriteConcernEvidence::decode(evidence)?)?;
519                }
520                receipt = receipt.with_requested_durability(WriteDurability::decode(
521                    result.requested_durability,
522                )?)?;
523                if result.status == meta::WriteConcernStatus::WriteConcernSatisfied as i32 {
524                    if receipt.evidence().is_some_and(|evidence| {
525                        evidence
526                            .acknowledgements()
527                            .iter()
528                            .any(|ack| ack.durability() < receipt.requested_durability())
529                    }) {
530                        return Err(invalid("success receipt lacks its advertised durability"));
531                    }
532                    if receipt.acknowledged_nodes.len() < receipt.required as usize {
533                        return Err(invalid("receipt lacks required acknowledgements"));
534                    }
535                    if receipt.receiving_region.is_some() && !receipt.other_region_satisfied() {
536                        return Err(invalid("receipt lacks required region acknowledgement"));
537                    }
538                    Ok(Self::Satisfied(receipt))
539                } else if result.status
540                    == meta::WriteConcernStatus::WriteConcernDurabilityFailed as i32
541                {
542                    Ok(Self::DurabilityFailed {
543                        receipt,
544                        error: result.error,
545                    })
546                } else {
547                    Ok(Self::TimedOut(receipt))
548                }
549            }
550            Ok(meta::WriteConcernStatus::WriteConcernInvalid) => {
551                Ok(Self::InvalidPolicy(result.error))
552            }
553            Ok(meta::WriteConcernStatus::WriteConcernUnsupported) => {
554                Ok(Self::Unsupported(result.error))
555            }
556            Ok(meta::WriteConcernStatus::WriteConcernBackpressure) => Ok(Self::Backpressure),
557            Ok(meta::WriteConcernStatus::WriteConcernUnknownOutcome) => {
558                Ok(Self::UnknownOutcome(result.error))
559            }
560            _ => Err(invalid("unknown write concern outcome")),
561        }
562    }
563}
564
565fn varint(bytes: &[u8], pos: &mut usize) -> Result<u64, WriteConcernPolicyError> {
566    let mut value = 0;
567    for shift in (0..70).step_by(7) {
568        let byte = *bytes.get(*pos).ok_or_else(|| invalid("truncated policy"))?;
569        *pos += 1;
570        if shift == 63 && byte > 1 {
571            return Err(invalid("overflowing policy varint"));
572        }
573        value |= u64::from(byte & 127) << shift;
574        if byte & 128 == 0 {
575            return Ok(value);
576        }
577    }
578    Err(invalid("overflowing policy varint"))
579}
580
581pub fn preflight_policy(bytes: &[u8]) -> Result<(), WriteConcernPolicyError> {
582    if bytes.len() > MAX_WRITE_CONCERN_POLICY_BYTES {
583        return Err(invalid("write concern policy exceeds byte limit"));
584    }
585    let mut pos = 0;
586    let mut seen = [false; 8];
587    while pos < bytes.len() {
588        let tag = varint(bytes, &mut pos)?;
589        let field = tag >> 3;
590        if !(1..=7).contains(&field) {
591            return Err(WriteConcernPolicyError::Unsupported(
592                "unsupported write concern policy field".into(),
593            ));
594        }
595        if tag & 7 != 0 || seen[field as usize] {
596            return Err(invalid("invalid or duplicate policy field"));
597        }
598        seen[field as usize] = true;
599        let value = varint(bytes, &mut pos)?;
600        if field == 4 && value > 1 {
601            return Err(invalid("invalid region requirement boolean"));
602        }
603        if field == 5 && value > 1 {
604            return Err(invalid("invalid strict acknowledgement boolean"));
605        }
606        if field == 6 && value > 2 {
607            return Err(invalid("invalid write durability"));
608        }
609        if field == 7 && value > 1 {
610            return Err(invalid("invalid evidence requirement boolean"));
611        }
612    }
613    Ok(())
614}
615
616pub fn preflight_result(bytes: &[u8]) -> Result<(), WriteConcernPolicyError> {
617    if bytes.len() > MAX_RESULT_BYTES {
618        return Err(invalid("write concern result exceeds byte limit"));
619    }
620    let mut pos = 0;
621    let mut nodes = 0usize;
622    let mut outside_nodes = 0usize;
623    let mut evidence_seen = false;
624    while pos < bytes.len() {
625        let tag = varint(bytes, &mut pos)?;
626        let field = tag >> 3;
627        if field == 0 {
628            return Err(invalid("invalid receipt field"));
629        }
630        match tag & 7 {
631            0 => {
632                varint(bytes, &mut pos)?;
633            }
634            1 => {
635                pos = pos
636                    .checked_add(8)
637                    .ok_or_else(|| invalid("receipt size overflow"))?;
638            }
639            5 => {
640                pos = pos
641                    .checked_add(4)
642                    .ok_or_else(|| invalid("receipt size overflow"))?;
643            }
644            2 => {
645                let len = usize::try_from(varint(bytes, &mut pos)?)
646                    .map_err(|_| invalid("receipt size overflow"))?;
647                if field == 6 {
648                    nodes += 1;
649                    if nodes > MAX_WRITE_CONCERN_REPLICAS || len > 255 {
650                        return Err(invalid("receipt acknowledgement limit"));
651                    }
652                }
653                if field == 9 {
654                    outside_nodes += 1;
655                    if outside_nodes > MAX_WRITE_CONCERN_REPLICAS || len > 255 {
656                        return Err(invalid("receipt region acknowledgement limit"));
657                    }
658                }
659                if (field == 2 || field == 8) && len > 255 || field == 7 && len > 4096 {
660                    return Err(invalid("receipt string limit"));
661                }
662                if field == 10 {
663                    if evidence_seen {
664                        return Err(invalid("duplicate evidence field"));
665                    }
666                    evidence_seen = true;
667                    evidence::preflight(
668                        bytes
669                            .get(pos..pos.saturating_add(len))
670                            .ok_or_else(|| invalid("truncated evidence"))?,
671                    )?;
672                }
673                pos = pos
674                    .checked_add(len)
675                    .ok_or_else(|| invalid("receipt size overflow"))?;
676            }
677            _ => return Err(invalid("invalid receipt wire type")),
678        }
679        if pos > bytes.len() {
680            return Err(invalid("truncated receipt"));
681        }
682    }
683    Ok(())
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689    fn certified_receipt(level: WriteDurability) -> WriteConcernReceipt {
690        let op = WriteOperationEvidence::new(
691            0,
692            "\u{001e}actordb.table.v1:4b56:78",
693            &[1],
694            crate::RecordVersion::new(7, 2),
695            "a".into(),
696            false,
697            None,
698        )
699        .unwrap();
700        WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "b".into()])
701            .unwrap()
702            .with_evidence(
703                WriteConcernEvidence::new(
704                    op,
705                    vec!["c".into(), "b".into(), "a".into()],
706                    vec![
707                        WriteAcknowledgement::new("a".into(), level),
708                        WriteAcknowledgement::new("b".into(), level),
709                    ],
710                )
711                .unwrap(),
712            )
713            .unwrap()
714            .with_requested_durability(WriteDurability::StableCommitted)
715            .unwrap()
716    }
717    #[test]
718    fn durability_policy_and_certified_receipts_roundtrip_without_lowering_promises() {
719        let options = WriteConcernOptions::builder()
720            .concern(WriteConcern::Majority)
721            .minimum_durability(WriteDurability::StableCommitted)
722            .build();
723        assert!(options.require_evidence());
724        assert_eq!(
725            WriteConcernOptions::decode(&options.encode().unwrap()).unwrap(),
726            options
727        );
728        let stable = certified_receipt(WriteDurability::StableCommitted);
729        assert!(stable.validate_for(&options, true).is_ok());
730        let outcome = WriteConcernOutcome::Satisfied(stable);
731        assert_eq!(
732            WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
733            outcome
734        );
735        let buffered = certified_receipt(WriteDurability::StoreCommitted);
736        assert!(buffered.validate_for(&options, true).is_err());
737        assert!(WriteConcernOutcome::decode(
738            &WriteConcernOutcome::Satisfied(buffered.clone()).encode()
739        )
740        .is_err());
741        assert!(buffered.validate_for(&options, false).is_ok());
742        let mut legacy = buffered;
743        legacy.evidence = None;
744        assert!(legacy.validate_for(&options, false).is_err());
745        assert!(WriteConcernOptions::decode(&[48, 3]).is_err());
746        assert!(WriteConcernOptions::decode(&[56, 2]).is_err());
747    }
748    #[test]
749    fn malformed_certificate_set_or_duplicate_evidence_is_rejected_before_certification() {
750        let receipt = certified_receipt(WriteDurability::StableCommitted);
751        let certificate = receipt.evidence().unwrap();
752        certificate
753            .operation()
754            .validate_application_write("KV", "x", &[1], false)
755            .unwrap();
756        assert!(certificate
757            .operation()
758            .validate_application_write("KV", "other", &[1], false)
759            .is_err());
760        for field in 0..3 {
761            let mut wire = certificate.encode();
762            match field {
763                0 => wire.operation.as_mut().unwrap().operation_digest[0] ^= 1,
764                1 => wire.replica_set_digest[0] ^= 1,
765                _ => wire.acknowledgements[0].replica_index = 100,
766            }
767            assert!(WriteConcernEvidence::decode(wire).is_err());
768        }
769        let mut duplicate = WriteConcernOutcome::Satisfied(receipt).encode();
770        duplicate.extend([82, 0]);
771        assert!(WriteConcernOutcome::decode(&duplicate).is_err());
772        assert!(evidence::preflight(&[18, 0].repeat(MAX_WRITE_CONCERN_REPLICAS + 1)).is_err());
773    }
774    #[test]
775    fn count_is_total_and_bounded_by_majority() {
776        assert_eq!(
777            WriteConcern::Count(3).required_acknowledgements(5).unwrap(),
778            3
779        );
780        assert_eq!(
781            WriteConcern::Majority.required_acknowledgements(4).unwrap(),
782            3
783        );
784        assert!(WriteConcern::Count(4).required_acknowledgements(5).is_err());
785        assert!(WriteConcern::Majority.required_acknowledgements(0).is_err());
786        assert_eq!(WriteConcern::Local.required_acknowledgements(0).unwrap(), 1);
787    }
788    #[test]
789    fn policy_roundtrip_rejects_unknown_and_duplicate_requirements() {
790        let options = WriteConcernOptions::builder()
791            .concern(WriteConcern::Count(2))
792            .build();
793        let bytes = options.encode().unwrap();
794        assert_eq!(WriteConcernOptions::decode(&bytes).unwrap(), options);
795        assert!(WriteConcernOptions::decode(&[bytes.clone(), bytes].concat()).is_err());
796        assert!(matches!(
797            WriteConcernOptions::decode(&[64, 1]),
798            Err(WriteConcernPolicyError::Unsupported(_))
799        ));
800    }
801    #[test]
802    fn strict_policy_roundtrips_and_uses_a_required_extension_field() {
803        let legacy = WriteConcernOptions::builder()
804            .concern(WriteConcern::Count(2))
805            .build();
806        assert!(!legacy.strict_acknowledgements());
807        let strict = legacy.clone().with_strict_acknowledgements(true);
808        let mut expected = legacy.encode().unwrap();
809        // Field 5 is omitted when false. Pre-extension peers reject this tag
810        // as unknown instead of silently applying a weaker request.
811        expected.extend([40, 1]);
812        assert_eq!(strict.encode().unwrap(), expected);
813        assert_eq!(WriteConcernOptions::decode(&expected).unwrap(), strict);
814        assert_eq!(strict.with_strict_acknowledgements(false), legacy);
815        assert!(WriteConcernOptions::decode(&[expected, vec![40, 1]].concat()).is_err());
816        assert!(WriteConcernOptions::decode(&[40, 2]).is_err());
817    }
818    #[test]
819    fn strict_receipts_reject_lowered_counts_even_for_timeout() {
820        let options = WriteConcernOptions::builder()
821            .concern(WriteConcern::Majority)
822            .strict_acknowledgements(true)
823            .build();
824        let lowered = WriteConcernReceipt::new("a".into(), 3, 2, 1, vec!["a".into()]).unwrap();
825        assert!(lowered.validate_for(&options, true).is_err());
826        assert!(lowered.validate_for(&options, false).is_err());
827        let preserved = WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into()]).unwrap();
828        assert!(preserved.validate_for(&options, false).is_ok());
829        assert!(preserved.validate_for(&options, true).is_err());
830    }
831    #[test]
832    fn receipts_require_distinct_acknowledgements_and_receiver() {
833        assert!(
834            WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "a".into()]).is_err()
835        );
836        let receipt =
837            WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "b".into()]).unwrap();
838        let result = WriteConcernOutcome::Satisfied(receipt);
839        assert_eq!(
840            WriteConcernOutcome::decode(&result.encode()).unwrap(),
841            result
842        );
843    }
844    #[test]
845    fn region_policy_roundtrips_and_requires_canonical_boolean() {
846        let options = WriteConcernOptions::builder()
847            .require_other_region(true)
848            .build();
849        let encoded = options.encode().unwrap();
850        assert_eq!(WriteConcernOptions::decode(&encoded).unwrap(), options);
851        assert!(WriteConcernOptions::decode(&[encoded, vec![32, 1]].concat()).is_err());
852        assert!(WriteConcernOptions::decode(&[32, 2]).is_err());
853    }
854    #[test]
855    fn region_receipts_require_acknowledged_distinct_outside_nodes() {
856        let base =
857            WriteConcernReceipt::new("a".into(), 3, 1, 1, vec!["a".into(), "b".into()]).unwrap();
858        for nodes in [
859            vec!["a".into()],
860            vec!["outside".into()],
861            vec!["b".into(), "b".into()],
862        ] {
863            assert!(base
864                .clone()
865                .with_region_progress("west".into(), nodes)
866                .is_err());
867        }
868        let receipt = base
869            .with_region_progress("west".into(), vec!["b".into()])
870            .unwrap();
871        let options = WriteConcernOptions::default().with_require_other_region(true);
872        assert!(receipt.validate_for(&options, true).is_ok());
873        let outcome = WriteConcernOutcome::Satisfied(receipt);
874        assert_eq!(
875            WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
876            outcome
877        );
878        let timed_out = WriteConcernReceipt::new("a".into(), 3, 1, 1, vec!["a".into()])
879            .unwrap()
880            .with_region_progress("west".into(), Vec::new())
881            .unwrap();
882        assert!(timed_out.validate_for(&options, false).is_ok());
883        assert!(timed_out.validate_for(&options, true).is_err());
884        assert!(WriteConcernOutcome::decode(
885            &WriteConcernOutcome::Satisfied(timed_out.clone()).encode()
886        )
887        .is_err());
888        let outcome = WriteConcernOutcome::TimedOut(timed_out);
889        assert_eq!(
890            WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
891            outcome
892        );
893    }
894    #[test]
895    fn maximum_region_receipt_fits_and_preflight_bounds_region_fields() {
896        let ids: Vec<_> = (0..MAX_WRITE_CONCERN_REPLICAS)
897            .map(|i| format!("{i:04}{}", "x".repeat(251)))
898            .collect();
899        let receipt = WriteConcernReceipt::new(
900            ids[0].clone(),
901            MAX_WRITE_CONCERN_REPLICAS as u32,
902            1,
903            1,
904            ids.clone(),
905        )
906        .unwrap()
907        .with_region_progress("west".into(), ids[1..].to_vec())
908        .unwrap();
909        let outcome = WriteConcernOutcome::Satisfied(receipt);
910        assert_eq!(
911            WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
912            outcome
913        );
914        // Field 9, 4097 one-byte strings.
915        let excessive = [74, 1, b'n'].repeat(MAX_WRITE_CONCERN_REPLICAS + 1);
916        assert!(preflight_result(&excessive).is_err());
917        let mut oversized_region = vec![66, 128, 2];
918        oversized_region.extend([b'x'; 256]);
919        assert!(preflight_result(&oversized_region).is_err());
920    }
921}