Skip to main content

lodestone_types/
operation_locator.rs

1//! Opaque typed locators for per-authority operations and immutable artifacts.
2//!
3//! These locators are presentation identities only. They never authorize an
4//! operation, select a collection, or become a global completion ledger. Every
5//! consumer must route to the named existing authority and reauthorize the
6//! authenticated principal before reading or mutating state.
7
8use std::error::Error;
9use std::fmt::{self, Display, Formatter};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{CollectionId, RecoveryEra, crc32c};
14
15pub const OPERATION_LOCATOR_FORMAT_VERSION: u16 = 2;
16pub const ARTIFACT_LOCATOR_FORMAT_VERSION: u16 = 2;
17pub const ARTIFACT_CHUNK_ALIGNMENT: u64 = 4 * 1024;
18pub const MAX_ARTIFACT_CHUNK_BYTES: u32 = 1024 * 1024;
19
20const OPERATION_MAGIC: &[u8; 8] = b"LDOPLOC2";
21const OPERATION_LOCATOR_BYTES: usize = 208;
22const OPERATION_LOCATOR_BODY_BYTES: usize = OPERATION_LOCATOR_BYTES - 4;
23const ARTIFACT_MAGIC: &[u8; 8] = b"LDARLOC2";
24const ARTIFACT_LOCATOR_BYTES: usize = 224;
25const ARTIFACT_LOCATOR_BODY_BYTES: usize = ARTIFACT_LOCATOR_BYTES - 4;
26
27#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
28#[serde(rename_all = "snake_case")]
29pub enum OperationAuthorityKindV1 {
30    Catalog,
31    Administration,
32    Security,
33    Backup,
34    Maintenance,
35    Diagnostic,
36    Portability,
37}
38
39impl OperationAuthorityKindV1 {
40    const fn tag(self) -> u8 {
41        match self {
42            Self::Catalog => 1,
43            Self::Administration => 2,
44            Self::Security => 3,
45            Self::Backup => 4,
46            Self::Maintenance => 5,
47            Self::Diagnostic => 6,
48            Self::Portability => 7,
49        }
50    }
51
52    fn from_tag(tag: u8) -> Result<Self, OperationLocatorError> {
53        match tag {
54            1 => Ok(Self::Catalog),
55            2 => Ok(Self::Administration),
56            3 => Ok(Self::Security),
57            4 => Ok(Self::Backup),
58            5 => Ok(Self::Maintenance),
59            6 => Ok(Self::Diagnostic),
60            7 => Ok(Self::Portability),
61            value => Err(OperationLocatorError::UnsupportedAuthority(value)),
62        }
63    }
64}
65
66/// Opaque-to-clients locator for one receipt owned by one named authority.
67#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
68#[serde(deny_unknown_fields)]
69pub struct OperationLocatorV1 {
70    format_version: u16,
71    authority: OperationAuthorityKindV1,
72    collection_id: CollectionId,
73    authority_identity: [u8; 32],
74    operation_identity: [u8; 32],
75    recovery_era: RecoveryEra,
76    authority_revision: u64,
77    receipt_ordinal: u64,
78    semantic_digest: [u8; 32],
79    submitted_at: u64,
80    expires_at: u64,
81    resource_digest: [u8; 32],
82}
83
84impl OperationLocatorV1 {
85    #[allow(clippy::too_many_arguments)]
86    pub fn new(
87        authority: OperationAuthorityKindV1,
88        collection_id: CollectionId,
89        authority_identity: [u8; 32],
90        operation_identity: [u8; 32],
91        recovery_era: RecoveryEra,
92        authority_revision: u64,
93        receipt_ordinal: u64,
94        semantic_digest: [u8; 32],
95        submitted_at: u64,
96        expires_at: u64,
97        resource_digest: [u8; 32],
98    ) -> Result<Self, OperationLocatorError> {
99        let locator = Self {
100            format_version: OPERATION_LOCATOR_FORMAT_VERSION,
101            authority,
102            collection_id,
103            authority_identity,
104            operation_identity,
105            recovery_era,
106            authority_revision,
107            receipt_ordinal,
108            semantic_digest,
109            submitted_at,
110            expires_at,
111            resource_digest,
112        };
113        locator.validate()?;
114        Ok(locator)
115    }
116
117    #[must_use]
118    pub const fn authority(&self) -> OperationAuthorityKindV1 {
119        self.authority
120    }
121
122    #[must_use]
123    pub const fn collection_id(&self) -> CollectionId {
124        self.collection_id
125    }
126
127    #[must_use]
128    pub const fn authority_identity(&self) -> [u8; 32] {
129        self.authority_identity
130    }
131
132    #[must_use]
133    pub const fn operation_identity(&self) -> [u8; 32] {
134        self.operation_identity
135    }
136
137    #[must_use]
138    pub const fn recovery_era(&self) -> RecoveryEra {
139        self.recovery_era
140    }
141
142    #[must_use]
143    pub const fn authority_revision(&self) -> u64 {
144        self.authority_revision
145    }
146
147    #[must_use]
148    pub const fn receipt_ordinal(&self) -> u64 {
149        self.receipt_ordinal
150    }
151
152    #[must_use]
153    pub const fn semantic_digest(&self) -> [u8; 32] {
154        self.semantic_digest
155    }
156
157    #[must_use]
158    pub const fn submitted_at(&self) -> u64 {
159        self.submitted_at
160    }
161
162    #[must_use]
163    pub const fn expires_at(&self) -> u64 {
164        self.expires_at
165    }
166
167    #[must_use]
168    pub const fn resource_digest(&self) -> [u8; 32] {
169        self.resource_digest
170    }
171
172    #[must_use]
173    pub fn binding_digest(&self) -> [u8; 32] {
174        *blake3::hash(&self.encode()).as_bytes()
175    }
176
177    #[must_use]
178    pub fn encode(&self) -> Vec<u8> {
179        let mut bytes = Vec::with_capacity(OPERATION_LOCATOR_BYTES);
180        bytes.extend_from_slice(OPERATION_MAGIC);
181        bytes.extend_from_slice(&OPERATION_LOCATOR_FORMAT_VERSION.to_be_bytes());
182        bytes.push(self.authority.tag());
183        bytes.push(0);
184        bytes.extend_from_slice(&self.collection_id.to_be_bytes());
185        bytes.extend_from_slice(&self.authority_identity);
186        bytes.extend_from_slice(&self.operation_identity);
187        bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
188        bytes.extend_from_slice(&self.authority_revision.to_be_bytes());
189        bytes.extend_from_slice(&self.receipt_ordinal.to_be_bytes());
190        bytes.extend_from_slice(&self.semantic_digest);
191        bytes.extend_from_slice(&self.submitted_at.to_be_bytes());
192        bytes.extend_from_slice(&self.expires_at.to_be_bytes());
193        bytes.extend_from_slice(&self.resource_digest);
194        let checksum = crc32c(&bytes);
195        bytes.extend_from_slice(&checksum.to_be_bytes());
196        bytes
197    }
198
199    pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
200        require_fixed_envelope(
201            bytes,
202            OPERATION_MAGIC,
203            OPERATION_LOCATOR_BYTES,
204            OPERATION_LOCATOR_BODY_BYTES,
205        )?;
206        let format_version = read_u16(bytes, 8)?;
207        if format_version != OPERATION_LOCATOR_FORMAT_VERSION {
208            return Err(OperationLocatorError::UnsupportedOperationVersion(format_version));
209        }
210        if read_byte(bytes, 11)? != 0 {
211            return Err(OperationLocatorError::NonCanonicalReservedByte);
212        }
213        let locator = Self {
214            format_version,
215            authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
216            collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
217                .map_err(|_| OperationLocatorError::InvalidCollection)?,
218            authority_identity: read_array(bytes, 28)?,
219            operation_identity: read_array(bytes, 60)?,
220            recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 92)?)
221                .map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
222            authority_revision: read_u64(bytes, 108)?,
223            receipt_ordinal: read_u64(bytes, 116)?,
224            semantic_digest: read_array(bytes, 124)?,
225            submitted_at: read_u64(bytes, 156)?,
226            expires_at: read_u64(bytes, 164)?,
227            resource_digest: read_array(bytes, 172)?,
228        };
229        locator.validate()?;
230        Ok(locator)
231    }
232
233    pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
234        self.validate()
235    }
236
237    fn validate(&self) -> Result<(), OperationLocatorError> {
238        if self.format_version != OPERATION_LOCATOR_FORMAT_VERSION {
239            return Err(OperationLocatorError::UnsupportedOperationVersion(self.format_version));
240        }
241        if self.authority_identity == [0; 32] || self.operation_identity == [0; 32] {
242            return Err(OperationLocatorError::InvalidOperationIdentity);
243        }
244        if self.authority_revision == 0 || self.receipt_ordinal == 0 {
245            return Err(OperationLocatorError::InvalidAuthorityPosition);
246        }
247        if self.semantic_digest == [0; 32] || self.resource_digest == [0; 32] {
248            return Err(OperationLocatorError::InvalidDigest);
249        }
250        if self.expires_at <= self.submitted_at {
251            return Err(OperationLocatorError::InvalidExpiry);
252        }
253        Ok(())
254    }
255}
256
257/// Public artifact locator. It deliberately carries no backend or object key.
258#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
259#[serde(deny_unknown_fields)]
260pub struct ArtifactLocatorV1 {
261    format_version: u16,
262    authority: OperationAuthorityKindV1,
263    collection_id: CollectionId,
264    authority_identity: [u8; 32],
265    recovery_era: RecoveryEra,
266    operation_identity: [u8; 32],
267    operation_binding_digest: [u8; 32],
268    artifact_identity: [u8; 32],
269    bytes: u64,
270    sha256: [u8; 32],
271    expires_at: u64,
272}
273
274impl ArtifactLocatorV1 {
275    pub fn new(
276        operation: &OperationLocatorV1,
277        artifact_identity: [u8; 32],
278        bytes: u64,
279        sha256: [u8; 32],
280        expires_at: u64,
281    ) -> Result<Self, OperationLocatorError> {
282        operation.validate()?;
283        let locator = Self {
284            format_version: ARTIFACT_LOCATOR_FORMAT_VERSION,
285            authority: operation.authority,
286            collection_id: operation.collection_id,
287            authority_identity: operation.authority_identity,
288            recovery_era: operation.recovery_era,
289            operation_identity: operation.operation_identity,
290            operation_binding_digest: operation.binding_digest(),
291            artifact_identity,
292            bytes,
293            sha256,
294            expires_at,
295        };
296        locator.validate()?;
297        Ok(locator)
298    }
299
300    #[must_use]
301    pub const fn authority(&self) -> OperationAuthorityKindV1 {
302        self.authority
303    }
304
305    #[must_use]
306    pub const fn collection_id(&self) -> CollectionId {
307        self.collection_id
308    }
309
310    #[must_use]
311    pub const fn authority_identity(&self) -> [u8; 32] {
312        self.authority_identity
313    }
314
315    #[must_use]
316    pub const fn recovery_era(&self) -> RecoveryEra {
317        self.recovery_era
318    }
319
320    #[must_use]
321    pub const fn operation_identity(&self) -> [u8; 32] {
322        self.operation_identity
323    }
324
325    #[must_use]
326    pub const fn operation_binding_digest(&self) -> [u8; 32] {
327        self.operation_binding_digest
328    }
329
330    #[must_use]
331    pub const fn artifact_identity(&self) -> [u8; 32] {
332        self.artifact_identity
333    }
334
335    #[must_use]
336    pub const fn bytes(&self) -> u64 {
337        self.bytes
338    }
339
340    #[must_use]
341    pub const fn sha256(&self) -> [u8; 32] {
342        self.sha256
343    }
344
345    #[must_use]
346    pub const fn expires_at(&self) -> u64 {
347        self.expires_at
348    }
349
350    pub fn require_operation(&self, operation: &OperationLocatorV1) -> Result<(), OperationLocatorError> {
351        operation.validate()?;
352        if self.authority != operation.authority
353            || self.collection_id != operation.collection_id
354            || self.authority_identity != operation.authority_identity
355            || self.recovery_era != operation.recovery_era
356            || self.operation_identity != operation.operation_identity
357            || self.operation_binding_digest != operation.binding_digest()
358        {
359            return Err(OperationLocatorError::OperationBindingMismatch);
360        }
361        Ok(())
362    }
363
364    #[must_use]
365    pub fn encode(&self) -> Vec<u8> {
366        let mut bytes = Vec::with_capacity(ARTIFACT_LOCATOR_BYTES);
367        bytes.extend_from_slice(ARTIFACT_MAGIC);
368        bytes.extend_from_slice(&ARTIFACT_LOCATOR_FORMAT_VERSION.to_be_bytes());
369        bytes.push(self.authority.tag());
370        bytes.push(0);
371        bytes.extend_from_slice(&self.collection_id.to_be_bytes());
372        bytes.extend_from_slice(&self.authority_identity);
373        bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
374        bytes.extend_from_slice(&self.operation_identity);
375        bytes.extend_from_slice(&self.operation_binding_digest);
376        bytes.extend_from_slice(&self.artifact_identity);
377        bytes.extend_from_slice(&self.bytes.to_be_bytes());
378        bytes.extend_from_slice(&self.sha256);
379        bytes.extend_from_slice(&self.expires_at.to_be_bytes());
380        let checksum = crc32c(&bytes);
381        bytes.extend_from_slice(&checksum.to_be_bytes());
382        bytes
383    }
384
385    pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
386        require_fixed_envelope(
387            bytes,
388            ARTIFACT_MAGIC,
389            ARTIFACT_LOCATOR_BYTES,
390            ARTIFACT_LOCATOR_BODY_BYTES,
391        )?;
392        let format_version = read_u16(bytes, 8)?;
393        if format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
394            return Err(OperationLocatorError::UnsupportedArtifactVersion(format_version));
395        }
396        if read_byte(bytes, 11)? != 0 {
397            return Err(OperationLocatorError::NonCanonicalReservedByte);
398        }
399        let locator = Self {
400            format_version,
401            authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
402            collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
403                .map_err(|_| OperationLocatorError::InvalidCollection)?,
404            authority_identity: read_array(bytes, 28)?,
405            recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 60)?)
406                .map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
407            operation_identity: read_array(bytes, 76)?,
408            operation_binding_digest: read_array(bytes, 108)?,
409            artifact_identity: read_array(bytes, 140)?,
410            bytes: read_u64(bytes, 172)?,
411            sha256: read_array(bytes, 180)?,
412            expires_at: read_u64(bytes, 212)?,
413        };
414        locator.validate()?;
415        Ok(locator)
416    }
417
418    pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
419        self.validate()
420    }
421
422    fn validate(&self) -> Result<(), OperationLocatorError> {
423        if self.format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
424            return Err(OperationLocatorError::UnsupportedArtifactVersion(self.format_version));
425        }
426        if self.authority_identity == [0; 32]
427            || self.operation_identity == [0; 32]
428            || self.operation_binding_digest == [0; 32]
429            || self.artifact_identity == [0; 32]
430            || self.sha256 == [0; 32]
431        {
432            return Err(OperationLocatorError::InvalidDigest);
433        }
434        if self.bytes == 0 || self.expires_at == 0 {
435            return Err(OperationLocatorError::InvalidArtifactBounds);
436        }
437        Ok(())
438    }
439}
440
441#[derive(Clone, Copy, Debug, Eq, PartialEq)]
442pub struct ArtifactReadRangeV1 {
443    pub offset: u64,
444    pub length: u32,
445}
446
447impl ArtifactReadRangeV1 {
448    pub fn new(offset: u64, length: u32, total_bytes: u64) -> Result<Self, OperationLocatorError> {
449        if !offset.is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT) || !(1..=MAX_ARTIFACT_CHUNK_BYTES).contains(&length) {
450            return Err(OperationLocatorError::InvalidArtifactRange);
451        }
452        let end = offset
453            .checked_add(u64::from(length))
454            .ok_or(OperationLocatorError::ArtifactRangeOverflow)?;
455        if offset >= total_bytes
456            || end > total_bytes
457            || (end < total_bytes && !u64::from(length).is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT))
458        {
459            return Err(OperationLocatorError::InvalidArtifactRange);
460        }
461        Ok(Self { offset, length })
462    }
463
464    pub fn end(self) -> Result<u64, OperationLocatorError> {
465        self.offset
466            .checked_add(u64::from(self.length))
467            .ok_or(OperationLocatorError::ArtifactRangeOverflow)
468    }
469}
470
471#[derive(Clone, Debug, Eq, PartialEq)]
472pub struct VerifiedArtifactChunkV1 {
473    pub offset: u64,
474    pub bytes: Vec<u8>,
475    pub chunk_sha256: [u8; 32],
476    pub artifact_sha256: [u8; 32],
477    pub next_offset: Option<u64>,
478    pub complete: bool,
479}
480
481pub fn verify_artifact_chunk_v1(
482    artifact: &ArtifactLocatorV1,
483    range: ArtifactReadRangeV1,
484    bytes: Vec<u8>,
485    authority_artifact_sha256: [u8; 32],
486) -> Result<VerifiedArtifactChunkV1, OperationLocatorError> {
487    artifact.validate()?;
488    let canonical_range = ArtifactReadRangeV1::new(range.offset, range.length, artifact.bytes)?;
489    if bytes.len() != range.length as usize || authority_artifact_sha256 != artifact.sha256 {
490        return Err(OperationLocatorError::ArtifactDigestMismatch);
491    }
492    let chunk_sha256: [u8; 32] = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &bytes)
493        .as_ref()
494        .try_into()
495        .map_err(|_| OperationLocatorError::ArtifactDigestMismatch)?;
496    let end = canonical_range.end()?;
497    let complete = end == artifact.bytes;
498    if range.offset == 0 && complete && chunk_sha256 != artifact.sha256 {
499        return Err(OperationLocatorError::ArtifactDigestMismatch);
500    }
501    Ok(VerifiedArtifactChunkV1 {
502        offset: range.offset,
503        bytes,
504        chunk_sha256,
505        artifact_sha256: artifact.sha256,
506        next_offset: (!complete).then_some(end),
507        complete,
508    })
509}
510
511fn require_fixed_envelope(
512    bytes: &[u8],
513    magic: &[u8; 8],
514    expected_length: usize,
515    body_length: usize,
516) -> Result<(), OperationLocatorError> {
517    if bytes.len() != expected_length {
518        return Err(OperationLocatorError::InvalidLength {
519            expected: expected_length,
520            actual: bytes.len(),
521        });
522    }
523    if bytes.get(..8) != Some(magic) {
524        return Err(OperationLocatorError::BadMagic);
525    }
526    let expected = read_u32(bytes, body_length)?;
527    let actual = crc32c(bytes.get(..body_length).ok_or(OperationLocatorError::Truncated)?);
528    if expected != actual {
529        return Err(OperationLocatorError::Checksum { expected, actual });
530    }
531    Ok(())
532}
533
534fn read_byte(bytes: &[u8], offset: usize) -> Result<u8, OperationLocatorError> {
535    bytes.get(offset).copied().ok_or(OperationLocatorError::Truncated)
536}
537
538fn read_array<const N: usize>(bytes: &[u8], offset: usize) -> Result<[u8; N], OperationLocatorError> {
539    bytes
540        .get(offset..offset.checked_add(N).ok_or(OperationLocatorError::Truncated)?)
541        .ok_or(OperationLocatorError::Truncated)?
542        .try_into()
543        .map_err(|_| OperationLocatorError::Truncated)
544}
545
546fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, OperationLocatorError> {
547    Ok(u16::from_be_bytes(read_array(bytes, offset)?))
548}
549
550fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, OperationLocatorError> {
551    Ok(u32::from_be_bytes(read_array(bytes, offset)?))
552}
553
554fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, OperationLocatorError> {
555    Ok(u64::from_be_bytes(read_array(bytes, offset)?))
556}
557
558#[derive(Clone, Debug, Eq, PartialEq)]
559pub enum OperationLocatorError {
560    InvalidLength { expected: usize, actual: usize },
561    BadMagic,
562    Truncated,
563    Checksum { expected: u32, actual: u32 },
564    UnsupportedOperationVersion(u16),
565    UnsupportedArtifactVersion(u16),
566    UnsupportedAuthority(u8),
567    NonCanonicalReservedByte,
568    InvalidCollection,
569    InvalidRecoveryEra,
570    InvalidOperationIdentity,
571    InvalidAuthorityPosition,
572    InvalidDigest,
573    InvalidExpiry,
574    InvalidArtifactBounds,
575    OperationBindingMismatch,
576    InvalidArtifactRange,
577    ArtifactRangeOverflow,
578    ArtifactDigestMismatch,
579}
580
581impl Display for OperationLocatorError {
582    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
583        match self {
584            Self::InvalidLength { expected, actual } => {
585                write!(
586                    formatter,
587                    "locator length {actual} does not match current length {expected}"
588                )
589            }
590            Self::BadMagic => formatter.write_str("locator magic is not current"),
591            Self::Truncated => formatter.write_str("locator is truncated"),
592            Self::Checksum { expected, actual } => {
593                write!(formatter, "locator checksum {actual} does not match {expected}")
594            }
595            Self::UnsupportedOperationVersion(version) => {
596                write!(formatter, "operation locator version {version} is unsupported")
597            }
598            Self::UnsupportedArtifactVersion(version) => {
599                write!(formatter, "artifact locator version {version} is unsupported")
600            }
601            Self::UnsupportedAuthority(authority) => {
602                write!(formatter, "operation authority tag {authority} is unsupported")
603            }
604            Self::NonCanonicalReservedByte => formatter.write_str("locator reserved byte is nonzero"),
605            Self::InvalidCollection => formatter.write_str("locator collection identity is invalid"),
606            Self::InvalidRecoveryEra => formatter.write_str("locator recovery era is invalid"),
607            Self::InvalidOperationIdentity => formatter.write_str("operation identity must be nonzero"),
608            Self::InvalidAuthorityPosition => {
609                formatter.write_str("authority revision and receipt ordinal must be nonzero")
610            }
611            Self::InvalidDigest => formatter.write_str("locator digest must be nonzero"),
612            Self::InvalidExpiry => formatter.write_str("operation expiry must follow submission"),
613            Self::InvalidArtifactBounds => formatter.write_str("artifact size or expiry is invalid"),
614            Self::OperationBindingMismatch => formatter.write_str("artifact is not bound to the supplied operation"),
615            Self::InvalidArtifactRange => {
616                formatter.write_str("artifact range is unaligned, empty, oversized, or outside the artifact")
617            }
618            Self::ArtifactRangeOverflow => formatter.write_str("artifact range overflows u64"),
619            Self::ArtifactDigestMismatch => {
620                formatter.write_str("artifact chunk or full digest does not match authoritative evidence")
621            }
622        }
623    }
624}
625
626impl Error for OperationLocatorError {}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    fn operation() -> Result<OperationLocatorV1, Box<dyn Error>> {
633        Ok(OperationLocatorV1::new(
634            OperationAuthorityKindV1::Diagnostic,
635            CollectionId::new(1)?,
636            [2; 32],
637            [3; 32],
638            RecoveryEra::new(4)?,
639            5,
640            6,
641            [7; 32],
642            8,
643            70_000,
644            [9; 32],
645        )?)
646    }
647
648    #[test]
649    fn operation_and_artifact_locators_round_trip_exactly() -> Result<(), Box<dyn Error>> {
650        let operation = operation()?;
651        assert_eq!(OperationLocatorV1::decode(&operation.encode())?, operation);
652        let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_192, [11; 32], 60_000)?;
653        assert_eq!(ArtifactLocatorV1::decode(&artifact.encode())?, artifact);
654        artifact.require_operation(&operation)?;
655        Ok(())
656    }
657
658    #[test]
659    fn malformed_locators_and_ranges_fail_closed() -> Result<(), Box<dyn Error>> {
660        let operation = operation()?;
661        let mut bytes = operation.encode();
662        bytes[40] ^= 1;
663        assert!(OperationLocatorV1::decode(&bytes).is_err());
664        let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_193, [11; 32], 60_000)?;
665        assert!(ArtifactReadRangeV1::new(1, 4_096, artifact.bytes()).is_err());
666        assert!(ArtifactReadRangeV1::new(0, 4_095, artifact.bytes()).is_err());
667        let final_range = ArtifactReadRangeV1::new(8_192, 1, artifact.bytes())?;
668        assert_eq!(final_range.end()?, 8_193);
669        Ok(())
670    }
671}