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#[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#[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#[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}