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