1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct PreRegistration {
13 pub title: String,
15 pub hypothesis: String,
17 pub methods: String,
19 pub analysis_plan: String,
21 pub notes: Option<String>,
23}
24
25impl PreRegistration {
26 pub fn new(
28 title: impl Into<String>,
29 hypothesis: impl Into<String>,
30 methods: impl Into<String>,
31 analysis_plan: impl Into<String>,
32 ) -> Self {
33 Self {
34 title: title.into(),
35 hypothesis: hypothesis.into(),
36 methods: methods.into(),
37 analysis_plan: analysis_plan.into(),
38 notes: None,
39 }
40 }
41
42 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
44 self.notes = Some(notes.into());
45 self
46 }
47
48 pub fn commit(&self) -> PreRegistrationCommitment {
50 let serialized =
51 serde_json::to_string(self).expect("PreRegistration should always serialize");
52 let mut hasher = Sha256::new();
53 hasher.update(serialized.as_bytes());
54 let hash = hex::encode(hasher.finalize());
55
56 PreRegistrationCommitment { hash, created_at: chrono::Utc::now() }
57 }
58
59 pub fn verify_commitment(&self, commitment: &PreRegistrationCommitment) -> bool {
61 let current = self.commit();
62 current.hash == commitment.hash
63 }
64
65 pub fn reveal(
67 &self,
68 commitment: &PreRegistrationCommitment,
69 ) -> Result<PreRegistrationReveal, PreRegistrationError> {
70 if !self.verify_commitment(commitment) {
71 return Err(PreRegistrationError::HashMismatch);
72 }
73
74 Ok(PreRegistrationReveal {
75 protocol: self.clone(),
76 commitment: commitment.clone(),
77 revealed_at: chrono::Utc::now(),
78 })
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct PreRegistrationCommitment {
85 pub hash: String,
87 pub created_at: chrono::DateTime<chrono::Utc>,
89}
90
91impl PreRegistrationCommitment {
92 pub fn hash_bytes(&self) -> Vec<u8> {
94 hex::decode(&self.hash).unwrap_or_default()
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct PreRegistrationReveal {
101 pub protocol: PreRegistration,
103 pub commitment: PreRegistrationCommitment,
105 pub revealed_at: chrono::DateTime<chrono::Utc>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum TimestampProof {
112 GitCommit(String),
114 Rfc3161(Vec<u8>),
116 OpenTimestamps(Vec<u8>),
118}
119
120impl TimestampProof {
121 pub fn git(commit_hash: impl Into<String>) -> Self {
123 Self::GitCommit(commit_hash.into())
124 }
125
126 pub fn is_git(&self) -> bool {
128 matches!(self, Self::GitCommit(_))
129 }
130
131 pub fn git_commit(&self) -> Option<&str> {
133 match self {
134 Self::GitCommit(hash) => Some(hash),
135 _ => None,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct SignedPreRegistration {
143 pub registration: PreRegistration,
145 pub commitment: PreRegistrationCommitment,
147 pub signature: String,
149 pub public_key: String,
151 pub timestamp_proof: Option<TimestampProof>,
153}
154
155impl SignedPreRegistration {
156 pub fn sign(registration: &PreRegistration, signing_key: &SigningKey) -> Self {
158 let commitment = registration.commit();
159 let signature = signing_key.sign(commitment.hash.as_bytes());
160 let public_key = signing_key.verifying_key();
161
162 Self {
163 registration: registration.clone(),
164 commitment,
165 signature: hex::encode(signature.to_bytes()),
166 public_key: hex::encode(public_key.to_bytes()),
167 timestamp_proof: None,
168 }
169 }
170
171 pub fn with_timestamp_proof(mut self, proof: TimestampProof) -> Self {
173 self.timestamp_proof = Some(proof);
174 self
175 }
176
177 pub fn verify(&self) -> Result<bool, PreRegistrationError> {
179 let pk_bytes =
181 hex::decode(&self.public_key).map_err(|_err| PreRegistrationError::InvalidPublicKey)?;
182 let pk_array: [u8; 32] =
183 pk_bytes.try_into().map_err(|_err| PreRegistrationError::InvalidPublicKey)?;
184 let public_key = VerifyingKey::from_bytes(&pk_array)
185 .map_err(|_err| PreRegistrationError::InvalidPublicKey)?;
186
187 let sig_bytes =
189 hex::decode(&self.signature).map_err(|_err| PreRegistrationError::InvalidSignature)?;
190 let sig_array: [u8; 64] =
191 sig_bytes.try_into().map_err(|_err| PreRegistrationError::InvalidSignature)?;
192 let signature = Signature::from_bytes(&sig_array);
193
194 Ok(public_key.verify(self.commitment.hash.as_bytes(), &signature).is_ok())
196 }
197
198 pub fn verify_full(&self) -> Result<bool, PreRegistrationError> {
200 if !self.verify()? {
202 return Ok(false);
203 }
204
205 Ok(self.registration.verify_commitment(&self.commitment))
207 }
208}
209
210#[derive(Debug, Clone, thiserror::Error)]
212pub enum PreRegistrationError {
213 #[error("Hash mismatch: pre-registration does not match commitment")]
214 HashMismatch,
215 #[error("Invalid public key")]
216 InvalidPublicKey,
217 #[error("Invalid signature")]
218 InvalidSignature,
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn create_test_registration() -> PreRegistration {
226 PreRegistration::new(
227 "Effect of Treatment A on Outcome B",
228 "Treatment A will improve Outcome B by at least 20%",
229 "Randomized controlled trial with 100 participants",
230 "Two-sample t-test with alpha=0.05",
231 )
232 }
233
234 #[test]
235 fn test_commit_creates_hash() {
236 let reg = create_test_registration();
237 let commitment = reg.commit();
238
239 assert!(!commitment.hash.is_empty());
240 assert_eq!(commitment.hash.len(), 64); }
242
243 #[test]
244 fn test_commit_is_deterministic() {
245 let reg = create_test_registration();
246 let commitment1 = reg.commit();
247 let commitment2 = reg.commit();
248
249 assert_eq!(commitment1.hash, commitment2.hash);
250 }
251
252 #[test]
253 fn test_reveal_verifies_hash() {
254 let reg = create_test_registration();
255 let commitment = reg.commit();
256 let reveal = reg.reveal(&commitment);
257
258 assert!(reveal.is_ok());
259 let reveal = reveal.expect("operation should succeed");
260 assert_eq!(reveal.protocol, reg);
261 }
262
263 #[test]
264 fn test_reveal_fails_on_tamper() {
265 let reg = create_test_registration();
266 let commitment = reg.commit();
267
268 let tampered = PreRegistration::new(
270 "Effect of Treatment A on Outcome B",
271 "Treatment A will improve Outcome B by at least 50%", "Randomized controlled trial with 100 participants",
273 "Two-sample t-test with alpha=0.05",
274 );
275
276 let result = tampered.reveal(&commitment);
277 assert!(result.is_err());
278 assert!(matches!(result.unwrap_err(), PreRegistrationError::HashMismatch));
279 }
280
281 #[test]
282 fn test_timestamp_proof_git() {
283 let proof = TimestampProof::git("abc123def456");
284
285 assert!(proof.is_git());
286 assert_eq!(proof.git_commit(), Some("abc123def456"));
287 }
288
289 #[test]
290 fn test_timestamp_proof_rfc3161() {
291 let proof = TimestampProof::Rfc3161(vec![1, 2, 3, 4]);
292
293 assert!(!proof.is_git());
294 assert_eq!(proof.git_commit(), None);
295 }
296
297 #[test]
298 fn test_ed25519_signing() {
299 let reg = create_test_registration();
300
301 let signing_key = SigningKey::from_bytes(&[1u8; 32]);
303
304 let signed = SignedPreRegistration::sign(®, &signing_key);
306
307 assert!(!signed.signature.is_empty());
308 assert!(!signed.public_key.is_empty());
309 assert_eq!(signed.signature.len(), 128); assert_eq!(signed.public_key.len(), 64); }
312
313 #[test]
314 fn test_ed25519_verification() {
315 let reg = create_test_registration();
316 let signing_key = SigningKey::from_bytes(&[42u8; 32]);
317 let signed = SignedPreRegistration::sign(®, &signing_key);
318
319 assert!(signed.verify().expect("operation should succeed"));
320 assert!(signed.verify_full().expect("operation should succeed"));
321 }
322
323 #[test]
324 fn test_ed25519_verification_fails_on_tamper() {
325 let reg = create_test_registration();
326 let signing_key = SigningKey::from_bytes(&[42u8; 32]);
327 let mut signed = SignedPreRegistration::sign(®, &signing_key);
328
329 signed.commitment.hash = "0".repeat(64);
331
332 assert!(!signed.verify().expect("operation should succeed"));
334 }
335
336 #[test]
337 fn test_signed_with_timestamp() {
338 let reg = create_test_registration();
339 let signing_key = SigningKey::from_bytes(&[1u8; 32]);
340
341 let signed = SignedPreRegistration::sign(®, &signing_key)
342 .with_timestamp_proof(TimestampProof::git("deadbeef"));
343
344 assert!(signed.timestamp_proof.is_some());
345 assert!(signed.timestamp_proof.as_ref().expect("operation should succeed").is_git());
346 }
347
348 #[test]
349 fn test_commitment_hash_bytes() {
350 let reg = create_test_registration();
351 let commitment = reg.commit();
352
353 let bytes = commitment.hash_bytes();
354 assert_eq!(bytes.len(), 32); }
356
357 #[test]
358 fn test_registration_with_notes() {
359 let reg = create_test_registration().with_notes("Additional protocol considerations");
360
361 assert_eq!(reg.notes, Some("Additional protocol considerations".to_string()));
362
363 let reg_without_notes = create_test_registration();
365 assert_ne!(reg.commit().hash, reg_without_notes.commit().hash);
366 }
367
368 #[test]
369 fn test_different_registrations_different_hashes() {
370 let reg1 = create_test_registration();
371 let reg2 = PreRegistration::new(
372 "Different Study",
373 "Different hypothesis",
374 "Different methods",
375 "Different analysis",
376 );
377
378 assert_ne!(reg1.commit().hash, reg2.commit().hash);
379 }
380}