1use 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;
28const 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 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
196pub 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 #[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 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 #[must_use]
407 pub fn routing_keys_iter(&self) -> StateRoutingKeys<'_> {
408 self.operation.routing_keys_iter()
409 }
410
411 #[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#[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 #[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 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}