1#![forbid(unsafe_code)]
12#![warn(missing_docs)]
13
14extern crate self as arkhe_forge_core;
17
18pub mod action;
19pub mod activity;
20pub mod actor;
21pub mod brand;
22pub mod bridge;
23pub mod component;
24pub mod context;
25pub mod entry;
26pub mod event;
27pub mod pii;
28pub mod pipeline;
29#[doc(hidden)]
37#[path = "sealed.rs"]
38pub mod __sealed;
39pub mod space;
40pub mod typecode;
41pub mod user;
42
43pub use arkhe_forge_macros::{arkhe_pure, ArkheAction, ArkheComponent, ArkheEvent};
47
48use arkhe_kernel::abi::{EntityId, InstanceId, Tick, TypeCode};
49
50pub const RUNTIME_SEMVER: (u16, u16, u16) = (0, 14, 0);
52
53pub const MAX_ID_DERIVE_RETRIES: u32 = 16;
55
56#[must_use]
64pub fn derive_entity_id(
65 world_seed: &[u8; 32],
66 instance_id: InstanceId,
67 type_code: TypeCode,
68 tick: Tick,
69 seq: u32,
70) -> Option<EntityId> {
71 let domain_key = blake3::derive_key("arkhe-forge-entity-id", world_seed);
72 for bump in 0..MAX_ID_DERIVE_RETRIES {
73 let mut h = blake3::Hasher::new_keyed(&domain_key);
74 h.update(&instance_id.get().to_be_bytes());
75 h.update(&type_code.0.to_be_bytes());
76 h.update(&tick.0.to_be_bytes());
77 h.update(&seq.to_be_bytes());
84 h.update(&bump.to_be_bytes());
85 let out = h.finalize();
86 let digest = out.as_bytes();
87 let mut first_eight = [0u8; 8];
88 first_eight.copy_from_slice(&digest[..8]);
89 let raw = u64::from_be_bytes(first_eight);
90 if let Some(id) = EntityId::new(raw) {
91 return Some(id);
92 }
93 }
94 None
95}
96
97#[cfg(test)]
98#[allow(clippy::unwrap_used, clippy::expect_used)]
99mod lib_tests {
100 use super::*;
101
102 fn instance(v: u64) -> InstanceId {
103 InstanceId::new(v).unwrap()
104 }
105
106 #[test]
107 fn derive_entity_id_is_deterministic() {
108 let seed = [0x11u8; 32];
109 let iid = instance(1);
110 let a = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(42), 0);
111 let b = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(42), 0);
112 assert_eq!(a, b);
113 assert!(a.is_some());
114 }
115
116 #[test]
117 fn derive_entity_id_differs_across_type_code() {
118 let seed = [0x11u8; 32];
119 let iid = instance(1);
120 let a = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(1), 0);
121 let b = derive_entity_id(&seed, iid, TypeCode(0x0001_0002), Tick(1), 0);
122 assert_ne!(a, b);
123 }
124
125 #[test]
126 fn derive_entity_id_differs_across_instance() {
127 let seed = [0x11u8; 32];
128 let a = derive_entity_id(&seed, instance(1), TypeCode(0x0001_0001), Tick(1), 0);
129 let b = derive_entity_id(&seed, instance(2), TypeCode(0x0001_0001), Tick(1), 0);
130 assert_ne!(a, b);
131 }
132
133 #[test]
134 fn derive_entity_id_differs_across_world_seed() {
135 let s1 = [0x01u8; 32];
136 let s2 = [0x02u8; 32];
137 let iid = instance(1);
138 let a = derive_entity_id(&s1, iid, TypeCode(0x0001_0001), Tick(1), 0);
139 let b = derive_entity_id(&s2, iid, TypeCode(0x0001_0001), Tick(1), 0);
140 assert_ne!(a, b);
141 }
142
143 #[test]
144 fn derive_entity_id_seq_changes_output() {
145 let seed = [0x11u8; 32];
146 let iid = instance(1);
147 let a = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(1), 0);
148 let b = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(1), 1);
149 assert_ne!(a, b);
150 }
151
152 #[test]
158 fn derive_entity_id_linear_seq_space_is_collision_free() {
159 use std::collections::HashSet;
160 let seed = [0x5Au8; 32];
161 let iid = instance(1);
162 let mut seen = HashSet::new();
163 for seq in 0u32..4096 {
164 let id = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(7), seq)
165 .expect("non-zero digest within retry bound");
166 assert!(
167 seen.insert(id.get()),
168 "seq {seq} produced a duplicate EntityId — linear seq collision",
169 );
170 }
171 }
172}