dpp_calc/kernel/
receipt.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use super::error::CalcError;
8
9pub use super::hashing::{input_hash, jcs_hash};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct CalculationReceipt {
26 pub receipt_id: Uuid,
28 pub input_hash: String,
30 pub output_hash: String,
33 pub ruleset_id: String,
35 pub ruleset_version: String,
37 pub factor_dataset_id: String,
39 pub factor_dataset_version: String,
41 pub factor_set_hash: Option<String>,
44 pub computed_at: DateTime<Utc>,
46 pub jws: Option<String>,
49}
50
51impl CalculationReceipt {
52 pub fn new(
53 input_hash: impl Into<String>,
54 ruleset_id: impl Into<String>,
55 ruleset_version: impl Into<String>,
56 ) -> Self {
57 Self {
58 receipt_id: Uuid::now_v7(),
59 input_hash: input_hash.into(),
60 output_hash: String::new(),
61 ruleset_id: ruleset_id.into(),
62 ruleset_version: ruleset_version.into(),
63 factor_dataset_id: String::new(),
64 factor_dataset_version: String::new(),
65 factor_set_hash: None,
66 computed_at: Utc::now(),
67 jws: None,
68 }
69 }
70
71 pub fn with_output_hash(mut self, hash: impl Into<String>) -> Self {
73 self.output_hash = hash.into();
74 self
75 }
76
77 pub fn with_factor_provider(mut self, provider: &dyn super::factor::FactorProvider) -> Self {
79 self.factor_dataset_id = provider.dataset_id().to_owned();
80 self.factor_dataset_version = provider.dataset_version().to_owned();
81 self.factor_set_hash = Some(provider.table_hash().to_owned());
82 self
83 }
84
85 pub fn seal_with_jws(mut self, jws: String) -> Self {
89 self.jws = Some(jws);
90 self
91 }
92
93 pub fn canonical_bytes_for_signing(&self) -> Result<Vec<u8>, CalcError> {
99 let mut v =
100 serde_json::to_value(self).map_err(|e| CalcError::CanonicalizeError(e.to_string()))?;
101 if let Some(obj) = v.as_object_mut() {
102 obj.remove("jws");
103 }
104 serde_jcs::to_vec(&v).map_err(|e| CalcError::CanonicalizeError(e.to_string()))
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::factor::FactorProvider;
112
113 struct DummyProvider;
114 impl FactorProvider for DummyProvider {
115 fn dataset_id(&self) -> &str {
116 "dummy-ds"
117 }
118 fn dataset_version(&self) -> &str {
119 "1.2.3"
120 }
121 fn gwp100(&self, _activity_uuid: &str) -> Result<f64, CalcError> {
122 Ok(1.0)
123 }
124 fn table_hash(&self) -> &str {
125 "deadbeef"
126 }
127 }
128
129 #[test]
130 fn builder_records_output_factor_and_jws() {
131 let receipt = CalculationReceipt::new("in-hash", "ruleset-x", "1.0.0")
132 .with_output_hash("out-hash")
133 .with_factor_provider(&DummyProvider)
134 .seal_with_jws("jws-token".to_owned());
135
136 assert_eq!(receipt.input_hash, "in-hash");
137 assert_eq!(receipt.output_hash, "out-hash");
138 assert_eq!(receipt.ruleset_id, "ruleset-x");
139 assert_eq!(receipt.ruleset_version, "1.0.0");
140 assert_eq!(receipt.factor_dataset_id, "dummy-ds");
141 assert_eq!(receipt.factor_dataset_version, "1.2.3");
142 assert_eq!(receipt.factor_set_hash.as_deref(), Some("deadbeef"));
143 assert_eq!(receipt.jws.as_deref(), Some("jws-token"));
144 }
145
146 #[test]
147 fn canonical_bytes_exclude_the_jws_field() {
148 let sealed = CalculationReceipt::new("in", "r", "1.0.0").seal_with_jws("secret".to_owned());
149 let bytes = sealed.canonical_bytes_for_signing().unwrap();
150 let text = String::from_utf8(bytes).unwrap();
151 assert!(
152 !text.contains("secret"),
153 "jws must be excluded from the signing payload"
154 );
155 assert!(text.contains("in"));
156 }
157}