chio_kernel/admission_operation/projection/
factor_assignment.rs1use chio_credit::factor::{VerifiedAssignmentAcknowledgementV1, VerifiedAssignmentNotAppliedV1};
2
3use super::*;
4
5pub fn verified_factor_assignment_applied_projection(
6 operation: &AdmissionOperationV1,
7 context: AdmissionProjectionContext,
8 acknowledgement: &VerifiedAssignmentAcknowledgementV1,
9) -> Result<AdmissionTerminalProjection, AdmissionOperationError> {
10 let body = acknowledgement.body();
11 validate_source(
12 operation,
13 &context,
14 body.operation_id(),
15 body.normalized_request_digest(),
16 body.assignment_authorization_set_digest(),
17 body.acknowledged_at_unix_ms(),
18 )?;
19 if body.resulting_disposition_version()
20 != body
21 .prior_disposition_version()
22 .checked_add(1)
23 .ok_or(AdmissionOperationError::InvalidEconomicMutationBinding)?
24 || body.resulting_snapshot_version()
25 != body
26 .expected_snapshot_version()
27 .checked_add(1)
28 .ok_or(AdmissionOperationError::InvalidEconomicMutationBinding)?
29 || body.resulting_resource_fence()
30 != body
31 .expected_resource_fence()
32 .checked_add(1)
33 .ok_or(AdmissionOperationError::InvalidEconomicMutationBinding)?
34 {
35 return Err(AdmissionOperationError::InvalidEconomicMutationBinding);
36 }
37 let result = result_binding(
38 operation,
39 &context,
40 AdmissionOperationState::EconomicMutationApplied,
41 body.acknowledgement_id(),
42 acknowledgement.envelope_digest(),
43 acknowledgement.signature_digest(),
44 body.authority_id(),
45 body.authority_key_epoch(),
46 body.obligation_id(),
47 body.expected_snapshot_version(),
48 body.resulting_snapshot_version(),
49 body.expected_resource_fence(),
50 body.resulting_resource_fence(),
51 EconomicMutationTerminalStatus::Applied,
52 )?;
53 let audit_event = audit_event(
54 operation,
55 &context,
56 AdmissionOperationState::EconomicMutationApplied,
57 body.acknowledgement_id(),
58 acknowledgement.envelope_digest(),
59 )?;
60 Ok(AdmissionTerminalProjection::EconomicMutationApplied {
61 context,
62 result: Box::new(VerifiedEconomicMutationApplied(result)),
63 audit_event: Box::new(audit_event),
64 })
65}
66
67pub fn verified_factor_assignment_not_applied_projection(
68 operation: &AdmissionOperationV1,
69 context: AdmissionProjectionContext,
70 result: &VerifiedAssignmentNotAppliedV1,
71) -> Result<AdmissionTerminalProjection, AdmissionOperationError> {
72 let body = result.body();
73 validate_source(
74 operation,
75 &context,
76 body.operation_id(),
77 body.normalized_request_digest(),
78 body.assignment_authorization_set_digest(),
79 body.decided_at_unix_ms(),
80 )?;
81 let result_binding = result_binding(
82 operation,
83 &context,
84 AdmissionOperationState::EconomicMutationNotApplied,
85 body.result_id(),
86 result.envelope_digest(),
87 result.signature_digest(),
88 body.authority_id(),
89 body.authority_key_epoch(),
90 body.obligation_id(),
91 body.expected_snapshot_version(),
92 body.observed_snapshot_version(),
93 body.expected_resource_fence(),
94 body.resource_fence(),
95 EconomicMutationTerminalStatus::PermanentlyNotApplied,
96 )?;
97 let audit_event = audit_event(
98 operation,
99 &context,
100 AdmissionOperationState::EconomicMutationNotApplied,
101 body.result_id(),
102 result.envelope_digest(),
103 )?;
104 Ok(AdmissionTerminalProjection::EconomicMutationNotApplied {
105 context,
106 result: Box::new(VerifiedEconomicMutationNotApplied(result_binding)),
107 audit_event: Box::new(audit_event),
108 })
109}
110
111fn validate_source(
112 operation: &AdmissionOperationV1,
113 context: &AdmissionProjectionContext,
114 operation_id: &str,
115 normalized_request_digest: &str,
116 authorization_set_digest: &str,
117 authority_time_unix_ms: u64,
118) -> Result<(), AdmissionOperationError> {
119 if operation.binding().kind() != AdmissionOperationKind::GovernedEconomicMutation
120 || operation.state() != AdmissionOperationState::MutationSubmitted
121 || operation.binding().operation_id().as_str() != operation_id
122 || operation.binding().immutable_request_hash().as_str() != normalized_request_digest
123 || operation
124 .supplemental_authorization_digest()
125 .is_none_or(|digest| digest.as_str() != authorization_set_digest)
126 || authority_time_unix_ms > context.trusted_time_unix_ms
127 {
128 return Err(AdmissionOperationError::InvalidEconomicMutationBinding);
129 }
130 context.validate()
131}
132
133#[allow(clippy::too_many_arguments)]
134fn result_binding(
135 operation: &AdmissionOperationV1,
136 context: &AdmissionProjectionContext,
137 projected_state: AdmissionOperationState,
138 record_id: &str,
139 envelope_digest: &str,
140 signature_digest: &str,
141 authority_id: &str,
142 authority_key_epoch: u64,
143 obligation_id: &str,
144 expected_version: u64,
145 resulting_version: u64,
146 expected_fence: u64,
147 resulting_fence: u64,
148 status: EconomicMutationTerminalStatus,
149) -> Result<GovernedEconomicMutationResultBinding, AdmissionOperationError> {
150 let invalid = |_| AdmissionOperationError::InvalidEconomicMutationBinding;
151 let binding = GovernedEconomicMutationResultBinding {
152 binding: AdmissionExactProjectionBindingV1::from_verified(
153 operation,
154 context,
155 projected_state,
156 )?,
157 record_id: AdmissionIdentifier::try_new(
158 "factor_assignment_result_id",
159 record_id.to_owned(),
160 )
161 .map_err(invalid)?,
162 record_digest: AdmissionDigest::try_new(
163 "factor_assignment_result_digest",
164 envelope_digest.to_owned(),
165 )
166 .map_err(invalid)?,
167 participant_id: AdmissionIdentifier::try_new(
168 "factor_assignment_authority_id",
169 authority_id.to_owned(),
170 )
171 .map_err(invalid)?,
172 participant_key_epoch: authority_key_epoch,
173 resource_id: AdmissionIdentifier::try_new(
174 "factor_assignment_obligation_id",
175 obligation_id.to_owned(),
176 )
177 .map_err(invalid)?,
178 expected_resource_version: expected_version,
179 resulting_resource_version: resulting_version,
180 expected_resource_fence: AdmissionIdentifier::try_new(
181 "factor_assignment_expected_resource_fence",
182 format!("factor-assignment-resource-fence:{expected_fence}"),
183 )
184 .map_err(invalid)?,
185 resulting_resource_fence: AdmissionIdentifier::try_new(
186 "factor_assignment_resulting_resource_fence",
187 format!("factor-assignment-resource-fence:{resulting_fence}"),
188 )
189 .map_err(invalid)?,
190 immutable_request_digest: operation.binding().request_binding_hash().clone(),
191 signature_digest: AdmissionDigest::try_new(
192 "factor_assignment_signature_digest",
193 signature_digest.to_owned(),
194 )
195 .map_err(invalid)?,
196 status,
197 anchored_effect: false,
198 };
199 binding.validate_against(operation, context)?;
200 Ok(binding)
201}
202
203fn audit_event(
204 operation: &AdmissionOperationV1,
205 context: &AdmissionProjectionContext,
206 projected_state: AdmissionOperationState,
207 result_id: &str,
208 envelope_digest: &str,
209) -> Result<GovernedMutationAuditEvent, AdmissionOperationError> {
210 let invalid = |_| AdmissionOperationError::InvalidEconomicMutationBinding;
211 Ok(GovernedMutationAuditEvent {
212 binding: AdmissionExactProjectionBindingV1::from_verified(
213 operation,
214 context,
215 projected_state,
216 )?,
217 record_id: AdmissionIdentifier::try_new(
218 "factor_assignment_audit_id",
219 format!("{result_id}:audit"),
220 )
221 .map_err(invalid)?,
222 record_digest: AdmissionDigest::try_new(
223 "factor_assignment_audit_digest",
224 envelope_digest.to_owned(),
225 )
226 .map_err(invalid)?,
227 })
228}