Skip to main content

canwu_sim/runtime/
random.rs

1use super::{CanwuError, ErrorCode};
2use canwu_core::{
3    ArmyId, BoundaryId, DeterministicRng, DomainRecordRef, EntityRef, EventId, EvidenceRef,
4    KnowledgeHolderRef, PersonId, RandomDrawId,
5};
6use canwu_event::CauseRef;
7use canwu_time::SimTime;
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11const STREAM_DERIVATION_DOMAIN: &[u8] = b"canwu.random-stream.v1";
12const OPERATION_DOMAIN: &[u8] = b"canwu.random.operation.v1";
13const PURPOSE_DOMAIN: &[u8] = b"canwu.random.purpose.v1";
14const OPERATION_TEXT_BYTES: usize = 256;
15
16#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum RandomAlgorithm {
19    #[default]
20    SplitMix64V1,
21}
22
23#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
24pub struct RandomStreamKey {
25    pub namespace: String,
26    pub name: String,
27    pub version: u32,
28}
29
30impl RandomStreamKey {
31    #[must_use]
32    pub fn new(namespace: impl Into<String>, name: impl Into<String>, version: u32) -> Self {
33        Self {
34            namespace: namespace.into(),
35            name: name.into(),
36            version,
37        }
38    }
39}
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct RandomStreamState {
43    pub key: RandomStreamKey,
44    pub algorithm: RandomAlgorithm,
45    pub seed: u64,
46    pub position: u64,
47    pub generator_state: u64,
48}
49
50impl RandomStreamState {
51    pub(crate) fn initial(root_seed: u64, key: RandomStreamKey) -> Self {
52        let seed = derive_stream_seed(root_seed, &key);
53        Self {
54            key,
55            algorithm: RandomAlgorithm::SplitMix64V1,
56            seed,
57            position: 0,
58            generator_state: seed,
59        }
60    }
61
62    pub(crate) fn is_coherent(&self, root_seed: u64) -> bool {
63        self.algorithm == RandomAlgorithm::SplitMix64V1
64            && self.seed == derive_stream_seed(root_seed, &self.key)
65            && self.generator_state == DeterministicRng::state_after(self.seed, self.position)
66    }
67}
68
69#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum RandomDrawProducer {
72    BoundarySystem {
73        boundary: BoundaryId,
74        plugin: String,
75        system: String,
76    },
77    CoreSystem {
78        system: String,
79    },
80}
81
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83#[serde(tag = "type", rename_all = "snake_case")]
84pub enum RandomDrawOutcome {
85    BoundarySystemDecision,
86    KnowledgeReportDelivery {
87        recipient: PersonId,
88        army: ArmyId,
89        dispatch_event: EventId,
90        arrives_at: SimTime,
91    },
92}
93
94/// Stable application target for an operation-addressed random draw.
95///
96/// Snapshot-format-5 target for the enabled byte-exact keyed algorithm.
97#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
98#[serde(tag = "type", content = "value", rename_all = "snake_case")]
99pub enum RandomOperationTarget {
100    Entity(EntityRef),
101    DomainRecord {
102        record: DomainRecordRef,
103        version: u64,
104    },
105    KnowledgeHolder(KnowledgeHolderRef),
106    CanonicalKey(String),
107}
108
109/// Version-one stable entropy address for a future keyed random draw.
110#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
111pub struct RandomOperationAddressV1 {
112    pub producer_plugin: String,
113    pub operation_kind: String,
114    pub application_operation_id: String,
115    pub target: RandomOperationTarget,
116    pub draw_slot: u32,
117}
118
119/// Persisted address of a random draw.
120#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
121#[serde(tag = "type", content = "value", rename_all = "snake_case")]
122pub enum RandomDrawAddress {
123    Sequential { position: u64 },
124    OperationV1(RandomOperationAddressV1),
125}
126
127impl RandomDrawAddress {
128    #[must_use]
129    pub const fn sequential_position(&self) -> Option<u64> {
130        match self {
131            Self::Sequential { position } => Some(*position),
132            Self::OperationV1(_) => None,
133        }
134    }
135}
136#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
137pub struct RandomDrawRecord {
138    pub id: RandomDrawId,
139    pub at: SimTime,
140    pub stream: RandomStreamKey,
141    pub address: RandomDrawAddress,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub operation_evidence: Option<EvidenceRef>,
144    pub upper_exclusive: u64,
145    pub value: u64,
146    pub purpose: String,
147    pub producer: RandomDrawProducer,
148    #[serde(default)]
149    pub outcome: Option<RandomDrawOutcome>,
150    pub cause: CauseRef,
151    pub correlation_id: u64,
152}
153
154#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
155pub struct KeyedDrawReservation {
156    pub stream: RandomStreamKey,
157    pub address: RandomOperationAddressV1,
158    pub upper_exclusive: u64,
159    pub purpose_hash: String,
160    pub result: u64,
161    pub draw_id: RandomDrawId,
162    pub operation_evidence: EvidenceRef,
163    pub draw_receipt: crate::ArchivedEvidenceReceipt,
164}
165
166#[derive(Clone, Debug)]
167pub(crate) struct PendingRandomDraw {
168    pub stream: RandomStreamKey,
169    pub address: RandomDrawAddress,
170    pub operation_evidence: Option<EvidenceRef>,
171    pub upper_exclusive: u64,
172    pub value: u64,
173    pub purpose: String,
174}
175
176#[derive(Clone, Debug, Eq, PartialEq)]
177pub(crate) struct KeyedDrawMemo {
178    pub operation_evidence: EvidenceRef,
179    pub upper_exclusive: u64,
180    pub value: u64,
181    pub purpose_hash: String,
182}
183
184pub(crate) type KeyedDrawIndex =
185    BTreeMap<(RandomStreamKey, RandomOperationAddressV1), KeyedDrawMemo>;
186
187pub(crate) struct RandomExecution {
188    pub states: BTreeMap<RandomStreamKey, RandomStreamState>,
189    pub draws: Vec<PendingRandomDraw>,
190}
191
192pub(crate) struct RandomSession {
193    states: BTreeMap<RandomStreamKey, RandomStreamState>,
194    draws: Vec<PendingRandomDraw>,
195    root_seed: u64,
196    producer_plugin: String,
197    keyed: KeyedDrawIndex,
198}
199
200impl RandomSession {
201    pub(crate) fn new(
202        available: &BTreeMap<RandomStreamKey, RandomStreamState>,
203        allowed: &[RandomStreamKey],
204        root_seed: u64,
205        producer_plugin: &str,
206        keyed: &KeyedDrawIndex,
207    ) -> Result<Self, CanwuError> {
208        let mut states = BTreeMap::new();
209        for key in allowed {
210            let Some(state) = available.get(key) else {
211                return Err(CanwuError::new(
212                    ErrorCode::InvalidRandomStream,
213                    format!(
214                        "declared random stream {}.{}@{} is not initialized",
215                        key.namespace, key.name, key.version
216                    ),
217                ));
218            };
219            states.insert(key.clone(), state.clone());
220        }
221        Ok(Self {
222            states,
223            draws: Vec::new(),
224            root_seed,
225            producer_plugin: producer_plugin.to_owned(),
226            keyed: keyed.clone(),
227        })
228    }
229
230    pub(crate) fn range(
231        &mut self,
232        key: &RandomStreamKey,
233        upper_exclusive: u64,
234        purpose: &str,
235    ) -> Result<u64, CanwuError> {
236        if upper_exclusive == 0 || purpose.trim().is_empty() || purpose != purpose.trim() {
237            return Err(CanwuError::new(
238                ErrorCode::InvalidRandomDraw,
239                "random draws require a positive bound and canonical purpose",
240            ));
241        }
242        let Some(state) = self.states.get_mut(key) else {
243            return Err(CanwuError::new(
244                ErrorCode::UndeclaredRandomStream,
245                format!(
246                    "random stream {}.{}@{} was not declared by this system",
247                    key.namespace, key.name, key.version
248                ),
249            ));
250        };
251        let next_position = state.position.checked_add(1).ok_or_else(|| {
252            CanwuError::new(
253                ErrorCode::IdentifierExhausted,
254                "random stream position is exhausted",
255            )
256        })?;
257        let position = state.position;
258        let mut generator = DeterministicRng::from_seed(state.generator_state);
259        let value = generator.range(upper_exclusive);
260        state.position = next_position;
261        state.generator_state = generator.state();
262        self.draws.push(PendingRandomDraw {
263            stream: key.clone(),
264            address: RandomDrawAddress::Sequential { position },
265            operation_evidence: None,
266            upper_exclusive,
267            value,
268            purpose: purpose.to_owned(),
269        });
270        Ok(value)
271    }
272
273    #[allow(clippy::too_many_arguments)]
274    pub(crate) fn range_for_operation(
275        &mut self,
276        key: &RandomStreamKey,
277        evidence: EvidenceRef,
278        operation_kind: &str,
279        application_operation_id: &str,
280        target: RandomOperationTarget,
281        draw_slot: u32,
282        upper_exclusive: u64,
283        purpose: &str,
284    ) -> Result<u64, CanwuError> {
285        if !self.states.contains_key(key) {
286            return Err(CanwuError::new(
287                ErrorCode::UndeclaredRandomStream,
288                format!(
289                    "random stream {}.{}@{} was not declared by this system",
290                    key.namespace, key.name, key.version
291                ),
292            ));
293        }
294        let address = RandomOperationAddressV1 {
295            producer_plugin: self.producer_plugin.clone(),
296            operation_kind: operation_kind.to_owned(),
297            application_operation_id: application_operation_id.to_owned(),
298            target,
299            draw_slot,
300        };
301        validate_operation_inputs(key, &address, upper_exclusive, purpose)?;
302        let index_key = (key.clone(), address.clone());
303        let purpose_hash = purpose_hash_hex_v1(purpose)?;
304        if let Some(existing) = self.keyed.get(&index_key) {
305            if existing.operation_evidence == evidence
306                && existing.upper_exclusive == upper_exclusive
307                && existing.purpose_hash == purpose_hash
308            {
309                return Ok(existing.value);
310            }
311            return Err(CanwuError::new(
312                ErrorCode::RandomOperationConflict,
313                "operation-keyed random address was reused with different evidence, bound, or purpose",
314            ));
315        }
316        let value = operation_value_v1(self.root_seed, key, &address, upper_exclusive, purpose)?;
317        let memo = KeyedDrawMemo {
318            operation_evidence: evidence.clone(),
319            upper_exclusive,
320            value,
321            purpose_hash,
322        };
323        self.keyed.insert(index_key, memo);
324        self.draws.push(PendingRandomDraw {
325            stream: key.clone(),
326            address: RandomDrawAddress::OperationV1(address),
327            operation_evidence: Some(evidence),
328            upper_exclusive,
329            value,
330            purpose: purpose.to_owned(),
331        });
332        Ok(value)
333    }
334
335    pub(crate) fn finish(self) -> RandomExecution {
336        RandomExecution {
337            states: self.states,
338            draws: self.draws,
339        }
340    }
341}
342
343pub(crate) fn retained_keyed_draws(
344    draws: &[RandomDrawRecord],
345) -> Result<KeyedDrawIndex, CanwuError> {
346    let mut index = BTreeMap::new();
347    for draw in draws {
348        let RandomDrawAddress::OperationV1(address) = &draw.address else {
349            continue;
350        };
351        let evidence = draw.operation_evidence.clone().ok_or_else(|| {
352            CanwuError::new(
353                ErrorCode::InvalidRandomDraw,
354                "operation-keyed draw is missing its evidence reference",
355            )
356        })?;
357        validate_operation_inputs(&draw.stream, address, draw.upper_exclusive, &draw.purpose)?;
358        let key = (draw.stream.clone(), address.clone());
359        let memo = KeyedDrawMemo {
360            operation_evidence: evidence,
361            upper_exclusive: draw.upper_exclusive,
362            value: draw.value,
363            purpose_hash: purpose_hash_hex_v1(&draw.purpose)?,
364        };
365        if index.insert(key, memo).is_some() {
366            return Err(CanwuError::new(
367                ErrorCode::RandomOperationConflict,
368                "random journal contains a duplicate operation-keyed address",
369            ));
370        }
371    }
372    Ok(index)
373}
374
375pub(crate) fn keyed_draws_with_reservations(
376    draws: &[RandomDrawRecord],
377    reservations: &[KeyedDrawReservation],
378) -> Result<KeyedDrawIndex, CanwuError> {
379    let mut index = retained_keyed_draws(draws)?;
380    for reservation in reservations {
381        validate_operation_address(
382            &reservation.stream,
383            &reservation.address,
384            reservation.upper_exclusive,
385        )?;
386        if reservation.purpose_hash.len() != 64
387            || reservation
388                .purpose_hash
389                .bytes()
390                .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
391            || reservation.draw_receipt.evidence != EvidenceRef::RandomDraw(reservation.draw_id)
392        {
393            return Err(CanwuError::new(
394                ErrorCode::InvalidRandomDraw,
395                "keyed draw reservation has an invalid purpose hash or draw receipt",
396            ));
397        }
398        let key = (reservation.stream.clone(), reservation.address.clone());
399        let memo = KeyedDrawMemo {
400            operation_evidence: reservation.operation_evidence.clone(),
401            upper_exclusive: reservation.upper_exclusive,
402            value: reservation.result,
403            purpose_hash: reservation.purpose_hash.clone(),
404        };
405        if index.insert(key, memo).is_some() {
406            return Err(CanwuError::new(
407                ErrorCode::RandomOperationConflict,
408                "keyed draw reservation overlaps retained or reserved evidence",
409            ));
410        }
411    }
412    Ok(index)
413}
414
415pub(crate) fn extend_keyed_draws(
416    index: &mut KeyedDrawIndex,
417    draws: &[PendingRandomDraw],
418) -> Result<(), CanwuError> {
419    for draw in draws {
420        let RandomDrawAddress::OperationV1(address) = &draw.address else {
421            continue;
422        };
423        let evidence = draw.operation_evidence.clone().ok_or_else(|| {
424            CanwuError::new(
425                ErrorCode::InvalidRandomDraw,
426                "pending operation-keyed draw is missing evidence",
427            )
428        })?;
429        let memo = KeyedDrawMemo {
430            operation_evidence: evidence,
431            upper_exclusive: draw.upper_exclusive,
432            value: draw.value,
433            purpose_hash: purpose_hash_hex_v1(&draw.purpose)?,
434        };
435        if index
436            .insert((draw.stream.clone(), address.clone()), memo)
437            .is_some()
438        {
439            return Err(CanwuError::new(
440                ErrorCode::RandomOperationConflict,
441                "pending random execution duplicated an operation-keyed address",
442            ));
443        }
444    }
445    Ok(())
446}
447
448pub(crate) fn validate_operation_draw(
449    root_seed: u64,
450    draw: &RandomDrawRecord,
451) -> Result<(), CanwuError> {
452    match (&draw.address, &draw.operation_evidence) {
453        (RandomDrawAddress::Sequential { .. }, None) => Ok(()),
454        (RandomDrawAddress::Sequential { .. }, Some(_))
455        | (RandomDrawAddress::OperationV1(_), None) => Err(CanwuError::new(
456            ErrorCode::InvalidRandomDraw,
457            "random address and operation evidence are inconsistent",
458        )),
459        (RandomDrawAddress::OperationV1(address), Some(_)) => {
460            let expected = operation_value_v1(
461                root_seed,
462                &draw.stream,
463                address,
464                draw.upper_exclusive,
465                &draw.purpose,
466            )?;
467            if expected != draw.value {
468                return Err(CanwuError::new(
469                    ErrorCode::InvalidRandomDraw,
470                    "operation-keyed random value does not match its exact V1 address",
471                ));
472            }
473            Ok(())
474        }
475    }
476}
477
478fn operation_value_v1(
479    root_seed: u64,
480    key: &RandomStreamKey,
481    address: &RandomOperationAddressV1,
482    upper_exclusive: u64,
483    purpose: &str,
484) -> Result<u64, CanwuError> {
485    validate_operation_inputs(key, address, upper_exclusive, purpose)?;
486    let purpose_hash = purpose_hash_v1(purpose)?;
487    for candidate_index in 0..=u32::MAX {
488        let bytes = operation_input_v1(
489            root_seed,
490            key,
491            address,
492            upper_exclusive,
493            &purpose_hash,
494            candidate_index,
495        )?;
496        let digest = blake3::hash(&bytes);
497        let mut candidate_bytes = [0_u8; 8];
498        candidate_bytes.copy_from_slice(&digest.as_bytes()[..8]);
499        let candidate = u64::from_le_bytes(candidate_bytes);
500        let range = 1_u128 << 64;
501        let bound = u128::from(upper_exclusive);
502        let accept_limit = (range / bound) * bound;
503        if u128::from(candidate) < accept_limit {
504            return Ok(candidate % upper_exclusive);
505        }
506    }
507    Err(CanwuError::new(
508        ErrorCode::IdentifierExhausted,
509        "operation-keyed random candidate space is exhausted",
510    ))
511}
512
513fn validate_operation_inputs(
514    key: &RandomStreamKey,
515    address: &RandomOperationAddressV1,
516    upper_exclusive: u64,
517    purpose: &str,
518) -> Result<(), CanwuError> {
519    validate_operation_address(key, address, upper_exclusive)?;
520    validate_operation_text(purpose)
521}
522
523fn validate_operation_address(
524    key: &RandomStreamKey,
525    address: &RandomOperationAddressV1,
526    upper_exclusive: u64,
527) -> Result<(), CanwuError> {
528    if upper_exclusive == 0 || key.version == 0 {
529        return Err(CanwuError::new(
530            ErrorCode::InvalidRandomDraw,
531            "operation-keyed draws require positive stream version and bound",
532        ));
533    }
534    for value in [
535        key.namespace.as_str(),
536        key.name.as_str(),
537        address.producer_plugin.as_str(),
538        address.operation_kind.as_str(),
539        address.application_operation_id.as_str(),
540    ] {
541        validate_operation_text(value)?;
542    }
543    validate_target(&address.target)
544}
545
546fn validate_target(target: &RandomOperationTarget) -> Result<(), CanwuError> {
547    match target {
548        RandomOperationTarget::Entity(EntityRef::Domain(reference)) => {
549            validate_operation_text(&reference.kind.namespace)?;
550            validate_operation_text(&reference.kind.name)?;
551            validate_operation_text(&reference.id)?;
552        }
553        RandomOperationTarget::DomainRecord { record, version } => {
554            if *version == 0 {
555                return Err(CanwuError::new(
556                    ErrorCode::InvalidRandomDraw,
557                    "operation-keyed domain record target requires a positive version",
558                ));
559            }
560            validate_operation_text(&record.kind.namespace)?;
561            validate_operation_text(&record.kind.name)?;
562            validate_operation_text(&record.id)?;
563        }
564        RandomOperationTarget::CanonicalKey(value) => validate_operation_text(value)?,
565        RandomOperationTarget::Entity(_) | RandomOperationTarget::KnowledgeHolder(_) => {}
566    }
567    if let RandomOperationTarget::KnowledgeHolder(KnowledgeHolderRef::Entity(EntityRef::Domain(
568        reference,
569    ))) = target
570    {
571        validate_operation_text(&reference.kind.namespace)?;
572        validate_operation_text(&reference.kind.name)?;
573        validate_operation_text(&reference.id)?;
574    }
575    Ok(())
576}
577
578fn validate_operation_text(value: &str) -> Result<(), CanwuError> {
579    if value.is_empty()
580        || value != value.trim()
581        || value.len() > OPERATION_TEXT_BYTES
582        || u32::try_from(value.len()).is_err()
583    {
584        return Err(CanwuError::new(
585            ErrorCode::InvalidRandomDraw,
586            "operation-keyed random text is empty, non-canonical, or too long",
587        ));
588    }
589    Ok(())
590}
591
592fn purpose_hash_v1(purpose: &str) -> Result<[u8; 32], CanwuError> {
593    validate_operation_text(purpose)?;
594    let mut bytes = Vec::new();
595    bytes.extend_from_slice(PURPOSE_DOMAIN);
596    bytes.push(0);
597    put_text(&mut bytes, purpose)?;
598    Ok(*blake3::hash(&bytes).as_bytes())
599}
600
601pub(crate) fn purpose_hash_hex_v1(purpose: &str) -> Result<String, CanwuError> {
602    Ok(blake3::Hash::from_bytes(purpose_hash_v1(purpose)?)
603        .to_hex()
604        .to_string())
605}
606
607fn operation_input_v1(
608    root_seed: u64,
609    key: &RandomStreamKey,
610    address: &RandomOperationAddressV1,
611    upper_exclusive: u64,
612    purpose_hash: &[u8; 32],
613    candidate_index: u32,
614) -> Result<Vec<u8>, CanwuError> {
615    let mut bytes = Vec::new();
616    bytes.extend_from_slice(OPERATION_DOMAIN);
617    bytes.push(0);
618    bytes.push(1);
619    bytes.extend_from_slice(&root_seed.to_le_bytes());
620    put_text(&mut bytes, &key.namespace)?;
621    put_text(&mut bytes, &key.name)?;
622    bytes.extend_from_slice(&key.version.to_le_bytes());
623    put_text(&mut bytes, &address.producer_plugin)?;
624    put_text(&mut bytes, &address.operation_kind)?;
625    put_text(&mut bytes, &address.application_operation_id)?;
626    encode_target(&mut bytes, &address.target)?;
627    bytes.extend_from_slice(&address.draw_slot.to_le_bytes());
628    bytes.extend_from_slice(&upper_exclusive.to_le_bytes());
629    bytes.extend_from_slice(purpose_hash);
630    bytes.extend_from_slice(&candidate_index.to_le_bytes());
631    Ok(bytes)
632}
633
634fn put_text(bytes: &mut Vec<u8>, value: &str) -> Result<(), CanwuError> {
635    validate_operation_text(value)?;
636    let length = u32::try_from(value.len()).map_err(|_| {
637        CanwuError::new(
638            ErrorCode::InvalidRandomDraw,
639            "operation-keyed text length exceeds u32",
640        )
641    })?;
642    bytes.extend_from_slice(&length.to_le_bytes());
643    bytes.extend_from_slice(value.as_bytes());
644    Ok(())
645}
646
647fn encode_target(bytes: &mut Vec<u8>, target: &RandomOperationTarget) -> Result<(), CanwuError> {
648    match target {
649        RandomOperationTarget::Entity(entity) => {
650            bytes.push(1);
651            encode_entity(bytes, entity)?;
652        }
653        RandomOperationTarget::DomainRecord { record, version } => {
654            if *version == 0 {
655                return Err(CanwuError::new(
656                    ErrorCode::InvalidRandomDraw,
657                    "operation-keyed domain record target requires a positive version",
658                ));
659            }
660            bytes.push(2);
661            put_text(bytes, &record.kind.namespace)?;
662            put_text(bytes, &record.kind.name)?;
663            put_text(bytes, &record.id)?;
664            bytes.extend_from_slice(&version.to_le_bytes());
665        }
666        RandomOperationTarget::KnowledgeHolder(holder) => {
667            bytes.push(3);
668            match holder {
669                KnowledgeHolderRef::Person(person) => {
670                    bytes.push(1);
671                    bytes.extend_from_slice(&person.get().to_le_bytes());
672                }
673                KnowledgeHolderRef::Entity(entity) => {
674                    bytes.push(2);
675                    encode_entity(bytes, entity)?;
676                }
677            }
678        }
679        RandomOperationTarget::CanonicalKey(value) => {
680            bytes.push(4);
681            put_text(bytes, value)?;
682        }
683    }
684    Ok(())
685}
686
687fn encode_entity(bytes: &mut Vec<u8>, entity: &EntityRef) -> Result<(), CanwuError> {
688    match entity {
689        EntityRef::Army(id) => {
690            bytes.push(1);
691            bytes.extend_from_slice(&id.get().to_le_bytes());
692        }
693        EntityRef::Government(id) => {
694            bytes.push(2);
695            bytes.extend_from_slice(&id.get().to_le_bytes());
696        }
697        EntityRef::Organization(id) => {
698            bytes.push(3);
699            bytes.extend_from_slice(&id.get().to_le_bytes());
700        }
701        EntityRef::Person(id) => {
702            bytes.push(4);
703            bytes.extend_from_slice(&id.get().to_le_bytes());
704        }
705        EntityRef::Resource(id) => {
706            bytes.push(5);
707            bytes.extend_from_slice(&id.get().to_le_bytes());
708        }
709        EntityRef::Route(id) => {
710            bytes.push(6);
711            bytes.extend_from_slice(&id.get().to_le_bytes());
712        }
713        EntityRef::Territory(id) => {
714            bytes.push(7);
715            bytes.extend_from_slice(&id.get().to_le_bytes());
716        }
717        EntityRef::Domain(reference) => {
718            bytes.push(8);
719            put_text(bytes, &reference.kind.namespace)?;
720            put_text(bytes, &reference.kind.name)?;
721            put_text(bytes, &reference.id)?;
722        }
723    }
724    Ok(())
725}
726
727pub(crate) fn derive_stream_seed(root_seed: u64, key: &RandomStreamKey) -> u64 {
728    if key.namespace == "canwu.core" && key.name == "knowledge-report-delay" && key.version == 1 {
729        return root_seed;
730    }
731    let mut hasher = blake3::Hasher::new();
732    hasher.update(STREAM_DERIVATION_DOMAIN);
733    hasher.update(&root_seed.to_le_bytes());
734    update_text(&mut hasher, &key.namespace);
735    update_text(&mut hasher, &key.name);
736    hasher.update(&key.version.to_le_bytes());
737    let mut seed = [0_u8; 8];
738    seed.copy_from_slice(&hasher.finalize().as_bytes()[..8]);
739    u64::from_le_bytes(seed)
740}
741
742pub(crate) fn core_report_delay_stream() -> RandomStreamKey {
743    RandomStreamKey::new("canwu.core", "knowledge-report-delay", 1)
744}
745
746fn update_text(hasher: &mut blake3::Hasher, value: &str) {
747    let length = u64::try_from(value.len()).unwrap_or(u64::MAX);
748    hasher.update(&length.to_le_bytes());
749    hasher.update(value.as_bytes());
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755    use canwu_core::{GovernmentId, KnowledgeHolderRef, PersonId};
756
757    fn fixture_stream() -> RandomStreamKey {
758        RandomStreamKey::new("fixture.random", "resolution", 1)
759    }
760
761    fn fixture_address(target: RandomOperationTarget) -> RandomOperationAddressV1 {
762        RandomOperationAddressV1 {
763            producer_plugin: "fixture-random".to_owned(),
764            operation_kind: "resolve".to_owned(),
765            application_operation_id: "operation-alpha".to_owned(),
766            target,
767            draw_slot: 3,
768        }
769    }
770
771    #[test]
772    fn operation_v1_golden_vectors_cover_every_target_encoding() {
773        let stream = fixture_stream();
774        let targets = [
775            RandomOperationTarget::Entity(EntityRef::Government(GovernmentId::new(9))),
776            RandomOperationTarget::DomainRecord {
777                record: DomainRecordRef {
778                    kind: canwu_core::DomainRecordKind::new("fixture", "record"),
779                    id: "r-7".to_owned(),
780                },
781                version: 4,
782            },
783            RandomOperationTarget::KnowledgeHolder(KnowledgeHolderRef::Person(PersonId::new(5))),
784            RandomOperationTarget::CanonicalKey("键-α".to_owned()),
785        ];
786        let values = targets
787            .into_iter()
788            .map(|target| {
789                let address = fixture_address(target);
790                let purpose_hash = purpose_hash_v1("stable outcome").expect("purpose should hash");
791                let input = operation_input_v1(
792                    0x0102_0304_0506_0708,
793                    &stream,
794                    &address,
795                    10_000,
796                    &purpose_hash,
797                    0,
798                )
799                .expect("golden input should encode");
800                let digest = blake3::hash(&input).to_hex().to_string();
801                let value = operation_value_v1(
802                    0x0102_0304_0506_0708,
803                    &stream,
804                    &address,
805                    10_000,
806                    "stable outcome",
807                )
808                .expect("golden vector should encode");
809                (input, digest, value)
810            })
811            .collect::<Vec<_>>();
812        assert_eq!(
813            values
814                .iter()
815                .map(|(_, digest, _)| digest.as_str())
816                .collect::<Vec<_>>(),
817            vec![
818                "55b21978711c8d81a42bdacef84c8b22e16bf5e2a36135097f850e658ed86a74",
819                "4ac40330c89fc0bce339b16c59fa25166f714b664f4fd3e9e252cb9960b979d7",
820                "c23814e1a5343aba6e00b88c1267aef583d6adf2385173b80268f14c6d34f594",
821                "8f84ed6f5700db025dff8e59966c8f14d9c2a262904726688ea08d9fd043816d",
822            ]
823        );
824        assert_eq!(
825            values
826                .iter()
827                .map(|(_, _, value)| *value)
828                .collect::<Vec<_>>(),
829            vec![8389, 6730, 1186, 1231]
830        );
831        assert_eq!(
832            values[0].0.iter().fold(String::new(), |mut output, byte| {
833                use std::fmt::Write as _;
834                write!(&mut output, "{byte:02x}").expect("writing to a string cannot fail");
835                output
836            }),
837            "63616e77752e72616e646f6d2e6f7065726174696f6e2e7631000108070605040302010e000000666978747572652e72616e646f6d0a0000007265736f6c7574696f6e010000000e000000666978747572652d72616e646f6d070000007265736f6c76650f0000006f7065726174696f6e2d616c70686101020900000000000000030000001027000000000000784ea13c085b19a2d0420898b8c3848517c24f30b1d82658617e80f99edff43a00000000"
838        );
839    }
840
841    #[test]
842    fn keyed_retry_is_idempotent_conflicts_fail_and_sequential_state_does_not_move() {
843        let stream = fixture_stream();
844        let state = RandomStreamState::initial(41, stream.clone());
845        let mut session = RandomSession::new(
846            &BTreeMap::from([(stream.clone(), state)]),
847            std::slice::from_ref(&stream),
848            41,
849            "fixture-random",
850            &BTreeMap::new(),
851        )
852        .expect("session should initialize");
853        let evidence = EvidenceRef::Ingress(canwu_core::IngressId::new(2));
854        let first = session
855            .range_for_operation(
856                &stream,
857                evidence.clone(),
858                "resolve",
859                "operation-alpha",
860                RandomOperationTarget::CanonicalKey("target".to_owned()),
861                0,
862                100,
863                "resolution",
864            )
865            .expect("first keyed draw should succeed");
866        let retry = session
867            .range_for_operation(
868                &stream,
869                evidence.clone(),
870                "resolve",
871                "operation-alpha",
872                RandomOperationTarget::CanonicalKey("target".to_owned()),
873                0,
874                100,
875                "resolution",
876            )
877            .expect("exact retry should reuse the result");
878        assert_eq!(retry, first);
879        let conflict = session
880            .range_for_operation(
881                &stream,
882                evidence,
883                "resolve",
884                "operation-alpha",
885                RandomOperationTarget::CanonicalKey("target".to_owned()),
886                0,
887                101,
888                "resolution",
889            )
890            .expect_err("changed bound must conflict");
891        assert_eq!(conflict.code, ErrorCode::RandomOperationConflict);
892        let purpose_conflict = session
893            .range_for_operation(
894                &stream,
895                EvidenceRef::Ingress(canwu_core::IngressId::new(2)),
896                "resolve",
897                "operation-alpha",
898                RandomOperationTarget::CanonicalKey("target".to_owned()),
899                0,
900                100,
901                "different-resolution",
902            )
903            .expect_err("changed purpose must conflict");
904        assert_eq!(purpose_conflict.code, ErrorCode::RandomOperationConflict);
905        let execution = session.finish();
906        assert_eq!(execution.draws.len(), 1);
907        assert_eq!(execution.states[&stream].position, 0);
908    }
909
910    #[test]
911    fn evidence_renumbering_and_unrelated_operations_do_not_change_keyed_entropy() {
912        let stream = fixture_stream();
913        let run = |evidence, include_unrelated| {
914            let state = RandomStreamState::initial(91, stream.clone());
915            let mut session = RandomSession::new(
916                &BTreeMap::from([(stream.clone(), state)]),
917                std::slice::from_ref(&stream),
918                91,
919                "fixture-random",
920                &BTreeMap::new(),
921            )
922            .expect("session should initialize");
923            if include_unrelated {
924                session
925                    .range_for_operation(
926                        &stream,
927                        EvidenceRef::Ingress(canwu_core::IngressId::new(1)),
928                        "resolve",
929                        "unrelated-operation",
930                        RandomOperationTarget::CanonicalKey("unrelated".to_owned()),
931                        0,
932                        1_000,
933                        "unrelated-purpose",
934                    )
935                    .expect("unrelated keyed operation should succeed");
936            }
937            session
938                .range_for_operation(
939                    &stream,
940                    EvidenceRef::Ingress(canwu_core::IngressId::new(evidence)),
941                    "resolve",
942                    "operation-alpha",
943                    RandomOperationTarget::CanonicalKey("target".to_owned()),
944                    0,
945                    1_000,
946                    "resolution",
947                )
948                .expect("target keyed operation should succeed")
949        };
950        let baseline = run(2, false);
951        assert_eq!(run(99, false), baseline);
952        assert_eq!(run(2, true), baseline);
953    }
954
955    #[test]
956    fn producer_namespace_is_encoded_and_rejection_reduction_retries() {
957        let stream = fixture_stream();
958        let mut first = fixture_address(RandomOperationTarget::CanonicalKey("target".to_owned()));
959        let mut second = first.clone();
960        second.producer_plugin = "fixture-random-b".to_owned();
961        assert_ne!(first, second);
962        let purpose_hash = purpose_hash_v1("resolution").expect("purpose should hash");
963        assert_ne!(
964            operation_input_v1(17, &stream, &first, 100, &purpose_hash, 0)
965                .expect("first producer input"),
966            operation_input_v1(17, &stream, &second, 100, &purpose_hash, 0)
967                .expect("second producer input")
968        );
969
970        first.application_operation_id = "rejection-reduction".to_owned();
971        let upper_exclusive = (1_u64 << 63) + 1;
972        let range = 1_u128 << 64;
973        let bound = u128::from(upper_exclusive);
974        let accept_limit = (range / bound) * bound;
975        let purpose = (0_u32..10_000)
976            .map(|index| format!("retry-purpose-{index}"))
977            .find(|purpose| {
978                let purpose_hash = purpose_hash_v1(purpose).expect("purpose should hash");
979                let bytes =
980                    operation_input_v1(17, &stream, &first, upper_exclusive, &purpose_hash, 0)
981                        .expect("candidate zero should encode");
982                let digest = blake3::hash(&bytes);
983                let mut candidate_bytes = [0_u8; 8];
984                candidate_bytes.copy_from_slice(&digest.as_bytes()[..8]);
985                u128::from(u64::from_le_bytes(candidate_bytes)) >= accept_limit
986            })
987            .expect("fixture search should find a rejected candidate zero");
988        let value = operation_value_v1(17, &stream, &first, upper_exclusive, &purpose)
989            .expect("rejection reduction must find a later candidate");
990        assert!(value < upper_exclusive);
991    }
992}