Skip to main content

lodestone_types/
state_outcomes.rs

1//! Transport-neutral state commands and exact write-outcome identities.
2//!
3//! RESP is deliberately absent from this module. Public native requests and
4//! the RESP leaf both translate into these semantics, while only native
5//! requests carry caller-known recovery identities.
6
7use std::collections::BTreeSet;
8use std::error::Error;
9use std::fmt::{self, Display, Formatter};
10
11use serde::{Deserialize, Deserializer, Serialize, de};
12
13use crate::{
14    CollectionIndexRef, DurabilityBoundary, DurabilityClass, DurabilityLoss, FenceToken, OperationFingerprint,
15    RoutingRevision, SeqNo, ShardId, StateIncarnation,
16};
17
18pub const STATE_COMMAND_FORMAT_VERSION: u16 = 1;
19pub const STATE_WRITE_OUTCOME_FORMAT_VERSION: u16 = 1;
20pub const REGISTERED_STATE_RECOVERY_LOCATOR_FORMAT_VERSION: u16 = 1;
21pub const EPHEMERAL_STATE_ACTION_REFERENCE_FORMAT_VERSION: u16 = 1;
22pub const MAX_STATE_COMMAND_BYTES: usize = 2 * 1024 * 1024;
23pub const MAX_STATE_COMMAND_KEYS: usize = 128;
24pub const MAX_STATE_COMMAND_KEY_BYTES: usize = 254;
25pub const MAX_STATE_COMMAND_VALUE_BYTES: usize = 1024 * 1024;
26pub const MAX_STATE_COMMAND_HASH_FIELDS: usize = 1024;
27// JSON byte vectors use at most four bytes per source byte (three decimal
28// digits plus a comma). The remaining envelope, scalar fields, and at most
29// 1,024 hash fields or 128 key/entry containers fit under this deliberately
30// generous structural allowance. Near the limit validation measures the
31// actual encoding instead of relying on this upper bound.
32const STATE_COMMAND_JSON_STRUCTURAL_ALLOWANCE: usize =
33    256 + 128 * (MAX_STATE_COMMAND_HASH_FIELDS + MAX_STATE_COMMAND_KEYS);
34pub const MAX_REGISTERED_STATE_OUTCOME_ENTRIES: usize = 4_096;
35pub const REGISTERED_STATE_OUTCOME_SEQUENCE_WINDOW: u64 = 65_536;
36pub const MAX_EPHEMERAL_STATE_OUTCOME_ENTRIES: usize = 1_024;
37pub const EPHEMERAL_STATE_OUTCOME_SEQUENCE_WINDOW: u64 = 8_192;
38
39#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
40#[serde(transparent)]
41pub struct StateActionId([u8; 32]);
42
43impl StateActionId {
44    pub fn new(bytes: [u8; 32]) -> Result<Self, StateContractError> {
45        if bytes == [0; 32] {
46            return Err(StateContractError::ZeroActionId);
47        }
48        Ok(Self(bytes))
49    }
50
51    #[must_use]
52    pub const fn get(self) -> [u8; 32] {
53        self.0
54    }
55
56    #[must_use]
57    pub const fn as_bytes(&self) -> &[u8; 32] {
58        &self.0
59    }
60}
61
62impl<'de> Deserialize<'de> for StateActionId {
63    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64    where
65        D: Deserializer<'de>,
66    {
67        let bytes = <[u8; 32]>::deserialize(deserializer)?;
68        Self::new(bytes).map_err(de::Error::custom)
69    }
70}
71
72#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
73#[serde(deny_unknown_fields)]
74pub struct StateHashFieldV1 {
75    pub field: Vec<u8>,
76    pub value: Vec<u8>,
77}
78
79#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
80#[serde(tag = "kind", content = "value", rename_all = "snake_case", deny_unknown_fields)]
81pub enum StateDataValueV1 {
82    Bytes(Vec<u8>),
83    Hash(Vec<StateHashFieldV1>),
84    /// Byte-exact canonical JSON object data. This is user data rather than an
85    /// open control-plane shape; adapters choose whether their dialect can
86    /// represent it.
87    Json(Vec<u8>),
88}
89
90impl StateDataValueV1 {
91    pub fn validate(&self) -> Result<(), StateContractError> {
92        match self {
93            Self::Bytes(bytes) => validate_value_bytes(bytes.len()),
94            Self::Json(bytes) => {
95                validate_value_bytes(bytes.len())?;
96                let parsed: serde_json::Value =
97                    serde_json::from_slice(bytes).map_err(|_| StateContractError::InvalidJsonValue)?;
98                if !parsed.is_object()
99                    || serde_json::to_vec(&parsed).map_err(|_| StateContractError::InvalidJsonValue)? != *bytes
100                {
101                    return Err(StateContractError::InvalidJsonValue);
102                }
103                Ok(())
104            }
105            Self::Hash(fields) => validate_hash_fields(fields),
106        }
107    }
108}
109
110fn validate_hash_fields(fields: &[StateHashFieldV1]) -> Result<(), StateContractError> {
111    if fields.len() > MAX_STATE_COMMAND_HASH_FIELDS {
112        return Err(StateContractError::TooManyHashFields(fields.len()));
113    }
114    let mut prior: Option<&[u8]> = None;
115    let mut total = 0_usize;
116    for field in fields {
117        if field.field.is_empty() || prior.is_some_and(|value| value >= field.field.as_slice()) {
118            return Err(StateContractError::NonCanonicalHashFields);
119        }
120        prior = Some(&field.field);
121        total = total
122            .checked_add(field.field.len())
123            .and_then(|value| value.checked_add(field.value.len()))
124            .ok_or(StateContractError::ValueTooLarge(usize::MAX))?;
125    }
126    validate_value_bytes(total)
127}
128
129#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub enum StateSetConditionV1 {
132    Always,
133    IfAbsent,
134    IfPresent,
135}
136
137#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
138#[serde(deny_unknown_fields)]
139pub struct StateSetEntryV1 {
140    pub key: Vec<u8>,
141    pub value: Vec<u8>,
142}
143
144#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
145#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
146pub enum StateOperationV1 {
147    Get {
148        key: Vec<u8>,
149    },
150    Set {
151        key: Vec<u8>,
152        value: Vec<u8>,
153        condition: StateSetConditionV1,
154        expires_at_millis: Option<u64>,
155    },
156    Delete {
157        keys: Vec<Vec<u8>>,
158    },
159    Exists {
160        keys: Vec<Vec<u8>>,
161    },
162    ExpireAt {
163        key: Vec<u8>,
164        absolute_millis: u64,
165    },
166    TtlStatus {
167        key: Vec<u8>,
168    },
169    Persist {
170        key: Vec<u8>,
171    },
172    MultiGet {
173        keys: Vec<Vec<u8>>,
174    },
175    MultiSet {
176        entries: Vec<StateSetEntryV1>,
177    },
178    Increment {
179        key: Vec<u8>,
180        delta: i64,
181    },
182    HashGet {
183        key: Vec<u8>,
184        field: Vec<u8>,
185    },
186    HashSet {
187        key: Vec<u8>,
188        fields: Vec<StateHashFieldV1>,
189    },
190    HashGetAll {
191        key: Vec<u8>,
192    },
193}
194
195/// Allocation-free view over the routing keys carried by one state command.
196pub struct StateRoutingKeys<'a> {
197    inner: StateRoutingKeysInner<'a>,
198}
199
200enum StateRoutingKeysInner<'a> {
201    One(Option<&'a [u8]>),
202    Keys(std::slice::Iter<'a, Vec<u8>>),
203    Entries(std::slice::Iter<'a, StateSetEntryV1>),
204}
205
206impl<'a> Iterator for StateRoutingKeys<'a> {
207    type Item = &'a [u8];
208
209    fn next(&mut self) -> Option<Self::Item> {
210        match &mut self.inner {
211            StateRoutingKeysInner::One(key) => key.take(),
212            StateRoutingKeysInner::Keys(keys) => keys.next().map(Vec::as_slice),
213            StateRoutingKeysInner::Entries(entries) => entries.next().map(|entry| entry.key.as_slice()),
214        }
215    }
216
217    fn size_hint(&self) -> (usize, Option<usize>) {
218        let remaining = self.len();
219        (remaining, Some(remaining))
220    }
221}
222
223impl ExactSizeIterator for StateRoutingKeys<'_> {
224    fn len(&self) -> usize {
225        match &self.inner {
226            StateRoutingKeysInner::One(key) => usize::from(key.is_some()),
227            StateRoutingKeysInner::Keys(keys) => keys.len(),
228            StateRoutingKeysInner::Entries(entries) => entries.len(),
229        }
230    }
231}
232
233impl StateOperationV1 {
234    #[must_use]
235    pub const fn is_write(&self) -> bool {
236        matches!(
237            self,
238            Self::Set { .. }
239                | Self::Delete { .. }
240                | Self::ExpireAt { .. }
241                | Self::Persist { .. }
242                | Self::MultiSet { .. }
243                | Self::Increment { .. }
244                | Self::HashSet { .. }
245        )
246    }
247
248    #[must_use]
249    pub fn routing_keys(&self) -> Vec<&[u8]> {
250        self.routing_keys_iter().collect()
251    }
252
253    /// Iterates routing keys without allocating an intermediate collection.
254    #[must_use]
255    pub fn routing_keys_iter(&self) -> StateRoutingKeys<'_> {
256        match self {
257            Self::Get { key }
258            | Self::Set { key, .. }
259            | Self::ExpireAt { key, .. }
260            | Self::TtlStatus { key }
261            | Self::Persist { key }
262            | Self::Increment { key, .. }
263            | Self::HashGet { key, .. }
264            | Self::HashSet { key, .. }
265            | Self::HashGetAll { key } => StateRoutingKeys {
266                inner: StateRoutingKeysInner::One(Some(key)),
267            },
268            Self::Delete { keys } | Self::Exists { keys } | Self::MultiGet { keys } => StateRoutingKeys {
269                inner: StateRoutingKeysInner::Keys(keys.iter()),
270            },
271            Self::MultiSet { entries } => StateRoutingKeys {
272                inner: StateRoutingKeysInner::Entries(entries.iter()),
273            },
274        }
275    }
276
277    pub fn validate(&self) -> Result<(), StateContractError> {
278        let keys = self.routing_keys_iter();
279        let key_count = keys.len();
280        if key_count == 0 || key_count > MAX_STATE_COMMAND_KEYS {
281            return Err(StateContractError::InvalidKeyCount(key_count));
282        }
283        for key in keys {
284            validate_key(key)?;
285        }
286        match self {
287            Self::Set { value, .. } => validate_value_bytes(value.len())?,
288            Self::MultiSet { entries } => {
289                let mut unique = BTreeSet::new();
290                for entry in entries {
291                    validate_value_bytes(entry.value.len())?;
292                    if !unique.insert(entry.key.as_slice()) {
293                        return Err(StateContractError::DuplicateWriteKey);
294                    }
295                }
296                if entries.windows(2).any(|pair| pair[0].key >= pair[1].key) {
297                    return Err(StateContractError::NonCanonicalWriteKeys);
298                }
299            }
300            Self::Delete { keys } => {
301                let mut unique = BTreeSet::new();
302                if keys.iter().any(|key| !unique.insert(key.as_slice())) {
303                    return Err(StateContractError::DuplicateWriteKey);
304                }
305                if keys.windows(2).any(|pair| pair[0] >= pair[1]) {
306                    return Err(StateContractError::NonCanonicalWriteKeys);
307                }
308            }
309            Self::HashGet { field, .. } => {
310                if field.is_empty() || field.len() > MAX_STATE_COMMAND_VALUE_BYTES {
311                    return Err(StateContractError::InvalidHashField);
312                }
313            }
314            Self::HashSet { fields, .. } => validate_hash_fields(fields)?,
315            Self::Get { .. }
316            | Self::Exists { .. }
317            | Self::ExpireAt { .. }
318            | Self::TtlStatus { .. }
319            | Self::Persist { .. }
320            | Self::MultiGet { .. }
321            | Self::Increment { .. }
322            | Self::HashGetAll { .. } => {}
323        }
324        Ok(())
325    }
326}
327
328#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
329#[serde(deny_unknown_fields)]
330pub struct StateCommandV1 {
331    pub format_version: u16,
332    pub operation: StateOperationV1,
333}
334
335impl StateCommandV1 {
336    #[must_use]
337    pub const fn new(operation: StateOperationV1) -> Self {
338        Self {
339            format_version: STATE_COMMAND_FORMAT_VERSION,
340            operation,
341        }
342    }
343
344    pub fn validate(&self) -> Result<(), StateContractError> {
345        if self.format_version != STATE_COMMAND_FORMAT_VERSION {
346            return Err(StateContractError::UnsupportedVersion(self.format_version));
347        }
348        self.operation.validate()?;
349        if self.serialized_upper_bound() <= MAX_STATE_COMMAND_BYTES {
350            return Ok(());
351        }
352        let bytes = serde_json::to_vec(self).map_err(|error| StateContractError::Codec(error.to_string()))?;
353        if bytes.len() > MAX_STATE_COMMAND_BYTES {
354            return Err(StateContractError::CommandTooLarge(bytes.len()));
355        }
356        Ok(())
357    }
358
359    fn serialized_upper_bound(&self) -> usize {
360        // INVARIANT: every byte vector is included by this exhaustive match.
361        // Keeping this bound conservative leaves the exact JSON length check
362        // authoritative for commands close to the public 2 MiB ceiling.
363        let key_bytes = self
364            .operation
365            .routing_keys_iter()
366            .fold(0_usize, |total, key| total.saturating_add(key.len()));
367        let other_bytes = match &self.operation {
368            StateOperationV1::Set { value, .. } => value.len(),
369            StateOperationV1::MultiSet { entries } => entries
370                .iter()
371                .fold(0_usize, |total, entry| total.saturating_add(entry.value.len())),
372            StateOperationV1::HashGet { field, .. } => field.len(),
373            StateOperationV1::HashSet { fields, .. } => fields.iter().fold(0_usize, |total, field| {
374                total
375                    .saturating_add(field.field.len())
376                    .saturating_add(field.value.len())
377            }),
378            StateOperationV1::Get { .. }
379            | StateOperationV1::Delete { .. }
380            | StateOperationV1::Exists { .. }
381            | StateOperationV1::ExpireAt { .. }
382            | StateOperationV1::TtlStatus { .. }
383            | StateOperationV1::Persist { .. }
384            | StateOperationV1::MultiGet { .. }
385            | StateOperationV1::Increment { .. }
386            | StateOperationV1::HashGetAll { .. } => 0,
387        };
388        key_bytes
389            .saturating_add(other_bytes)
390            .saturating_mul(4)
391            .saturating_add(STATE_COMMAND_JSON_STRUCTURAL_ALLOWANCE)
392    }
393
394    #[must_use]
395    pub const fn is_write(&self) -> bool {
396        self.operation.is_write()
397    }
398
399    #[must_use]
400    pub fn routing_keys(&self) -> Vec<&[u8]> {
401        self.operation.routing_keys()
402    }
403
404    /// Iterates routing keys without allocating an intermediate collection.
405    #[must_use]
406    pub fn routing_keys_iter(&self) -> StateRoutingKeys<'_> {
407        self.operation.routing_keys_iter()
408    }
409
410    /// Captures only the result-shape and bounded-count contract needed after
411    /// execution has consumed the command payload.
412    #[must_use]
413    pub fn result_expectation(&self) -> StateCommandResultExpectation {
414        match &self.operation {
415            StateOperationV1::Get { .. } => StateCommandResultExpectation::Get,
416            StateOperationV1::Set { .. } => StateCommandResultExpectation::Set,
417            StateOperationV1::Delete { keys } => StateCommandResultExpectation::Delete {
418                maximum_removed: keys.len(),
419            },
420            StateOperationV1::Exists { keys } => StateCommandResultExpectation::Exists {
421                maximum_count: keys.len(),
422            },
423            StateOperationV1::ExpireAt { .. } => StateCommandResultExpectation::Expire,
424            StateOperationV1::TtlStatus { .. } => StateCommandResultExpectation::TtlStatus,
425            StateOperationV1::Persist { .. } => StateCommandResultExpectation::Persist,
426            StateOperationV1::MultiGet { keys } => StateCommandResultExpectation::MultiGet {
427                expected_values: keys.len(),
428            },
429            StateOperationV1::MultiSet { entries } => StateCommandResultExpectation::MultiSet {
430                expected_written: entries.len(),
431            },
432            StateOperationV1::Increment { .. } => StateCommandResultExpectation::Increment,
433            StateOperationV1::HashGet { .. } => StateCommandResultExpectation::HashGet,
434            StateOperationV1::HashSet { fields, .. } => StateCommandResultExpectation::HashSet {
435                maximum_fields_added: fields.len(),
436            },
437            StateOperationV1::HashGetAll { .. } => StateCommandResultExpectation::HashGetAll,
438        }
439    }
440
441    pub fn semantic_digest(
442        &self,
443        collection_index_ref: CollectionIndexRef,
444        state_incarnation: StateIncarnation,
445    ) -> Result<OperationFingerprint, StateContractError> {
446        self.validate()?;
447        if state_incarnation.get() == 0 {
448            return Err(StateContractError::ZeroStateIncarnation);
449        }
450        let body = serde_json::to_vec(self).map_err(|error| StateContractError::Codec(error.to_string()))?;
451        let mut hasher = blake3::Hasher::new();
452        hash_part(&mut hasher, b"lodestone/state-command-semantic/v1");
453        hash_part(&mut hasher, &collection_index_ref.collection_id().get().to_be_bytes());
454        hash_part(&mut hasher, &collection_index_ref.index_id().get().to_be_bytes());
455        hash_part(&mut hasher, &state_incarnation.get().to_be_bytes());
456        hash_part(&mut hasher, &body);
457        Ok(OperationFingerprint::new(*hasher.finalize().as_bytes()))
458    }
459}
460
461#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
462#[serde(deny_unknown_fields)]
463pub struct StateWriteIntentV1 {
464    pub action_id: StateActionId,
465    pub submitted_state_incarnation: StateIncarnation,
466    pub semantic_digest: OperationFingerprint,
467}
468
469impl StateWriteIntentV1 {
470    pub fn validate_for(
471        self,
472        command: &StateCommandV1,
473        collection_index_ref: CollectionIndexRef,
474    ) -> Result<(), StateContractError> {
475        if !command.is_write() || self.submitted_state_incarnation.get() == 0 {
476            return Err(StateContractError::InvalidWriteIntent);
477        }
478        if command.semantic_digest(collection_index_ref, self.submitted_state_incarnation)? != self.semantic_digest {
479            return Err(StateContractError::SemanticDigestMismatch);
480        }
481        Ok(())
482    }
483}
484
485#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
486#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
487pub enum StateWriteResultV1 {
488    Set { applied: bool },
489    Delete { removed: u32 },
490    Expire { changed: bool },
491    Persist { changed: bool },
492    MultiSet { written: u32 },
493    Increment { value: i64 },
494    HashSet { fields_added: u32 },
495}
496
497#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
498#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
499pub enum StateTtlStatusResultV1 {
500    Missing,
501    Persistent,
502    ExpiresInMillis { remaining_millis: u64 },
503}
504
505#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
506#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
507pub enum StateCommandResultV1 {
508    Get { value: Option<StateDataValueV1> },
509    Set { applied: bool },
510    Delete { removed: u32 },
511    Exists { count: u32 },
512    Expire { changed: bool },
513    TtlStatus { status: StateTtlStatusResultV1 },
514    Persist { changed: bool },
515    MultiGet { values: Vec<Option<StateDataValueV1>> },
516    MultiSet { written: u32 },
517    Increment { value: i64 },
518    HashGet { value: Option<Vec<u8>> },
519    HashSet { fields_added: u32 },
520    HashGetAll { fields: Vec<StateHashFieldV1> },
521}
522
523/// Non-serialized validation descriptor retained when execution consumes a
524/// state command's owned key/value payload.
525#[derive(Clone, Copy, Debug, Eq, PartialEq)]
526pub enum StateCommandResultExpectation {
527    Get,
528    Set,
529    Delete { maximum_removed: usize },
530    Exists { maximum_count: usize },
531    Expire,
532    TtlStatus,
533    Persist,
534    MultiGet { expected_values: usize },
535    MultiSet { expected_written: usize },
536    Increment,
537    HashGet,
538    HashSet { maximum_fields_added: usize },
539    HashGetAll,
540}
541
542impl StateCommandResultV1 {
543    pub fn validate_for(&self, command: &StateCommandV1) -> Result<(), StateContractError> {
544        self.validate_for_expectation(command.result_expectation())
545    }
546
547    pub fn validate_for_expectation(
548        &self,
549        expectation: StateCommandResultExpectation,
550    ) -> Result<(), StateContractError> {
551        let shape_matches = matches!(
552            (expectation, self),
553            (StateCommandResultExpectation::Get, Self::Get { .. })
554                | (StateCommandResultExpectation::Set, Self::Set { .. })
555                | (StateCommandResultExpectation::Delete { .. }, Self::Delete { .. })
556                | (StateCommandResultExpectation::Exists { .. }, Self::Exists { .. })
557                | (StateCommandResultExpectation::Expire, Self::Expire { .. })
558                | (StateCommandResultExpectation::TtlStatus, Self::TtlStatus { .. })
559                | (StateCommandResultExpectation::Persist, Self::Persist { .. })
560                | (StateCommandResultExpectation::MultiGet { .. }, Self::MultiGet { .. })
561                | (StateCommandResultExpectation::MultiSet { .. }, Self::MultiSet { .. })
562                | (StateCommandResultExpectation::Increment, Self::Increment { .. })
563                | (StateCommandResultExpectation::HashGet, Self::HashGet { .. })
564                | (StateCommandResultExpectation::HashSet { .. }, Self::HashSet { .. })
565                | (StateCommandResultExpectation::HashGetAll, Self::HashGetAll { .. })
566        );
567        if !shape_matches {
568            return Err(StateContractError::InvalidCommandResult);
569        }
570        match (expectation, self) {
571            (StateCommandResultExpectation::Delete { maximum_removed }, Self::Delete { removed })
572                if usize::try_from(*removed).map_or(true, |count| count > maximum_removed) =>
573            {
574                return Err(StateContractError::InvalidCommandResult);
575            }
576            (StateCommandResultExpectation::Exists { maximum_count }, Self::Exists { count })
577                if usize::try_from(*count).map_or(true, |count| count > maximum_count) =>
578            {
579                return Err(StateContractError::InvalidCommandResult);
580            }
581            (StateCommandResultExpectation::MultiGet { expected_values }, Self::MultiGet { values })
582                if values.len() != expected_values =>
583            {
584                return Err(StateContractError::InvalidCommandResult);
585            }
586            (StateCommandResultExpectation::MultiSet { expected_written }, Self::MultiSet { written })
587                if usize::try_from(*written) != Ok(expected_written) =>
588            {
589                return Err(StateContractError::InvalidCommandResult);
590            }
591            (StateCommandResultExpectation::HashSet { maximum_fields_added }, Self::HashSet { fields_added })
592                if usize::try_from(*fields_added).map_or(true, |count| count > maximum_fields_added) =>
593            {
594                return Err(StateContractError::InvalidCommandResult);
595            }
596            _ => {}
597        }
598        match self {
599            Self::Get { value: Some(value) } => value.validate()?,
600            Self::MultiGet { values } => {
601                if values.len() > MAX_STATE_COMMAND_KEYS {
602                    return Err(StateContractError::InvalidCommandResult);
603                }
604                for value in values.iter().flatten() {
605                    value.validate()?;
606                }
607            }
608            Self::Delete { removed } | Self::Exists { count: removed } | Self::MultiSet { written: removed }
609                if usize::try_from(*removed).map_or(true, |count| count > MAX_STATE_COMMAND_KEYS) =>
610            {
611                return Err(StateContractError::InvalidCommandResult);
612            }
613            Self::HashSet { fields_added }
614                if usize::try_from(*fields_added).map_or(true, |count| count > MAX_STATE_COMMAND_HASH_FIELDS) =>
615            {
616                return Err(StateContractError::InvalidCommandResult);
617            }
618            Self::HashGet { value: Some(value) } => validate_value_bytes(value.len())?,
619            Self::HashGetAll { fields } => StateDataValueV1::Hash(fields.clone()).validate()?,
620            _ => {}
621        }
622        Ok(())
623    }
624
625    pub fn as_write_result(&self) -> Option<StateWriteResultV1> {
626        match self {
627            Self::Set { applied } => Some(StateWriteResultV1::Set { applied: *applied }),
628            Self::Delete { removed } => Some(StateWriteResultV1::Delete { removed: *removed }),
629            Self::Expire { changed } => Some(StateWriteResultV1::Expire { changed: *changed }),
630            Self::Persist { changed } => Some(StateWriteResultV1::Persist { changed: *changed }),
631            Self::MultiSet { written } => Some(StateWriteResultV1::MultiSet { written: *written }),
632            Self::Increment { value } => Some(StateWriteResultV1::Increment { value: *value }),
633            Self::HashSet { fields_added } => Some(StateWriteResultV1::HashSet {
634                fields_added: *fields_added,
635            }),
636            Self::Get { .. }
637            | Self::Exists { .. }
638            | Self::TtlStatus { .. }
639            | Self::MultiGet { .. }
640            | Self::HashGet { .. }
641            | Self::HashGetAll { .. } => None,
642        }
643    }
644}
645
646impl StateWriteResultV1 {
647    pub fn validate(&self) -> Result<(), StateContractError> {
648        match self {
649            Self::Delete { removed } | Self::MultiSet { written: removed }
650                if usize::try_from(*removed).map_or(true, |count| count > MAX_STATE_COMMAND_KEYS) =>
651            {
652                Err(StateContractError::InvalidWriteResult)
653            }
654            Self::HashSet { fields_added }
655                if usize::try_from(*fields_added).map_or(true, |count| count > MAX_STATE_COMMAND_HASH_FIELDS) =>
656            {
657                Err(StateContractError::InvalidWriteResult)
658            }
659            _ => Ok(()),
660        }
661    }
662}
663
664#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
665#[serde(deny_unknown_fields)]
666pub struct StateWriteOutcomeRecordV1 {
667    pub format_version: u16,
668    pub action_id: StateActionId,
669    pub semantic_digest: OperationFingerprint,
670    pub submitted_state_incarnation: StateIncarnation,
671    pub durability_class: DurabilityClass,
672    pub first_sequence: SeqNo,
673    pub last_sequence: SeqNo,
674    pub result: StateWriteResultV1,
675}
676
677impl StateWriteOutcomeRecordV1 {
678    pub fn validate(&self) -> Result<(), StateContractError> {
679        if self.format_version != STATE_WRITE_OUTCOME_FORMAT_VERSION {
680            return Err(StateContractError::UnsupportedVersion(self.format_version));
681        }
682        if self.submitted_state_incarnation.get() == 0
683            || self.first_sequence.get() == 0
684            || self.last_sequence < self.first_sequence
685            || !matches!(
686                self.durability_class,
687                DurabilityClass::RegionalObject | DurabilityClass::ZonalLowLatency | DurabilityClass::Ephemeral
688            )
689        {
690            return Err(StateContractError::InvalidOutcomeSequence);
691        }
692        self.result.validate()?;
693        let expected_operations = match &self.result {
694            StateWriteResultV1::Delete { removed: 0 } => 1,
695            StateWriteResultV1::Delete { removed } | StateWriteResultV1::MultiSet { written: removed } => {
696                u64::from(*removed)
697            }
698            StateWriteResultV1::Set { .. }
699            | StateWriteResultV1::Expire { .. }
700            | StateWriteResultV1::Persist { .. }
701            | StateWriteResultV1::Increment { .. }
702            | StateWriteResultV1::HashSet { .. } => 1,
703        };
704        let actual_operations = self
705            .last_sequence
706            .get()
707            .checked_sub(self.first_sequence.get())
708            .and_then(|span| span.checked_add(1))
709            .ok_or(StateContractError::InvalidOutcomeSequence)?;
710        if actual_operations != expected_operations {
711            return Err(StateContractError::InvalidOutcomeSequence);
712        }
713        Ok(())
714    }
715}
716
717#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
718#[serde(rename_all = "snake_case")]
719pub enum StateSearchVisibilityV1 {
720    Pending,
721    Visible,
722}
723
724#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
725#[serde(deny_unknown_fields)]
726pub struct StateWriteCompletionV1 {
727    pub outcome: StateWriteOutcomeRecordV1,
728    pub durability_class: DurabilityClass,
729    pub durability_boundary: DurabilityBoundary,
730    pub maximum_loss: DurabilityLoss,
731    pub search_visibility: StateSearchVisibilityV1,
732    pub deduplicated: bool,
733}
734
735impl StateWriteCompletionV1 {
736    pub fn validate(&self) -> Result<(), StateContractError> {
737        self.outcome.validate()?;
738        if self.durability_class != self.outcome.durability_class {
739            return Err(StateContractError::DurabilityMismatch);
740        }
741        match self.durability_class {
742            DurabilityClass::RegionalObject | DurabilityClass::ZonalLowLatency
743                if self.durability_boundary == DurabilityBoundary::Registered
744                    && self.maximum_loss == DurabilityLoss::Zero =>
745            {
746                Ok(())
747            }
748            DurabilityClass::Ephemeral
749                if self.durability_boundary == DurabilityBoundary::IncarnationMemory
750                    && self.maximum_loss == DurabilityLoss::WholeIncarnation =>
751            {
752                Ok(())
753            }
754            _ => Err(StateContractError::DurabilityMismatch),
755        }
756    }
757}
758
759#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
760#[serde(deny_unknown_fields)]
761pub struct StateOutcomeRetentionV1 {
762    pub maximum_entries: u32,
763    pub sequence_window: u64,
764    pub retired_through: Option<SeqNo>,
765    pub current_high_water: Option<SeqNo>,
766}
767
768impl StateOutcomeRetentionV1 {
769    pub fn validate(self) -> Result<(), StateContractError> {
770        if self.maximum_entries == 0
771            || usize::try_from(self.maximum_entries)
772                .map_or(true, |maximum| maximum > MAX_REGISTERED_STATE_OUTCOME_ENTRIES)
773            || self.sequence_window == 0
774            || self.sequence_window > REGISTERED_STATE_OUTCOME_SEQUENCE_WINDOW
775            || self.retired_through.is_some_and(|sequence| sequence.get() == 0)
776            || self.current_high_water.is_some_and(|sequence| sequence.get() == 0)
777            || (self.retired_through.is_some() && self.current_high_water.is_none())
778            || self
779                .retired_through
780                .zip(self.current_high_water)
781                .is_some_and(|(retired, high)| retired > high)
782        {
783            return Err(StateContractError::InvalidRetention);
784        }
785        Ok(())
786    }
787}
788
789#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
790#[serde(deny_unknown_fields)]
791pub struct RegisteredStateRecoveryLocatorV1 {
792    pub format_version: u16,
793    pub collection_index_ref: CollectionIndexRef,
794    pub shard_id: ShardId,
795    pub routing_revision: RoutingRevision,
796    pub submitted_fence: FenceToken,
797    pub action_id: StateActionId,
798    pub submitted_state_incarnation: StateIncarnation,
799    pub semantic_digest: OperationFingerprint,
800    pub observed_registered_high_water: Option<SeqNo>,
801    pub retention_maximum_entries: u32,
802    pub retention_sequence_window: u64,
803}
804
805impl RegisteredStateRecoveryLocatorV1 {
806    pub fn validate(&self) -> Result<(), StateContractError> {
807        if self.format_version != REGISTERED_STATE_RECOVERY_LOCATOR_FORMAT_VERSION {
808            return Err(StateContractError::UnsupportedVersion(self.format_version));
809        }
810        if self.submitted_state_incarnation.get() == 0 || self.submitted_fence.epoch().get() == 0 {
811            return Err(StateContractError::InvalidLocator);
812        }
813        StateOutcomeRetentionV1 {
814            maximum_entries: self.retention_maximum_entries,
815            sequence_window: self.retention_sequence_window,
816            retired_through: None,
817            current_high_water: self.observed_registered_high_water,
818        }
819        .validate()
820    }
821}
822
823#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
824#[serde(deny_unknown_fields)]
825pub struct EphemeralStateActionReferenceV1 {
826    pub format_version: u16,
827    pub collection_index_ref: CollectionIndexRef,
828    pub shard_id: ShardId,
829    pub routing_revision: RoutingRevision,
830    pub action_id: StateActionId,
831    pub reset_epoch: StateIncarnation,
832    pub semantic_digest: OperationFingerprint,
833    pub observed_sequence_high_water: Option<SeqNo>,
834}
835
836impl EphemeralStateActionReferenceV1 {
837    pub fn validate(&self) -> Result<(), StateContractError> {
838        if self.format_version != EPHEMERAL_STATE_ACTION_REFERENCE_FORMAT_VERSION {
839            return Err(StateContractError::UnsupportedVersion(self.format_version));
840        }
841        if self.reset_epoch.get() == 0
842            || self
843                .observed_sequence_high_water
844                .is_some_and(|sequence| sequence.get() == 0)
845        {
846            return Err(StateContractError::InvalidLocator);
847        }
848        Ok(())
849    }
850}
851
852#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
853#[serde(
854    tag = "durability",
855    content = "locator",
856    rename_all = "snake_case",
857    deny_unknown_fields
858)]
859pub enum PreparedStateWriteV1 {
860    Registered(RegisteredStateRecoveryLocatorV1),
861    Ephemeral(EphemeralStateActionReferenceV1),
862}
863
864impl PreparedStateWriteV1 {
865    // INVARIANT: construction binds the complete pre-submit route, fence,
866    // action, incarnation, command digest, and retention cut atomically.
867    #[allow(clippy::too_many_arguments)]
868    pub fn registered(
869        collection_index_ref: CollectionIndexRef,
870        shard_id: ShardId,
871        routing_revision: RoutingRevision,
872        submitted_fence: FenceToken,
873        action_id: StateActionId,
874        state_incarnation: StateIncarnation,
875        command: &StateCommandV1,
876        retention: StateOutcomeRetentionV1,
877    ) -> Result<Self, StateContractError> {
878        retention.validate()?;
879        let locator = RegisteredStateRecoveryLocatorV1 {
880            format_version: REGISTERED_STATE_RECOVERY_LOCATOR_FORMAT_VERSION,
881            collection_index_ref,
882            shard_id,
883            routing_revision,
884            submitted_fence,
885            action_id,
886            submitted_state_incarnation: state_incarnation,
887            semantic_digest: command.semantic_digest(collection_index_ref, state_incarnation)?,
888            observed_registered_high_water: retention.current_high_water,
889            retention_maximum_entries: retention.maximum_entries,
890            retention_sequence_window: retention.sequence_window,
891        };
892        locator.validate()?;
893        Ok(Self::Registered(locator))
894    }
895
896    pub fn ephemeral(
897        collection_index_ref: CollectionIndexRef,
898        shard_id: ShardId,
899        routing_revision: RoutingRevision,
900        action_id: StateActionId,
901        reset_epoch: StateIncarnation,
902        command: &StateCommandV1,
903        retention: StateOutcomeRetentionV1,
904    ) -> Result<Self, StateContractError> {
905        retention.validate()?;
906        if usize::try_from(retention.maximum_entries)
907            .map_or(true, |maximum| maximum > MAX_EPHEMERAL_STATE_OUTCOME_ENTRIES)
908            || retention.sequence_window > EPHEMERAL_STATE_OUTCOME_SEQUENCE_WINDOW
909        {
910            return Err(StateContractError::InvalidRetention);
911        }
912        let reference = EphemeralStateActionReferenceV1 {
913            format_version: EPHEMERAL_STATE_ACTION_REFERENCE_FORMAT_VERSION,
914            collection_index_ref,
915            shard_id,
916            routing_revision,
917            action_id,
918            reset_epoch,
919            semantic_digest: command.semantic_digest(collection_index_ref, reset_epoch)?,
920            observed_sequence_high_water: retention.current_high_water,
921        };
922        reference.validate()?;
923        Ok(Self::Ephemeral(reference))
924    }
925
926    pub fn intent_for(
927        &self,
928        command: &StateCommandV1,
929        expected_collection_index_ref: CollectionIndexRef,
930    ) -> Result<StateWriteIntentV1, StateContractError> {
931        let (collection_index_ref, action_id, submitted_state_incarnation, semantic_digest) = match self {
932            Self::Registered(locator) => {
933                locator.validate()?;
934                (
935                    locator.collection_index_ref,
936                    locator.action_id,
937                    locator.submitted_state_incarnation,
938                    locator.semantic_digest,
939                )
940            }
941            Self::Ephemeral(reference) => {
942                reference.validate()?;
943                (
944                    reference.collection_index_ref,
945                    reference.action_id,
946                    reference.reset_epoch,
947                    reference.semantic_digest,
948                )
949            }
950        };
951        if collection_index_ref != expected_collection_index_ref {
952            return Err(StateContractError::InvalidLocator);
953        }
954        let intent = StateWriteIntentV1 {
955            action_id,
956            submitted_state_incarnation,
957            semantic_digest,
958        };
959        intent.validate_for(command, expected_collection_index_ref)?;
960        Ok(intent)
961    }
962
963    #[must_use]
964    pub const fn shard_id(&self) -> ShardId {
965        match self {
966            Self::Registered(locator) => locator.shard_id,
967            Self::Ephemeral(reference) => reference.shard_id,
968        }
969    }
970
971    #[must_use]
972    pub const fn routing_revision(&self) -> RoutingRevision {
973        match self {
974            Self::Registered(locator) => locator.routing_revision,
975            Self::Ephemeral(reference) => reference.routing_revision,
976        }
977    }
978
979    #[must_use]
980    pub const fn observed_high_water(&self) -> Option<SeqNo> {
981        match self {
982            Self::Registered(locator) => locator.observed_registered_high_water,
983            Self::Ephemeral(reference) => reference.observed_sequence_high_water,
984        }
985    }
986}
987
988#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
989#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
990pub enum StateWriteResolutionV1 {
991    NotSubmitted,
992    Resolved {
993        completion: StateWriteCompletionV1,
994    },
995    KnownRetired {
996        retired_through: SeqNo,
997    },
998    StateReset {
999        submitted_reset_epoch: StateIncarnation,
1000        current_reset_epoch: StateIncarnation,
1001    },
1002    Indeterminate {
1003        observed_high_water: Option<SeqNo>,
1004        current_high_water: Option<SeqNo>,
1005        retired_through: Option<SeqNo>,
1006    },
1007}
1008
1009pub fn resolve_absent_state_action_v1(
1010    observed_high_water: Option<SeqNo>,
1011    retention: StateOutcomeRetentionV1,
1012) -> Result<StateWriteResolutionV1, StateContractError> {
1013    retention.validate()?;
1014    if observed_high_water.is_some_and(|observed| retention.current_high_water.is_none_or(|current| observed > current))
1015    {
1016        return Ok(StateWriteResolutionV1::Indeterminate {
1017            observed_high_water,
1018            current_high_water: retention.current_high_water,
1019            retired_through: retention.retired_through,
1020        });
1021    }
1022    if retention
1023        .retired_through
1024        .is_some_and(|floor| observed_high_water.is_none_or(|observed| floor > observed))
1025    {
1026        return Ok(StateWriteResolutionV1::KnownRetired {
1027            retired_through: retention.retired_through.ok_or(StateContractError::InvalidRetention)?,
1028        });
1029    }
1030    // INVARIANT: if retirement has not crossed the preparation cut, every
1031    // action submitted after that cut would still be in the outcome map.
1032    // Absence therefore proves non-submission even if unrelated state
1033    // sequences advanced the high-water mark.
1034    Ok(StateWriteResolutionV1::NotSubmitted)
1035}
1036
1037pub fn encode_state_command_v1(command: &StateCommandV1) -> Result<Vec<u8>, StateContractError> {
1038    command.validate()?;
1039    serde_json::to_vec(command).map_err(|error| StateContractError::Codec(error.to_string()))
1040}
1041
1042pub fn decode_state_command_v1(bytes: &[u8]) -> Result<StateCommandV1, StateContractError> {
1043    if bytes.is_empty() || bytes.len() > MAX_STATE_COMMAND_BYTES {
1044        return Err(StateContractError::CommandTooLarge(bytes.len()));
1045    }
1046    let command = serde_json::from_slice::<StateCommandV1>(bytes)
1047        .map_err(|error| StateContractError::Codec(error.to_string()))?;
1048    command.validate()?;
1049    let canonical = serde_json::to_vec(&command).map_err(|error| StateContractError::Codec(error.to_string()))?;
1050    if canonical != bytes {
1051        return Err(StateContractError::NonCanonicalEncoding);
1052    }
1053    Ok(command)
1054}
1055
1056#[derive(Clone, Debug, Eq, PartialEq)]
1057pub enum StateContractError {
1058    Codec(String),
1059    CommandTooLarge(usize),
1060    DuplicateWriteKey,
1061    DurabilityMismatch,
1062    InvalidHashField,
1063    InvalidJsonValue,
1064    InvalidCommandResult,
1065    InvalidKeyCount(usize),
1066    InvalidLocator,
1067    InvalidOutcomeSequence,
1068    InvalidRetention,
1069    InvalidWriteIntent,
1070    InvalidWriteResult,
1071    KeyTooLarge(usize),
1072    NonCanonicalEncoding,
1073    NonCanonicalHashFields,
1074    NonCanonicalWriteKeys,
1075    SemanticDigestMismatch,
1076    TooManyHashFields(usize),
1077    UnsupportedVersion(u16),
1078    ValueTooLarge(usize),
1079    ZeroActionId,
1080    ZeroStateIncarnation,
1081}
1082
1083impl Display for StateContractError {
1084    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1085        match self {
1086            Self::Codec(error) => write!(formatter, "state contract codec refusal: {error}"),
1087            Self::CommandTooLarge(bytes) => write!(formatter, "state command has {bytes} bytes"),
1088            Self::DuplicateWriteKey => formatter.write_str("state write batch contains a duplicate key"),
1089            Self::DurabilityMismatch => formatter.write_str("state completion durability fields disagree"),
1090            Self::InvalidHashField => formatter.write_str("state hash field is empty or oversized"),
1091            Self::InvalidJsonValue => formatter.write_str("state JSON value is not a canonical object"),
1092            Self::InvalidCommandResult => formatter.write_str("state command result differs from its request"),
1093            Self::InvalidKeyCount(count) => write!(formatter, "state command has invalid key count {count}"),
1094            Self::InvalidLocator => formatter.write_str("state recovery locator identity is invalid"),
1095            Self::InvalidOutcomeSequence => formatter.write_str("state outcome sequence range is invalid"),
1096            Self::InvalidRetention => formatter.write_str("state outcome retention bounds are invalid"),
1097            Self::InvalidWriteIntent => formatter.write_str("state write intent does not bind a write"),
1098            Self::InvalidWriteResult => formatter.write_str("state write result exceeds its command bound"),
1099            Self::KeyTooLarge(bytes) => write!(formatter, "state key has invalid length {bytes}"),
1100            Self::NonCanonicalEncoding => formatter.write_str("state command encoding is not canonical"),
1101            Self::NonCanonicalHashFields => formatter.write_str("state hash fields are not sorted and unique"),
1102            Self::NonCanonicalWriteKeys => formatter.write_str("state write keys are not sorted and unique"),
1103            Self::SemanticDigestMismatch => formatter.write_str("state write semantic digest differs"),
1104            Self::TooManyHashFields(count) => write!(formatter, "state hash has {count} fields"),
1105            Self::UnsupportedVersion(version) => write!(formatter, "unsupported state contract version {version}"),
1106            Self::ValueTooLarge(bytes) => write!(formatter, "state value has {bytes} bytes"),
1107            Self::ZeroActionId => formatter.write_str("state action identity must be nonzero"),
1108            Self::ZeroStateIncarnation => formatter.write_str("state incarnation must be nonzero"),
1109        }
1110    }
1111}
1112
1113impl Error for StateContractError {}
1114
1115fn validate_key(key: &[u8]) -> Result<(), StateContractError> {
1116    if key.is_empty() || key.len() > MAX_STATE_COMMAND_KEY_BYTES {
1117        return Err(StateContractError::KeyTooLarge(key.len()));
1118    }
1119    Ok(())
1120}
1121
1122fn validate_value_bytes(bytes: usize) -> Result<(), StateContractError> {
1123    if bytes > MAX_STATE_COMMAND_VALUE_BYTES {
1124        return Err(StateContractError::ValueTooLarge(bytes));
1125    }
1126    Ok(())
1127}
1128
1129fn hash_part(hasher: &mut blake3::Hasher, bytes: &[u8]) {
1130    hasher.update(&(bytes.len() as u64).to_be_bytes());
1131    hasher.update(bytes);
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136    use super::*;
1137    use crate::{CollectionId, Epoch, IndexId, RecoveryEra};
1138
1139    fn scope() -> Result<CollectionIndexRef, Box<dyn Error>> {
1140        Ok(CollectionIndexRef::new(CollectionId::new(7)?, IndexId::new(9)?))
1141    }
1142
1143    fn command() -> StateCommandV1 {
1144        StateCommandV1::new(StateOperationV1::Increment {
1145            key: b"counter".to_vec(),
1146            delta: 1,
1147        })
1148    }
1149
1150    #[test]
1151    fn compact_result_expectation_preserves_shape_and_count_validation() -> Result<(), Box<dyn Error>> {
1152        let command = StateCommandV1::new(StateOperationV1::MultiGet {
1153            keys: vec![b"{state}a".to_vec(), b"{state}b".to_vec()],
1154        });
1155        let expectation = command.result_expectation();
1156        StateCommandResultV1::MultiGet {
1157            values: vec![None, Some(StateDataValueV1::Bytes(b"value".to_vec()))],
1158        }
1159        .validate_for_expectation(expectation)?;
1160        assert_eq!(
1161            StateCommandResultV1::MultiGet { values: vec![None] }.validate_for_expectation(expectation),
1162            Err(StateContractError::InvalidCommandResult)
1163        );
1164        assert_eq!(
1165            StateCommandResultV1::Get { value: None }.validate_for_expectation(expectation),
1166            Err(StateContractError::InvalidCommandResult)
1167        );
1168
1169        assert_eq!(
1170            StateCommandResultV1::HashSet { fields_added: 3 }.validate_for_expectation(
1171                StateCommandResultExpectation::HashSet {
1172                    maximum_fields_added: 2,
1173                },
1174            ),
1175            Err(StateContractError::InvalidCommandResult)
1176        );
1177        Ok(())
1178    }
1179
1180    #[test]
1181    fn transport_command_is_canonical_bounded_and_resp_free() -> Result<(), Box<dyn Error>> {
1182        let command = command();
1183        let bytes = encode_state_command_v1(&command)?;
1184        assert_eq!(decode_state_command_v1(&bytes)?, command);
1185        assert!(!String::from_utf8(bytes)?.to_ascii_lowercase().contains("resp"));
1186
1187        let duplicate = StateCommandV1::new(StateOperationV1::MultiSet {
1188            entries: vec![
1189                StateSetEntryV1 {
1190                    key: b"same".to_vec(),
1191                    value: b"one".to_vec(),
1192                },
1193                StateSetEntryV1 {
1194                    key: b"same".to_vec(),
1195                    value: b"two".to_vec(),
1196                },
1197            ],
1198        });
1199        assert_eq!(duplicate.validate(), Err(StateContractError::DuplicateWriteKey));
1200        let reordered = StateCommandV1::new(StateOperationV1::Delete {
1201            keys: vec![b"z".to_vec(), b"a".to_vec()],
1202        });
1203        assert_eq!(reordered.validate(), Err(StateContractError::NonCanonicalWriteKeys));
1204        StateDataValueV1::Json(br#"{"value":1}"#.to_vec()).validate()?;
1205        assert_eq!(
1206            StateDataValueV1::Json(br#"{ "value": 1 }"#.to_vec()).validate(),
1207            Err(StateContractError::InvalidJsonValue)
1208        );
1209        Ok(())
1210    }
1211
1212    #[test]
1213    fn fast_command_size_bound_covers_every_variant_and_exact_fallback() -> Result<(), Box<dyn Error>> {
1214        let key = b"{state}key".to_vec();
1215        let value = vec![0, 9, 10, 99, 100, 255];
1216        let operations = vec![
1217            StateOperationV1::Get { key: key.clone() },
1218            StateOperationV1::Set {
1219                key: key.clone(),
1220                value: value.clone(),
1221                condition: StateSetConditionV1::IfAbsent,
1222                expires_at_millis: Some(u64::MAX),
1223            },
1224            StateOperationV1::Delete {
1225                keys: vec![key.clone()],
1226            },
1227            StateOperationV1::Exists {
1228                keys: vec![key.clone()],
1229            },
1230            StateOperationV1::ExpireAt {
1231                key: key.clone(),
1232                absolute_millis: u64::MAX,
1233            },
1234            StateOperationV1::TtlStatus { key: key.clone() },
1235            StateOperationV1::Persist { key: key.clone() },
1236            StateOperationV1::MultiGet {
1237                keys: vec![key.clone(), key.clone()],
1238            },
1239            StateOperationV1::MultiSet {
1240                entries: (0_u8..128)
1241                    .map(|ordinal| StateSetEntryV1 {
1242                        key: vec![ordinal],
1243                        value: value.clone(),
1244                    })
1245                    .collect(),
1246            },
1247            StateOperationV1::Increment {
1248                key: key.clone(),
1249                delta: i64::MIN,
1250            },
1251            StateOperationV1::HashGet {
1252                key: key.clone(),
1253                field: value.clone(),
1254            },
1255            StateOperationV1::HashSet {
1256                key: key.clone(),
1257                fields: (0_u16..1024)
1258                    .map(|ordinal| StateHashFieldV1 {
1259                        field: ordinal.to_be_bytes().to_vec(),
1260                        value: value.clone(),
1261                    })
1262                    .collect(),
1263            },
1264            StateOperationV1::HashGetAll { key },
1265        ];
1266        for operation in operations {
1267            let command = StateCommandV1::new(operation);
1268            command.operation.validate()?;
1269            let actual_bytes = serde_json::to_vec(&command)?.len();
1270            assert!(command.serialized_upper_bound() >= actual_bytes);
1271            assert_eq!(command.validate().is_ok(), actual_bytes <= MAX_STATE_COMMAND_BYTES);
1272        }
1273
1274        let accepted_fallback = StateCommandV1::new(StateOperationV1::Set {
1275            key: b"key".to_vec(),
1276            value: vec![0; 500_000],
1277            condition: StateSetConditionV1::Always,
1278            expires_at_millis: None,
1279        });
1280        assert!(accepted_fallback.serialized_upper_bound() > MAX_STATE_COMMAND_BYTES);
1281        accepted_fallback.validate()?;
1282        assert!(serde_json::to_vec(&accepted_fallback)?.len() <= MAX_STATE_COMMAND_BYTES);
1283
1284        let refused_fallback = StateCommandV1::new(StateOperationV1::Set {
1285            key: b"key".to_vec(),
1286            value: vec![255; 600_000],
1287            condition: StateSetConditionV1::Always,
1288            expires_at_millis: None,
1289        });
1290        assert!(refused_fallback.serialized_upper_bound() > MAX_STATE_COMMAND_BYTES);
1291        assert!(matches!(
1292            refused_fallback.validate(),
1293            Err(StateContractError::CommandTooLarge(bytes)) if bytes > MAX_STATE_COMMAND_BYTES
1294        ));
1295        Ok(())
1296    }
1297
1298    #[test]
1299    fn semantic_digest_binds_scope_incarnation_and_complete_command() -> Result<(), Box<dyn Error>> {
1300        let command = command();
1301        let digest = command.semantic_digest(scope()?, StateIncarnation::new(3))?;
1302        let intent = StateWriteIntentV1 {
1303            action_id: StateActionId::new([1; 32])?,
1304            submitted_state_incarnation: StateIncarnation::new(3),
1305            semantic_digest: digest,
1306        };
1307        intent.validate_for(&command, scope()?)?;
1308        assert_eq!(
1309            intent.validate_for(
1310                &command,
1311                CollectionIndexRef::new(CollectionId::new(8)?, IndexId::new(9)?)
1312            ),
1313            Err(StateContractError::SemanticDigestMismatch)
1314        );
1315        Ok(())
1316    }
1317
1318    #[test]
1319    fn absent_resolution_distinguishes_not_submitted_retired_and_indeterminate() -> Result<(), Box<dyn Error>> {
1320        let retention = |current: Option<u64>, retired: Option<u64>| StateOutcomeRetentionV1 {
1321            maximum_entries: 4,
1322            sequence_window: 16,
1323            current_high_water: current.map(SeqNo::new),
1324            retired_through: retired.map(SeqNo::new),
1325        };
1326        assert_eq!(
1327            resolve_absent_state_action_v1(Some(SeqNo::new(5)), retention(Some(5), None))?,
1328            StateWriteResolutionV1::NotSubmitted
1329        );
1330        assert_eq!(
1331            resolve_absent_state_action_v1(Some(SeqNo::new(5)), retention(Some(12), Some(8)))?,
1332            StateWriteResolutionV1::KnownRetired {
1333                retired_through: SeqNo::new(8)
1334            }
1335        );
1336        assert_eq!(
1337            resolve_absent_state_action_v1(Some(SeqNo::new(5)), retention(Some(12), Some(4)))?,
1338            StateWriteResolutionV1::NotSubmitted
1339        );
1340        assert!(matches!(
1341            resolve_absent_state_action_v1(Some(SeqNo::new(12)), retention(Some(5), None))?,
1342            StateWriteResolutionV1::Indeterminate { .. }
1343        ));
1344        assert_eq!(
1345            retention(None, Some(1)).validate(),
1346            Err(StateContractError::InvalidRetention)
1347        );
1348        assert_eq!(
1349            retention(Some(0), None).validate(),
1350            Err(StateContractError::InvalidRetention)
1351        );
1352        Ok(())
1353    }
1354
1355    #[test]
1356    fn locator_and_completion_reject_wrong_durability_or_identity() -> Result<(), Box<dyn Error>> {
1357        let locator = RegisteredStateRecoveryLocatorV1 {
1358            format_version: REGISTERED_STATE_RECOVERY_LOCATOR_FORMAT_VERSION,
1359            collection_index_ref: scope()?,
1360            shard_id: ShardId::new(3),
1361            routing_revision: RoutingRevision::new(4),
1362            submitted_fence: FenceToken::new(RecoveryEra::new(1)?, Epoch::new(2))?,
1363            action_id: StateActionId::new([3; 32])?,
1364            submitted_state_incarnation: StateIncarnation::new(5),
1365            semantic_digest: OperationFingerprint::new([4; 32]),
1366            observed_registered_high_water: Some(SeqNo::new(8)),
1367            retention_maximum_entries: 16,
1368            retention_sequence_window: 64,
1369        };
1370        locator.validate()?;
1371        let outcome = StateWriteOutcomeRecordV1 {
1372            format_version: STATE_WRITE_OUTCOME_FORMAT_VERSION,
1373            action_id: locator.action_id,
1374            semantic_digest: locator.semantic_digest,
1375            submitted_state_incarnation: locator.submitted_state_incarnation,
1376            durability_class: DurabilityClass::RegionalObject,
1377            first_sequence: SeqNo::new(9),
1378            last_sequence: SeqNo::new(9),
1379            result: StateWriteResultV1::Increment { value: 42 },
1380        };
1381        let completion = StateWriteCompletionV1 {
1382            outcome: outcome.clone(),
1383            durability_class: DurabilityClass::RegionalObject,
1384            durability_boundary: DurabilityBoundary::IncarnationMemory,
1385            maximum_loss: DurabilityLoss::Zero,
1386            search_visibility: StateSearchVisibilityV1::Pending,
1387            deduplicated: false,
1388        };
1389        assert_eq!(completion.validate(), Err(StateContractError::DurabilityMismatch));
1390        let rebound = StateWriteCompletionV1 {
1391            outcome,
1392            durability_class: DurabilityClass::ZonalLowLatency,
1393            durability_boundary: DurabilityBoundary::Registered,
1394            maximum_loss: DurabilityLoss::Zero,
1395            search_visibility: StateSearchVisibilityV1::Pending,
1396            deduplicated: true,
1397        };
1398        assert_eq!(rebound.validate(), Err(StateContractError::DurabilityMismatch));
1399        Ok(())
1400    }
1401
1402    #[test]
1403    fn outcome_sequence_span_is_exact_for_single_batch_and_no_effect_results() -> Result<(), Box<dyn Error>> {
1404        let action_id = StateActionId::new([5; 32])?;
1405        let outcome = |first, last, result| StateWriteOutcomeRecordV1 {
1406            format_version: STATE_WRITE_OUTCOME_FORMAT_VERSION,
1407            action_id,
1408            semantic_digest: OperationFingerprint::new([6; 32]),
1409            submitted_state_incarnation: StateIncarnation::new(7),
1410            durability_class: DurabilityClass::RegionalObject,
1411            first_sequence: SeqNo::new(first),
1412            last_sequence: SeqNo::new(last),
1413            result,
1414        };
1415        outcome(4, 4, StateWriteResultV1::Delete { removed: 0 }).validate()?;
1416        outcome(4, 6, StateWriteResultV1::MultiSet { written: 3 }).validate()?;
1417        assert_eq!(
1418            outcome(4, 5, StateWriteResultV1::Increment { value: 1 }).validate(),
1419            Err(StateContractError::InvalidOutcomeSequence)
1420        );
1421        assert_eq!(
1422            outcome(4, 6, StateWriteResultV1::MultiSet { written: 2 }).validate(),
1423            Err(StateContractError::InvalidOutcomeSequence)
1424        );
1425        Ok(())
1426    }
1427
1428    #[test]
1429    fn prepared_write_binds_route_scope_fence_reset_and_retention_before_submission() -> Result<(), Box<dyn Error>> {
1430        let command = command();
1431        let retention = StateOutcomeRetentionV1 {
1432            maximum_entries: 16,
1433            sequence_window: 64,
1434            retired_through: None,
1435            current_high_water: Some(SeqNo::new(8)),
1436        };
1437        let registered = PreparedStateWriteV1::registered(
1438            scope()?,
1439            ShardId::new(3),
1440            RoutingRevision::new(4),
1441            FenceToken::new(RecoveryEra::new(1)?, Epoch::new(2))?,
1442            StateActionId::new([8; 32])?,
1443            StateIncarnation::new(5),
1444            &command,
1445            retention,
1446        )?;
1447        assert_eq!(registered.observed_high_water(), Some(SeqNo::new(8)));
1448        registered.intent_for(&command, scope()?)?;
1449        assert_eq!(
1450            registered.intent_for(
1451                &command,
1452                CollectionIndexRef::new(CollectionId::new(9)?, IndexId::new(9)?)
1453            ),
1454            Err(StateContractError::InvalidLocator)
1455        );
1456
1457        let oversized_ephemeral = StateOutcomeRetentionV1 {
1458            maximum_entries: u32::try_from(MAX_EPHEMERAL_STATE_OUTCOME_ENTRIES + 1)?,
1459            sequence_window: EPHEMERAL_STATE_OUTCOME_SEQUENCE_WINDOW,
1460            retired_through: None,
1461            current_high_water: None,
1462        };
1463        assert_eq!(
1464            PreparedStateWriteV1::ephemeral(
1465                scope()?,
1466                ShardId::new(3),
1467                RoutingRevision::new(4),
1468                StateActionId::new([9; 32])?,
1469                StateIncarnation::new(5),
1470                &command,
1471                oversized_ephemeral,
1472            ),
1473            Err(StateContractError::InvalidRetention)
1474        );
1475        Ok(())
1476    }
1477}