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