1use sha2::{Digest, Sha256};
15
16use serde::{Deserialize, Serialize};
17use serde_json::{Map, Value};
18use thiserror::Error;
19
20pub const RNG_RECORDS_METADATA_KEY: &str = "rng_records";
22
23const REPLICATE_SEED_METHOD: &str = "sha256-domain-separated";
24const REPLICATE_SEED_VERSION: &str = "scientific-workflow.replicate-seed.v1";
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct ReplicateSeedDeriver {
34 base_seed: u64,
35 replicate_index: u64,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct DerivedSeed {
41 value: u64,
42 record: RngRecord,
43}
44
45impl ReplicateSeedDeriver {
46 pub const fn new(base_seed: u64, replicate_index: u64) -> Self {
48 Self {
49 base_seed,
50 replicate_index,
51 }
52 }
53
54 pub const fn base_seed(self) -> u64 {
56 self.base_seed
57 }
58
59 pub const fn replicate_index(self) -> u64 {
61 self.replicate_index
62 }
63
64 pub fn derive(self, namespace: &str) -> Result<DerivedSeed, RngRecordError> {
72 if namespace.trim().is_empty() {
73 return Err(RngRecordError::EmptySeedNamespace);
74 }
75 let namespace_length =
76 u64::try_from(namespace.len()).map_err(|_| RngRecordError::SeedNamespaceTooLong)?;
77 let mut hasher = Sha256::new();
78 hasher.update(REPLICATE_SEED_VERSION.as_bytes());
79 hasher.update([0]);
80 hasher.update(self.base_seed.to_be_bytes());
81 hasher.update(self.replicate_index.to_be_bytes());
82 hasher.update(namespace_length.to_be_bytes());
83 hasher.update(namespace.as_bytes());
84 let digest = hasher.finalize();
85 let value = u64::from_be_bytes(
86 digest[..8]
87 .try_into()
88 .expect("SHA-256 always contains at least eight bytes"),
89 );
90 let parameters = Map::from_iter([
91 ("base_seed".to_owned(), Value::from(self.base_seed)),
92 (
93 "replicate_index".to_owned(),
94 Value::from(self.replicate_index),
95 ),
96 ]);
97 let record = RngRecord::new(
98 namespace,
99 REPLICATE_SEED_METHOD,
100 REPLICATE_SEED_VERSION,
101 "u64-decimal",
102 value.to_string(),
103 Some(parameters),
104 )?;
105 Ok(DerivedSeed { value, record })
106 }
107}
108
109impl DerivedSeed {
110 pub const fn value(&self) -> u64 {
112 self.value
113 }
114
115 pub const fn record(&self) -> &RngRecord {
117 &self.record
118 }
119
120 pub fn into_parts(self) -> (u64, RngRecord) {
122 (self.value, self.record)
123 }
124}
125
126#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
135#[serde(deny_unknown_fields)]
136pub struct RngRecord {
137 namespace: String,
138 method: String,
139 version: String,
140 key_encoding: String,
141 key: String,
142 #[serde(default, skip_serializing_if = "Map::is_empty")]
143 parameters: Map<String, Value>,
144}
145
146impl RngRecord {
147 pub fn new(
149 namespace: impl Into<String>,
150 method: impl Into<String>,
151 version: impl Into<String>,
152 key_encoding: impl Into<String>,
153 key: impl Into<String>,
154 parameters: Option<Map<String, Value>>,
155 ) -> Result<Self, RngRecordError> {
156 let record = Self {
157 namespace: namespace.into(),
158 method: method.into(),
159 version: version.into(),
160 key_encoding: key_encoding.into(),
161 key: key.into(),
162 parameters: parameters.unwrap_or_default(),
163 };
164 record.validate()?;
165 Ok(record)
166 }
167
168 pub fn namespace(&self) -> &str {
170 &self.namespace
171 }
172
173 pub fn method(&self) -> &str {
175 &self.method
176 }
177
178 pub fn version(&self) -> &str {
180 &self.version
181 }
182
183 pub fn key_encoding(&self) -> &str {
185 &self.key_encoding
186 }
187
188 pub fn key(&self) -> &str {
190 &self.key
191 }
192
193 pub const fn parameters(&self) -> &Map<String, Value> {
195 &self.parameters
196 }
197
198 pub fn insert_into_metadata(
203 &self,
204 metadata: &mut Map<String, Value>,
205 ) -> Result<(), RngRecordError> {
206 self.validate()?;
207 let records = metadata
208 .entry(RNG_RECORDS_METADATA_KEY.to_owned())
209 .or_insert_with(|| Value::Object(Map::new()))
210 .as_object_mut()
211 .ok_or(RngRecordError::InvalidMetadataShape)?;
212 if records.contains_key(&self.namespace) {
213 return Err(RngRecordError::DuplicateNamespace {
214 namespace: self.namespace.clone(),
215 });
216 }
217 records.insert(
218 self.namespace.clone(),
219 serde_json::to_value(self).expect("RNG records contain only JSON-compatible values"),
220 );
221 Ok(())
222 }
223
224 pub fn from_metadata(
226 metadata: &Map<String, Value>,
227 namespace: &str,
228 ) -> Result<Option<Self>, RngRecordError> {
229 let Some(value) = metadata.get(RNG_RECORDS_METADATA_KEY) else {
230 return Ok(None);
231 };
232 let records = value
233 .as_object()
234 .ok_or(RngRecordError::InvalidMetadataShape)?;
235 let Some(value) = records.get(namespace) else {
236 return Ok(None);
237 };
238 let record: Self = serde_json::from_value(value.clone()).map_err(|source| {
239 RngRecordError::InvalidStoredRecord {
240 namespace: namespace.to_owned(),
241 source,
242 }
243 })?;
244 record.validate()?;
245 if record.namespace != namespace {
246 return Err(RngRecordError::NamespaceMismatch {
247 index: namespace.to_owned(),
248 record: record.namespace,
249 });
250 }
251 Ok(Some(record))
252 }
253
254 fn validate(&self) -> Result<(), RngRecordError> {
255 for (field, value) in [
256 ("namespace", self.namespace.as_str()),
257 ("method", self.method.as_str()),
258 ("version", self.version.as_str()),
259 ("key_encoding", self.key_encoding.as_str()),
260 ("key", self.key.as_str()),
261 ] {
262 if value.trim().is_empty() {
263 return Err(RngRecordError::EmptyField { field });
264 }
265 }
266 Ok(())
267 }
268}
269
270#[derive(Debug, Error)]
272#[non_exhaustive]
273pub enum RngRecordError {
274 #[error("replicate seed namespace must not be empty or whitespace-only")]
276 EmptySeedNamespace,
277 #[error("replicate seed namespace is too long")]
279 SeedNamespaceTooLong,
280 #[error("RNG record field `{field}` must not be empty")]
282 EmptyField {
283 field: &'static str,
285 },
286 #[error("RNG namespace `{namespace}` is recorded more than once")]
288 DuplicateNamespace {
289 namespace: String,
291 },
292 #[error("user metadata `{RNG_RECORDS_METADATA_KEY}` entry must be an object")]
294 InvalidMetadataShape,
295 #[error("invalid RNG record for namespace `{namespace}`")]
297 InvalidStoredRecord {
298 namespace: String,
300 #[source]
302 source: serde_json::Error,
303 },
304 #[error("RNG metadata index `{index}` contains record namespace `{record}`")]
306 NamespaceMismatch {
307 index: String,
309 record: String,
311 },
312}