1use crate::types::{AccountTransactionEffects, BlockItemSummaryDetails};
25use crate::v2;
26
27use crate::endpoints::RPCError;
28use crate::smart_contracts::common::AccountAddress;
29use crate::v2::{AccountIdentifier, BlockIdentifier, IntoBlockIdentifier, QueryError};
30use concordium_base::base::{CredentialRegistrationID, Nonce};
31use concordium_base::common::cbor;
32use concordium_base::common::cbor::CborSerializationError;
33use concordium_base::common::upward::UnknownDataError;
34use concordium_base::hashes::TransactionHash;
35use concordium_base::id::types;
36use concordium_base::id::types::{AccountCredentialWithoutProofs, ArInfos, IpIdentity};
37use concordium_base::transactions::{
38 send, BlockItem, ExactSizeTransactionSigner, RegisteredData, TooLargeError,
39};
40use concordium_base::web3id::v1::anchor::{
41 self as anchor, CredentialValidityType, PresentationVerificationResult,
42 VerifiablePresentationV1, VerificationAuditRecord, VerificationContext, VerificationMaterial,
43 VerificationMaterialWithValidity, VerificationRequest, VerificationRequestAnchor,
44 VerificationRequestAnchorAndBlockHash, VerificationRequestData,
45};
46use concordium_base::web3id::v1::{
47 AccountCredentialVerificationMaterial, CredentialMetadataTypeV1, CredentialMetadataV1,
48 IdentityCredentialVerificationMaterial,
49};
50use concordium_base::{hashes, web3id};
51
52use concordium_base::common::types::TransactionTime;
53use futures::StreamExt;
54use futures::{future, TryStreamExt};
55use std::collections::{BTreeMap, HashMap};
56
57#[derive(Debug, Clone, PartialEq)]
64pub struct PresentationVerificationData {
65 pub verification_result: PresentationVerificationResult,
68 pub audit_record: VerificationAuditRecord,
75 pub anchor_transaction_hash: Option<hashes::TransactionHash>,
80}
81
82#[derive(thiserror::Error, Debug)]
84pub enum VerifyError {
85 #[error("on-chain request anchor transaction is of invalid type")]
86 InvalidRequestAnchor,
87 #[error("on-chain request anchor transaction not finalized yet")]
88 RequestAnchorNotFinalized,
89 #[error("unknown data error: {0}")]
90 UnknownDataError(#[from] UnknownDataError),
91 #[error("node query error: {0}")]
92 Query(#[from] v2::QueryError),
93 #[error("create anchor: {0}")]
94 Anchor(#[from] CreateAnchorError),
95 #[error("CBOR serialization error: {0}")]
96 CborSerialization(#[from] CborSerializationError),
97 #[error("unknown identity provider: {0}")]
98 UnknownIdentityProvider(IpIdentity),
99 #[error("credential {cred_id} no longer present on account: {account}")]
100 CredentialNotPresent {
101 cred_id: CredentialRegistrationID,
102 account: AccountAddress,
103 },
104 #[error("initial credential {cred_id} cannot be used.")]
105 InitialCredential { cred_id: CredentialRegistrationID },
106}
107
108pub struct AuditRecordArgument<S: ExactSizeTransactionSigner> {
110 pub audit_record_id: String,
112 pub public_info: Option<HashMap<String, cbor::value::Value>>,
114 pub audit_record_anchor_transaction_metadata: AnchorTransactionMetadata<S>,
116}
117
118pub async fn verify_presentation_and_submit_audit_anchor(
126 client: &mut v2::Client,
127 network: web3id::did::Network,
128 block_identifier: impl IntoBlockIdentifier,
129 verification_request: VerificationRequest,
130 verifiable_presentation: VerifiablePresentationV1,
131 audit_record_arg: AuditRecordArgument<impl ExactSizeTransactionSigner>,
132) -> Result<PresentationVerificationData, VerifyError> {
133 let block_identifier = block_identifier.into_block_identifier();
134 let global_context = client
135 .get_cryptographic_parameters(block_identifier)
136 .await?
137 .response;
138
139 let block_info = client.get_block_info(block_identifier).await?.response;
140
141 let request_anchor = lookup_request_anchor(client, &verification_request).await?;
142
143 let verification_material = lookup_verification_materials_and_validity(
144 client,
145 block_identifier,
146 &verifiable_presentation,
147 )
148 .await?;
149
150 let context = VerificationContext {
151 network,
152 validity_time: block_info.block_slot_time,
153 };
154
155 let verification_result = anchor::verify_presentation_with_request_anchor(
156 &global_context,
157 &context,
158 &verification_request,
159 &verifiable_presentation,
160 &request_anchor,
161 &verification_material,
162 );
163
164 let audit_record = VerificationAuditRecord::new(
165 audit_record_arg.audit_record_id,
166 verification_request,
167 verifiable_presentation,
168 );
169
170 let anchor_transaction_hash = if verification_result.is_success() {
171 let txn_hash = submit_verification_audit_record_anchor(
172 client,
173 audit_record_arg.audit_record_anchor_transaction_metadata,
174 &audit_record,
175 audit_record_arg.public_info,
176 )
177 .await?;
178 Some(txn_hash)
179 } else {
180 None
181 };
182
183 Ok(PresentationVerificationData {
184 verification_result,
185 audit_record,
186 anchor_transaction_hash,
187 })
188}
189
190pub async fn verify_audit_record(
194 client: &mut v2::Client,
195 network: web3id::did::Network,
196 block_identifier: impl IntoBlockIdentifier,
197 verification_audit_record: &VerificationAuditRecord,
198) -> Result<PresentationVerificationResult, VerifyError> {
199 let block_identifier = block_identifier.into_block_identifier();
200 let global_context = client
201 .get_cryptographic_parameters(block_identifier)
202 .await?
203 .response;
204
205 let block_info = client.get_block_info(block_identifier).await?.response;
206
207 let request_anchor = lookup_request_anchor(client, &verification_audit_record.request).await?;
208
209 let verification_material = lookup_verification_materials_and_validity(
210 client,
211 block_identifier,
212 &verification_audit_record.presentation,
213 )
214 .await?;
215
216 let context = VerificationContext {
217 network,
218 validity_time: block_info.block_slot_time,
219 };
220
221 Ok(anchor::verify_presentation_with_request_anchor(
222 &global_context,
223 &context,
224 &verification_audit_record.request,
225 &verification_audit_record.presentation,
226 &request_anchor,
227 &verification_material,
228 ))
229}
230
231pub async fn lookup_request_anchor(
234 client: &mut v2::Client,
235 verification_request: &VerificationRequest,
236) -> Result<VerificationRequestAnchorAndBlockHash, VerifyError> {
237 let item_status = client
239 .get_block_item_status(&verification_request.anchor_transaction_hash)
240 .await?;
241
242 let (block_hash, summary) = item_status
243 .is_finalized()
244 .ok_or(VerifyError::RequestAnchorNotFinalized)?;
245
246 let BlockItemSummaryDetails::AccountTransaction(anchor_tx) =
248 summary.details.as_ref().known_or_err()?
249 else {
250 return Err(VerifyError::InvalidRequestAnchor);
251 };
252
253 let AccountTransactionEffects::DataRegistered { data } =
255 anchor_tx.effects.as_ref().known_or_err()?
256 else {
257 return Err(VerifyError::InvalidRequestAnchor);
258 };
259
260 let verification_request_anchor: VerificationRequestAnchor = cbor::cbor_decode(data.as_ref())?;
262
263 Ok(VerificationRequestAnchorAndBlockHash {
264 verification_request_anchor,
265 block_hash: *block_hash,
266 })
267}
268
269pub async fn lookup_verification_materials_and_validity(
271 client: &mut v2::Client,
272 block_identifier: BlockIdentifier,
273 presentation: &VerifiablePresentationV1,
274) -> Result<Vec<VerificationMaterialWithValidity>, VerifyError> {
275 let verification_material =
276 future::try_join_all(presentation.metadata().map(|cred_metadata| {
277 let mut client = client.clone();
278 async move {
279 lookup_verification_material_and_validity(
280 &mut client,
281 block_identifier,
282 &cred_metadata,
283 )
284 .await
285 }
286 }))
287 .await?;
288 Ok(verification_material)
289}
290
291async fn lookup_verification_material_and_validity(
293 client: &mut v2::Client,
294 block_identifier: BlockIdentifier,
295 cred_metadata: &CredentialMetadataV1,
296) -> Result<VerificationMaterialWithValidity, VerifyError> {
297 Ok(match &cred_metadata.cred_metadata {
298 CredentialMetadataTypeV1::Account(metadata) => {
299 let account_info = client
300 .get_account_info(
301 &AccountIdentifier::CredId(metadata.cred_id),
302 block_identifier,
303 )
304 .await?;
305
306 let Some(account_cred) =
307 account_info
308 .response
309 .account_credentials
310 .values()
311 .find_map(|cred| {
312 cred.value
313 .as_ref()
314 .known()
315 .and_then(|c| (c.cred_id() == metadata.cred_id.as_ref()).then_some(c))
316 })
317 else {
318 return Err(VerifyError::CredentialNotPresent {
319 cred_id: metadata.cred_id,
320 account: account_info.response.account_address,
321 });
322 };
323
324 match account_cred {
325 AccountCredentialWithoutProofs::Initial { .. } => {
326 return Err(VerifyError::InitialCredential {
327 cred_id: metadata.cred_id,
328 })
329 }
330 AccountCredentialWithoutProofs::Normal { cdv, commitments } => {
331 let credential_validity = types::CredentialValidity {
332 created_at: account_cred.policy().created_at,
333 valid_to: cdv.policy.valid_to,
334 };
335
336 VerificationMaterialWithValidity {
337 verification_material: VerificationMaterial::Account(
338 AccountCredentialVerificationMaterial {
339 issuer: cdv.ip_identity,
340 attribute_commitments: commitments.cmm_attributes.clone(),
341 },
342 ),
343 validity: CredentialValidityType::ValidityPeriod(credential_validity),
344 }
345 }
346 }
347 }
348 CredentialMetadataTypeV1::Identity(metadata) => {
349 let ip_info = client
350 .get_identity_providers(block_identifier)
351 .await?
352 .response
353 .try_filter(|ip| future::ready(ip.ip_identity == metadata.issuer))
354 .next()
355 .await
356 .ok_or(VerifyError::UnknownIdentityProvider(metadata.issuer))?
357 .map_err(|status| QueryError::RPCError(RPCError::CallError(status)))?;
358
359 let ars_infos: BTreeMap<_, _> = client
360 .get_anonymity_revokers(block_identifier)
361 .await?
362 .response
363 .map_ok(|ar_info| (ar_info.ar_identity, ar_info))
364 .try_collect()
365 .await
366 .map_err(|status| QueryError::RPCError(RPCError::CallError(status)))?;
367
368 VerificationMaterialWithValidity {
369 verification_material: VerificationMaterial::Identity(
370 IdentityCredentialVerificationMaterial {
371 ip_info,
372 ars_infos: ArInfos {
373 anonymity_revokers: ars_infos,
374 },
375 },
376 ),
377 validity: CredentialValidityType::ValidityPeriod(metadata.validity.clone()),
378 }
379 }
380 })
381}
382
383#[derive(thiserror::Error, Debug)]
385pub enum CreateAnchorError {
386 #[error("node query error: {0}")]
387 Query(#[from] v2::QueryError),
388 #[error("data register transaction data is too large: {0}")]
389 TooLarge(#[from] TooLargeError),
390 #[error("CBOR serialization error: {0}")]
391 CborSerialization(#[from] CborSerializationError),
392}
393
394impl From<RPCError> for CreateAnchorError {
395 fn from(err: RPCError) -> Self {
396 CreateAnchorError::Query(err.into())
397 }
398}
399
400pub struct AnchorTransactionMetadata<S: ExactSizeTransactionSigner> {
402 pub signer: S,
404 pub sender: AccountAddress,
406 pub account_sequence_number: Nonce,
408 pub expiry: TransactionTime,
410}
411
412pub async fn create_verification_request_and_submit_request_anchor<
421 S: ExactSizeTransactionSigner,
422>(
423 client: &mut v2::Client,
424 anchor_transaction_metadata: AnchorTransactionMetadata<S>,
425 verification_request_data: VerificationRequestData,
426 public_info: Option<HashMap<String, cbor::value::Value>>,
427) -> Result<VerificationRequest, CreateAnchorError> {
428 let verification_request_anchor = verification_request_data.to_anchor(public_info);
429 let cbor = cbor::cbor_encode(&verification_request_anchor)?;
430 let register_data = RegisteredData::try_from(cbor)?;
431
432 let tx = send::register_data(
433 &anchor_transaction_metadata.signer,
434 anchor_transaction_metadata.sender,
435 anchor_transaction_metadata.account_sequence_number,
436 anchor_transaction_metadata.expiry,
437 register_data,
438 );
439 let block_item = BlockItem::AccountTransaction(tx);
440
441 let transaction_hash = client.send_block_item(&block_item).await?;
443
444 Ok(VerificationRequest {
445 context: verification_request_data.context,
446 subject_claims: verification_request_data.subject_claims,
447 anchor_transaction_hash: transaction_hash,
448 })
449}
450
451pub async fn submit_verification_audit_record_anchor<S: ExactSizeTransactionSigner>(
457 client: &mut v2::Client,
458 anchor_transaction_metadata: AnchorTransactionMetadata<S>,
459 verification_audit_record: &VerificationAuditRecord,
460 public_info: Option<HashMap<String, cbor::value::Value>>,
461) -> Result<TransactionHash, CreateAnchorError> {
462 let verification_audit_anchor = verification_audit_record.to_anchor(public_info);
463 let cbor = cbor::cbor_encode(&verification_audit_anchor)?;
464 let register_data = RegisteredData::try_from(cbor)?;
465
466 let tx = send::register_data(
467 &anchor_transaction_metadata.signer,
468 anchor_transaction_metadata.sender,
469 anchor_transaction_metadata.account_sequence_number,
470 anchor_transaction_metadata.expiry,
471 register_data,
472 );
473 let item = BlockItem::AccountTransaction(tx);
474
475 let transaction_hash = client.send_block_item(&item).await?;
477
478 Ok(transaction_hash)
479}