trustchain_ion/
verifier.rs

1//! Implementation of `Verifier` API for ION DID method.
2use crate::commitment::{BlockTimestampCommitment, IONCommitment};
3use crate::config::ion_config;
4use crate::resolver::HTTPTrustchainResolver;
5use crate::sidetree::{ChunkFile, ChunkFileUri, CoreIndexFile, ProvisionalIndexFile};
6use crate::utils::{
7    block_header, decode_ipfs_content, locate_transaction, query_ipfs, transaction,
8    tx_to_op_return_cid,
9};
10use crate::{FullClient, LightClient, URL};
11use async_trait::async_trait;
12use bitcoin::blockdata::transaction::Transaction;
13use bitcoin::hash_types::BlockHash;
14use bitcoincore_rpc::RpcApi;
15use did_ion::sidetree::Delta;
16use futures::TryFutureExt;
17use ipfs_api_backend_hyper::IpfsClient;
18use mongodb::bson::doc;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use ssi::did::Document;
22use ssi::did_resolve::{DIDResolver, DocumentMetadata};
23use std::collections::HashMap;
24use std::marker::PhantomData;
25use std::str::FromStr;
26use std::sync::{Arc, Mutex};
27use trustchain_core::commitment::{
28    CommitmentChain, CommitmentError, DIDCommitment, TimestampCommitment,
29};
30use trustchain_core::resolver::{ResolverError, TrustchainResolver};
31use trustchain_core::verifier::{Timestamp, VerifiableTimestamp, Verifier, VerifierError};
32
33/// Data bundle for DID timestamp verification.
34#[derive(Serialize, Deserialize, Clone, Debug)]
35pub struct VerificationBundle {
36    /// DID Document.
37    did_doc: Document,
38    /// DID Document Metadata.
39    did_doc_meta: DocumentMetadata,
40    /// ION chunk file.
41    chunk_file: Vec<u8>,
42    /// ION provisional index file.
43    provisional_index_file: Vec<u8>,
44    /// ION core index file.
45    core_index_file: Vec<u8>,
46    /// Bitcoin Transaction (the one that anchors the DID operation in the blockchain).
47    transaction: Vec<u8>,
48    /// MerkleBlock (containing a PartialMerkleTree and the BlockHeader).
49    merkle_block: Vec<u8>,
50    /// Bitcoin block header.
51    block_header: Vec<u8>,
52}
53
54impl VerificationBundle {
55    pub fn new(
56        did_doc: Document,
57        did_doc_meta: DocumentMetadata,
58        chunk_file: Vec<u8>,
59        provisional_index_file: Vec<u8>,
60        core_index_file: Vec<u8>,
61        transaction: Vec<u8>,
62        merkle_block: Vec<u8>,
63        block_header: Vec<u8>,
64    ) -> Self {
65        Self {
66            did_doc,
67            did_doc_meta,
68            chunk_file,
69            provisional_index_file,
70            core_index_file,
71            transaction,
72            merkle_block,
73            block_header,
74        }
75    }
76}
77
78/// Trustchain Verifier implementation via the ION DID method.
79pub struct TrustchainVerifier<T, U = FullClient>
80where
81    T: Sync + Send + DIDResolver,
82{
83    // TODO: consider replacing resolver with single generic over TrustchainResolver
84    resolver: HTTPTrustchainResolver<T, U>,
85    rpc_client: Option<bitcoincore_rpc::Client>,
86    ipfs_client: Option<IpfsClient>,
87    bundles: Mutex<HashMap<String, Arc<VerificationBundle>>>,
88    endpoint: Option<URL>,
89    _marker: PhantomData<U>,
90}
91
92impl<T> TrustchainVerifier<T, FullClient>
93where
94    T: Send + Sync + DIDResolver,
95{
96    /// Constructs a new TrustchainVerifier.
97    // TODO: refactor to use config struct over direct config file lookup
98    pub fn new(resolver: HTTPTrustchainResolver<T>) -> Self {
99        // Construct a Bitcoin RPC client to communicate with the ION Bitcoin node.
100        let rpc_client = bitcoincore_rpc::Client::new(
101            &ion_config().bitcoin_connection_string,
102            bitcoincore_rpc::Auth::UserPass(
103                ion_config().bitcoin_rpc_username.clone(),
104                ion_config().bitcoin_rpc_password.clone(),
105            ),
106        )
107        // Safe to use unwrap() here, as Client::new can only return Err when using cookie authentication.
108        .unwrap();
109
110        // This client must be configured to connect to the endpoint
111        // specified as "ipfsHttpApiEndpointUri" in the ION config file
112        // named "testnet-core-config.json" (or "mainnet-core-config.json").
113        // Similar for the MongoDB client.
114        // TODO: add customisable endpoint configuration to `trustchain_config.toml`
115        let ipfs_client = IpfsClient::default();
116        let bundles = Mutex::new(HashMap::new());
117        Self {
118            resolver,
119            rpc_client: Some(rpc_client),
120            ipfs_client: Some(ipfs_client),
121            bundles,
122            endpoint: None,
123            _marker: PhantomData,
124        }
125    }
126
127    /// Gets RPC client.
128    fn rpc_client(&self) -> &bitcoincore_rpc::Client {
129        self.rpc_client.as_ref().unwrap()
130    }
131
132    /// Gets IPFS client.
133    fn ipfs_client(&self) -> &IpfsClient {
134        self.ipfs_client.as_ref().unwrap()
135    }
136
137    /// Fetches the data needed to verify the DID's timestamp and stores it as a verification bundle.
138    // TODO: offline functionality will require interfacing with a persistent cache instead of the
139    // in-memory verifier HashMap.
140    pub async fn fetch_bundle(&self, did: &str) -> Result<(), VerifierError> {
141        let (did_doc, did_doc_meta) = self.resolve_did(did).await?;
142        let (block_hash, tx_index) = locate_transaction(did, self.rpc_client()).await?;
143        let tx = self.fetch_transaction(&block_hash, tx_index)?;
144        let transaction = bitcoin::util::psbt::serialize::Serialize::serialize(&tx);
145        let cid = self.op_return_cid(&tx)?;
146        let core_index_file = self.fetch_core_index_file(&cid).await?;
147        let provisional_index_file = self.fetch_prov_index_file(&core_index_file).await?;
148        let chunk_file = self.fetch_chunk_file(&provisional_index_file).await?;
149        let merkle_block = self.fetch_merkle_block(&block_hash, &tx)?;
150        let block_header = self.fetch_block_header(&block_hash)?;
151        // TODO: Consider extracting the block header (bytes) from the MerkleBlock to avoid one RPC call.
152        let bundle = VerificationBundle::new(
153            did_doc,
154            did_doc_meta,
155            chunk_file,
156            provisional_index_file,
157            core_index_file,
158            transaction,
159            merkle_block,
160            block_header,
161        );
162        // Insert the bundle into the HashMap of bundles, keyed by the DID.
163        self.bundles
164            .lock()
165            .unwrap()
166            .insert(did.to_string(), Arc::new(bundle));
167        Ok(())
168    }
169
170    fn fetch_transaction(
171        &self,
172        block_hash: &BlockHash,
173        tx_index: u32,
174    ) -> Result<Transaction, VerifierError> {
175        transaction(block_hash, tx_index, Some(self.rpc_client())).map_err(|e| {
176            VerifierError::ErrorFetchingVerificationMaterial(
177                "Failed to fetch transaction.".to_string(),
178                e.into(),
179            )
180        })
181    }
182
183    async fn fetch_core_index_file(&self, cid: &str) -> Result<Vec<u8>, VerifierError> {
184        query_ipfs(cid, self.ipfs_client())
185            .map_err(|e| {
186                VerifierError::ErrorFetchingVerificationMaterial(
187                    "Failed to fetch core index file".to_string(),
188                    e.into(),
189                )
190            })
191            .await
192    }
193
194    async fn fetch_prov_index_file(
195        &self,
196        core_index_file: &[u8],
197    ) -> Result<Vec<u8>, VerifierError> {
198        let content = decode_ipfs_content(core_index_file, true).map_err(|e| {
199            VerifierError::FailureToFetchVerificationMaterial(format!(
200                "Failed to decode ION core index file: {}",
201                e
202            ))
203        })?;
204        let provisional_index_file_uri = serde_json::from_value::<CoreIndexFile>(content.clone())?
205            .provisional_index_file_uri
206            .ok_or(VerifierError::FailureToFetchVerificationMaterial(format!(
207                "Missing provisional index file URI in core index file: {content}."
208            )))?;
209        query_ipfs(&provisional_index_file_uri, self.ipfs_client())
210            .map_err(|e| {
211                VerifierError::ErrorFetchingVerificationMaterial(
212                    "Failed to fetch ION provisional index file.".to_string(),
213                    e.into(),
214                )
215            })
216            .await
217    }
218
219    async fn fetch_chunk_file(&self, prov_index_file: &[u8]) -> Result<Vec<u8>, VerifierError> {
220        let content = decode_ipfs_content(prov_index_file, true).map_err(|err| {
221            VerifierError::ErrorFetchingVerificationMaterial(
222                "Failed to decode ION provisional index file".to_string(),
223                err.into(),
224            )
225        })?;
226
227        // // Look inside the "chunks" element.
228        let prov_index_file: ProvisionalIndexFile =
229            serde_json::from_value(content).map_err(|err| {
230                VerifierError::ErrorFetchingVerificationMaterial(
231                    "Failed to parse ION provisional index file.".to_string(),
232                    err.into(),
233                )
234            })?;
235
236        // In the current version of the Sidetree protocol, a single chunk entry must be present in
237        // the chunks array (see https://identity.foundation/sidetree/spec/#provisional-index-file).
238        // So here we only need to consider the first entry in the content. This may need to be
239        // updated in future to accommodate changes to the Sidetre protocol.
240        let chunk_file_uri = match prov_index_file.chunks.as_deref() {
241            Some([ChunkFileUri { chunk_file_uri }]) => chunk_file_uri,
242            _ => return Err(VerifierError::FailureToGetDIDContent("".to_string())),
243        };
244
245        // Get Chunk File
246        query_ipfs(chunk_file_uri, self.ipfs_client())
247            .map_err(|err| {
248                VerifierError::ErrorFetchingVerificationMaterial(
249                    "Failed to fetch ION provisional index file.".to_string(),
250                    err.into(),
251                )
252            })
253            .await
254    }
255
256    /// Fetches a Merkle proof directly from a Bitcoin node.
257    fn fetch_merkle_block(
258        &self,
259        block_hash: &BlockHash,
260        tx: &Transaction,
261    ) -> Result<Vec<u8>, VerifierError> {
262        self.rpc_client()
263            .get_tx_out_proof(&[tx.txid()], Some(block_hash))
264            .map_err(|e| {
265                VerifierError::ErrorFetchingVerificationMaterial(
266                    "Failed to fetch Merkle proof via RPC.".to_string(),
267                    e.into(),
268                )
269            })
270    }
271
272    fn fetch_block_header(&self, block_hash: &BlockHash) -> Result<Vec<u8>, VerifierError> {
273        block_header(block_hash, Some(self.rpc_client()))
274            .map_err(|e| {
275                VerifierError::ErrorFetchingVerificationMaterial(
276                    "Failed to fetch Bitcoin block header via RPC.".to_string(),
277                    e.into(),
278                )
279            })
280            .map(|block_header| bitcoin::consensus::serialize(&block_header))
281    }
282
283    /// Gets a DID verification bundle, including a fetch if not initially cached.
284    pub async fn verification_bundle(
285        &self,
286        did: &str,
287    ) -> Result<Arc<VerificationBundle>, VerifierError> {
288        // Fetch (and store) the bundle if it isn't already available.
289        if !self.bundles.lock().unwrap().contains_key(did) {
290            self.fetch_bundle(did).await?;
291        }
292        Ok(self.bundles.lock().unwrap().get(did).cloned().unwrap())
293    }
294    /// Resolves the given DID to obtain the DID Document and Document Metadata.
295    async fn resolve_did(&self, did: &str) -> Result<(Document, DocumentMetadata), VerifierError> {
296        let (res_meta, doc, doc_meta) = self.resolver.resolve_as_result(did).await?;
297        if let (Some(doc), Some(doc_meta)) = (doc, doc_meta) {
298            Ok((doc, doc_meta))
299        } else {
300            Err(VerifierError::DIDResolutionError(
301                format!("Missing Document and/or DocumentMetadata for DID: {}", did),
302                ResolverError::FailureWithMetadata(res_meta).into(),
303            ))
304        }
305    }
306}
307impl<T> TrustchainVerifier<T, LightClient>
308where
309    T: Send + Sync + DIDResolver,
310{
311    /// Constructs a new TrustchainVerifier.
312    // TODO: consider refactor to remove resolver from API
313    pub fn with_endpoint(resolver: HTTPTrustchainResolver<T, LightClient>, endpoint: URL) -> Self {
314        Self {
315            resolver,
316            rpc_client: None,
317            ipfs_client: None,
318            bundles: Mutex::new(HashMap::new()),
319            endpoint: Some(endpoint),
320            _marker: PhantomData,
321        }
322    }
323    /// Gets endpoint of verifier.
324    fn endpoint(&self) -> &str {
325        self.endpoint.as_ref().unwrap()
326    }
327    /// Fetches the data needed to verify the DID's timestamp and stores it as a verification bundle.
328    // TODO: offline functionality will require interfacing with a persistent cache instead of the
329    // in-memory verifier HashMap.
330    // If running on a Trustchain light client, make an API call to a full node to request the bundle.
331    pub async fn fetch_bundle(&self, did: &str) -> Result<(), VerifierError> {
332        let response = reqwest::get(format!("{}did/bundle/{did}", self.endpoint()))
333            .await
334            .map_err(|e| {
335                VerifierError::ErrorFetchingVerificationMaterial(
336                    format!("Error requesting bundle from endpoint: {}", self.endpoint()),
337                    e.into(),
338                )
339            })?;
340        let bundle: VerificationBundle = serde_json::from_str(
341            &response
342                .text()
343                .map_err(|e| {
344                    VerifierError::ErrorFetchingVerificationMaterial(
345                        format!(
346                            "Error extracting bundle response body from endpoint: {}",
347                            self.endpoint()
348                        ),
349                        e.into(),
350                    )
351                })
352                .await?,
353        )?;
354        // Insert the bundle into the HashMap of bundles, keyed by the DID.
355        self.bundles
356            .lock()
357            .unwrap()
358            .insert(did.to_string(), Arc::new(bundle));
359        Ok(())
360    }
361
362    /// Gets a DID verification bundle, including a fetch if not initially cached.
363    pub async fn verification_bundle(
364        &self,
365        did: &str,
366    ) -> Result<Arc<VerificationBundle>, VerifierError> {
367        // Fetch (and store) the bundle if it isn't already available.
368        if !self.bundles.lock().unwrap().contains_key(did) {
369            self.fetch_bundle(did).await?;
370        }
371        Ok(self.bundles.lock().unwrap().get(did).cloned().unwrap())
372    }
373}
374
375impl<T, U> TrustchainVerifier<T, U>
376where
377    T: Send + Sync + DIDResolver,
378{
379    /// Extracts the IPFS content identifier from the ION OP_RETURN data inside a Bitcoin transaction.
380    fn op_return_cid(&self, tx: &Transaction) -> Result<String, VerifierError> {
381        tx_to_op_return_cid(tx)
382    }
383}
384
385/// Converts a VerificationBundle into an IONCommitment.
386pub fn construct_commitment(
387    bundle: Arc<VerificationBundle>,
388) -> Result<IONCommitment, CommitmentError> {
389    IONCommitment::new(
390        bundle.did_doc.clone(),
391        bundle.chunk_file.clone(),
392        bundle.provisional_index_file.clone(),
393        bundle.core_index_file.clone(),
394        bundle.transaction.clone(),
395        bundle.merkle_block.clone(),
396        bundle.block_header.clone(),
397    )
398}
399
400/// Converts DID content from a chunk file into a vector of Delta objects.
401pub fn content_deltas(chunk_file_json: &Value) -> Result<Vec<Delta>, VerifierError> {
402    let chunk_file: ChunkFile =
403        serde_json::from_value(chunk_file_json.to_owned()).map_err(|_| {
404            VerifierError::FailureToParseDIDContent(format!(
405                "Failed to parse chunk file: {}",
406                chunk_file_json
407            ))
408        })?;
409    Ok(chunk_file.deltas)
410}
411
412// TODO: consider whether duplication can be avoided in the LightClient impl
413#[async_trait]
414impl<T> Verifier<T> for TrustchainVerifier<T, FullClient>
415where
416    T: Sync + Send + DIDResolver,
417{
418    fn validate_pow_hash(&self, hash: &str) -> Result<(), VerifierError> {
419        let block_hash = BlockHash::from_str(hash)
420            .map_err(|_| VerifierError::InvalidProofOfWorkHash(hash.to_string()))?;
421        let _block_header = block_header(&block_hash, Some(self.rpc_client()))
422            .map_err(|_| VerifierError::FailureToGetBlockHeader(hash.to_string()))?;
423        Ok(())
424    }
425
426    async fn did_commitment(&self, did: &str) -> Result<Box<dyn DIDCommitment>, VerifierError> {
427        let bundle = self.verification_bundle(did).await?;
428        Ok(construct_commitment(bundle).map(Box::new)?)
429    }
430
431    fn resolver(&self) -> &dyn TrustchainResolver {
432        &self.resolver
433    }
434
435    async fn verifiable_timestamp(
436        &self,
437        did: &str,
438        expected_timestamp: Timestamp,
439    ) -> Result<Box<dyn VerifiableTimestamp>, VerifierError> {
440        let did_commitment = self.did_commitment(did).await?;
441        // Downcast to IONCommitment to extract data for constructing a TimestampCommitment.
442        let ion_commitment = did_commitment
443            .as_any()
444            .downcast_ref::<IONCommitment>()
445            .unwrap(); // Safe because IONCommitment implements DIDCommitment.
446        let timestamp_commitment = Box::new(BlockTimestampCommitment::new(
447            ion_commitment
448                .chained_commitment()
449                .commitments()
450                .last()
451                .expect("Unexpected empty commitment chain.")
452                .candidate_data()
453                .to_owned(),
454            expected_timestamp,
455        )?);
456        Ok(Box::new(IONTimestamp::new(
457            did_commitment,
458            timestamp_commitment,
459        )))
460    }
461}
462
463#[async_trait]
464impl<T> Verifier<T> for TrustchainVerifier<T, LightClient>
465where
466    T: Sync + Send + DIDResolver,
467{
468    fn validate_pow_hash(&self, hash: &str) -> Result<(), VerifierError> {
469        // Check the PoW difficulty of the hash against the configured minimum threshold.
470        // TODO: update Cargo.toml to use version 0.30.0+ of the bitcoin Rust library
471        // and specify a minimum work/target in the Trustchain client config, see:
472        // https://docs.rs/bitcoin/0.30.0/src/bitcoin/pow.rs.html#72-78
473        // In the meantime, just check for a minimum number of leading zeros in the hash.
474        if hash.chars().take_while(|&c| c == '0').count() < crate::MIN_POW_ZEROS {
475            return Err(VerifierError::InvalidProofOfWorkHash(format!(
476                "{}, only has {} zeros but MIN_POW_ZEROS is {}",
477                hash,
478                hash.chars().take_while(|&c| c == '0').count(),
479                crate::MIN_POW_ZEROS
480            )));
481        }
482
483        // If the PoW difficulty is satisfied, accept the timestamp in the DID commitment.
484        Ok(())
485    }
486
487    async fn did_commitment(&self, did: &str) -> Result<Box<dyn DIDCommitment>, VerifierError> {
488        let bundle = self.verification_bundle(did).await?;
489        Ok(construct_commitment(bundle).map(Box::new)?)
490    }
491
492    fn resolver(&self) -> &dyn TrustchainResolver {
493        &self.resolver
494    }
495
496    async fn verifiable_timestamp(
497        &self,
498        did: &str,
499        expected_timestamp: Timestamp,
500    ) -> Result<Box<dyn VerifiableTimestamp>, VerifierError> {
501        let did_commitment = self.did_commitment(did).await?;
502        // Downcast to IONCommitment to extract data for constructing a TimestampCommitment.
503        let ion_commitment = did_commitment
504            .as_any()
505            .downcast_ref::<IONCommitment>()
506            .unwrap(); // Safe because IONCommitment implements DIDCommitment.
507        let timestamp_commitment = Box::new(BlockTimestampCommitment::new(
508            ion_commitment
509                .chained_commitment()
510                .commitments()
511                .last()
512                .expect("Unexpected empty commitment chain.")
513                .candidate_data()
514                .to_owned(),
515            expected_timestamp,
516        )?);
517        Ok(Box::new(IONTimestamp::new(
518            did_commitment,
519            timestamp_commitment,
520        )))
521    }
522}
523
524/// Contains the corresponding `DIDCommitment` and `TimestampCommitment` for a given DID.
525pub struct IONTimestamp {
526    did_commitment: Box<dyn DIDCommitment>,
527    timestamp_commitment: Box<dyn TimestampCommitment>,
528}
529
530impl IONTimestamp {
531    fn new(
532        did_commitment: Box<dyn DIDCommitment>,
533        timestamp_commitment: Box<dyn TimestampCommitment>,
534    ) -> Self {
535        Self {
536            did_commitment,
537            timestamp_commitment,
538        }
539    }
540
541    /// Gets the DID.
542    pub fn did(&self) -> &str {
543        self.did_commitment.did()
544    }
545    /// Gets the DID Document.
546    pub fn did_document(&self) -> &Document {
547        self.did_commitment.did_document()
548    }
549}
550
551impl VerifiableTimestamp for IONTimestamp {
552    fn did_commitment(&self) -> &dyn DIDCommitment {
553        self.did_commitment.as_ref()
554    }
555
556    fn timestamp_commitment(&self) -> &dyn TimestampCommitment {
557        self.timestamp_commitment.as_ref()
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use crate::{
565        data::{
566            TEST_BLOCK_HEADER_HEX, TEST_CHUNK_FILE_HEX, TEST_CORE_INDEX_FILE_HEX,
567            TEST_MERKLE_BLOCK_HEX, TEST_PROVISIONAL_INDEX_FILE_HEX, TEST_TRANSACTION_HEX,
568        },
569        trustchain_resolver,
570    };
571    use bitcoin::{BlockHeader, MerkleBlock};
572    use flate2::read::GzDecoder;
573    use std::{io::Read, str::FromStr};
574    use trustchain_core::commitment::TrivialCommitment;
575
576    const ENDPOINT: &str = "http://localhost:3000/";
577
578    #[test]
579    #[ignore = "Integration test requires Bitcoin RPC"]
580    fn test_op_return_cid() {
581        let resolver = trustchain_resolver(ENDPOINT);
582        let target = TrustchainVerifier::new(resolver);
583
584        // The transaction, including OP_RETURN data, can be found on-chain:
585        // https://blockstream.info/testnet/tx/9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c
586        let expected = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
587
588        // Block 2377445.
589        let block_hash =
590            BlockHash::from_str("000000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f")
591                .unwrap();
592        let tx_index = 3;
593        let tx = transaction(&block_hash, tx_index, Some(target.rpc_client())).unwrap();
594
595        let actual = target.op_return_cid(&tx).unwrap();
596        assert_eq!(expected, actual);
597    }
598
599    #[tokio::test]
600    #[ignore = "Integration test requires ION"]
601    async fn test_resolve_did() {
602        // Use a SidetreeClient for the resolver in this case, as we need to resolve a DID.
603        let resolver = trustchain_resolver(ENDPOINT);
604        let target = TrustchainVerifier::new(resolver);
605        let did = "did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg";
606        let result = target.resolve_did(did).await;
607        assert!(result.is_ok());
608    }
609
610    #[tokio::test]
611    #[ignore = "Integration test requires IPFS"]
612    async fn test_fetch_chunk_file() {
613        let resolver = trustchain_resolver(ENDPOINT);
614        let target = TrustchainVerifier::new(resolver);
615
616        let prov_index_file = hex::decode(TEST_PROVISIONAL_INDEX_FILE_HEX).unwrap();
617
618        let result = target.fetch_chunk_file(&prov_index_file).await;
619        assert!(result.is_ok());
620        let chunk_file_bytes = result.unwrap();
621
622        let mut decoder = GzDecoder::new(&*chunk_file_bytes);
623        let mut ipfs_content_str = String::new();
624        let value: serde_json::Value = match decoder.read_to_string(&mut ipfs_content_str) {
625            Ok(_) => serde_json::from_str(&ipfs_content_str).unwrap(),
626            Err(_) => panic!(),
627        };
628        assert!(value.is_object());
629        assert!(value.as_object().unwrap().contains_key("deltas"));
630    }
631
632    #[tokio::test]
633    #[ignore = "Integration test requires IPFS"]
634    async fn test_fetch_core_index_file() {
635        let resolver = trustchain_resolver(ENDPOINT);
636        let target = TrustchainVerifier::new(resolver);
637
638        let cid = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
639        let result = target.fetch_core_index_file(cid).await;
640        assert!(result.is_ok());
641        let core_index_file_bytes = result.unwrap();
642
643        let mut decoder = GzDecoder::new(&*core_index_file_bytes);
644        let mut ipfs_content_str = String::new();
645        let value: serde_json::Value = match decoder.read_to_string(&mut ipfs_content_str) {
646            Ok(_) => serde_json::from_str(&ipfs_content_str).unwrap(),
647            Err(_) => panic!(),
648        };
649        assert!(value.is_object());
650        assert!(value
651            .as_object()
652            .unwrap()
653            .contains_key("provisionalIndexFileUri"));
654    }
655
656    #[tokio::test]
657    #[ignore = "Integration test requires ION, MongoDB, IPFS and Bitcoin RPC"]
658    async fn test_fetch_bundle() {
659        // Use a SidetreeClient for the resolver in this case, as we need to resolve a DID.
660        let resolver = trustchain_resolver(ENDPOINT);
661        let target = TrustchainVerifier::new(resolver);
662
663        assert!(target.bundles.lock().unwrap().is_empty());
664        let did = "did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg";
665        target.fetch_bundle(did).await.unwrap();
666
667        assert!(!target.bundles.lock().unwrap().is_empty());
668        assert_eq!(target.bundles.lock().unwrap().len(), 1);
669        assert!(target.bundles.lock().unwrap().contains_key(did));
670    }
671
672    #[tokio::test]
673    #[ignore = "Integration test requires ION, MongoDB, IPFS and Bitcoin RPC"]
674    async fn test_commitment() {
675        // Use a SidetreeClient for the resolver in this case, as we need to resolve a DID.
676        let resolver = trustchain_resolver(ENDPOINT);
677        let target = TrustchainVerifier::new(resolver);
678
679        let did = "did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg";
680
681        assert!(target.bundles.lock().unwrap().is_empty());
682        let result = target.did_commitment(did).await.unwrap();
683
684        // Check that the verification bundle for the commitment is now stored in the Verifier.
685        assert!(!target.bundles.lock().unwrap().is_empty());
686        assert_eq!(target.bundles.lock().unwrap().len(), 1);
687        let bundle = target.bundles.lock().unwrap().get(did).cloned().unwrap();
688        let commitment = construct_commitment(bundle).unwrap();
689        assert_eq!(result.hash().unwrap(), commitment.hash().unwrap());
690    }
691
692    #[test]
693    fn test_chunk_file_deserialize() {
694        let bytes = hex::decode(TEST_CHUNK_FILE_HEX).unwrap();
695        let mut decoder = GzDecoder::new(&*bytes);
696        let mut ipfs_content_str = String::new();
697        let value: serde_json::Value = match decoder.read_to_string(&mut ipfs_content_str) {
698            Ok(_) => serde_json::from_str(&ipfs_content_str).unwrap(),
699            Err(_) => panic!(),
700        };
701        assert!(value.is_object());
702        assert!(value.as_object().unwrap().contains_key("deltas"));
703    }
704
705    #[test]
706    fn test_prov_index_file_deserialize() {
707        let bytes = hex::decode(TEST_PROVISIONAL_INDEX_FILE_HEX).unwrap();
708        let mut decoder = GzDecoder::new(&*bytes);
709        let mut ipfs_content_str = String::new();
710        let value: serde_json::Value = match decoder.read_to_string(&mut ipfs_content_str) {
711            Ok(_) => serde_json::from_str(&ipfs_content_str).unwrap(),
712            Err(_) => panic!(),
713        };
714        assert!(value.is_object());
715        assert!(value.as_object().unwrap().contains_key("chunks"));
716    }
717
718    #[test]
719    fn test_core_index_file_deserialize() {
720        let bytes = hex::decode(TEST_CORE_INDEX_FILE_HEX).unwrap();
721        let mut decoder = GzDecoder::new(&*bytes);
722        let mut ipfs_content_str = String::new();
723        let value: serde_json::Value = match decoder.read_to_string(&mut ipfs_content_str) {
724            Ok(_) => serde_json::from_str(&ipfs_content_str).unwrap(),
725            Err(_) => panic!(),
726        };
727        assert!(value.is_object());
728        assert!(value
729            .as_object()
730            .unwrap()
731            .contains_key("provisionalIndexFileUri"));
732    }
733
734    #[test]
735    fn test_tx_deserialize() {
736        let bytes = hex::decode(TEST_TRANSACTION_HEX).unwrap();
737        let tx: Transaction =
738            bitcoin::util::psbt::serialize::Deserialize::deserialize(&bytes).unwrap();
739        let expected_txid = "9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c";
740        assert_eq!(tx.txid().to_string(), expected_txid);
741    }
742
743    #[test]
744    fn test_merkle_block_deserialize() {
745        let bytes = hex::decode(TEST_MERKLE_BLOCK_HEX).unwrap();
746        let merkle_block: MerkleBlock = bitcoin::consensus::deserialize(&bytes).unwrap();
747        let header = merkle_block.header;
748        let expected_merkle_root =
749            "7dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69";
750        assert_eq!(header.merkle_root.to_string(), expected_merkle_root);
751    }
752
753    #[test]
754    fn test_block_header_deserialize() {
755        let bytes = hex::decode(TEST_BLOCK_HEADER_HEX).unwrap();
756        let header: BlockHeader = bitcoin::consensus::deserialize(&bytes).unwrap();
757        let expected_merkle_root =
758            "7dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69";
759        assert_eq!(header.merkle_root.to_string(), expected_merkle_root);
760    }
761}