trustchain_ion/
commitment.rs

1//! Implementation of `Commitment` API for ION DID method.
2use bitcoin::util::psbt::serialize::Deserialize;
3use bitcoin::MerkleBlock;
4use bitcoin::Transaction;
5use ipfs_hasher::IpfsHasher;
6use serde_json::{json, Value};
7use sha2::{Digest, Sha256};
8use ssi::did::Document;
9use std::convert::TryInto;
10use std::marker::PhantomData;
11use trustchain_core::commitment::TimestampCommitment;
12use trustchain_core::commitment::{ChainedCommitment, CommitmentChain, CommitmentResult};
13use trustchain_core::commitment::{Commitment, CommitmentError};
14use trustchain_core::commitment::{DIDCommitment, TrivialCommitment};
15use trustchain_core::utils::{HasEndpoints, HasKeys};
16use trustchain_core::verifier::Timestamp;
17
18use crate::sidetree::CoreIndexFile;
19use crate::utils::tx_to_op_return_cid;
20use crate::utils::{decode_block_header, decode_ipfs_content, reverse_endianness};
21use crate::MERKLE_ROOT_KEY;
22use crate::TIMESTAMP_KEY;
23
24const CID_KEY: &str = "cid";
25const DELTAS_KEY: &str = "deltas";
26
27fn ipfs_hasher() -> fn(&[u8]) -> CommitmentResult<String> {
28    |x| Ok(IpfsHasher::default().compute(x))
29}
30
31fn ipfs_decode_candidate_data() -> fn(&[u8]) -> CommitmentResult<Value> {
32    |x| decode_ipfs_content(x, true).map_err(|_| CommitmentError::DataDecodingFailure)
33}
34
35fn block_header_hasher() -> fn(&[u8]) -> CommitmentResult<String> {
36    // Candidate data the block header bytes.
37    |x| {
38        // Bitcoin block hash is a double SHA256 hash of the block header.
39        // We use a generic sha2 crate to avoid trust in rust-bitcoin.
40        let double_hash_hex = hex::encode(Sha256::digest(Sha256::digest(x)));
41        // For leading (not trailing) zeros, convert the hex to big-endian.
42        Ok(reverse_endianness(&double_hash_hex).unwrap())
43    }
44}
45
46fn block_header_decoder() -> fn(&[u8]) -> CommitmentResult<Value> {
47    |x| {
48        if x.len() != 80 {
49            return Err(CommitmentError::DataDecodingError(
50                "Error: Bitcoin block header must be 80 bytes.".to_string(),
51            ));
52        };
53        let decoded_header = decode_block_header(x.try_into().map_err(|err| {
54            CommitmentError::DataDecodingError(format!(
55                "Error: Bitcoin block header must be 80 bytes with error: {err}"
56            ))
57        })?);
58
59        match decoded_header {
60            Ok(x) => Ok(x),
61            Err(e) => Err(CommitmentError::DataDecodingError(format!(
62                "Error decoding Bitcoin block header: {}.",
63                e
64            ))),
65        }
66    }
67}
68
69/// Unit struct for incomplete commitments.
70pub struct Incomplete;
71/// Unit struct for complete commitments.
72pub struct Complete;
73
74/// A Commitment whose hash is an IPFS content identifier (CID) for an ION Index file.
75pub struct IpfsIndexFileCommitment<T = Incomplete> {
76    candidate_data: Vec<u8>,
77    expected_data: Option<Value>,
78    _state: PhantomData<T>, // Dummy field for type marker
79}
80
81impl IpfsIndexFileCommitment<Incomplete> {
82    pub fn new(candidate_data: Vec<u8>) -> Self {
83        Self {
84            candidate_data,
85            expected_data: None,
86            _state: PhantomData::<Incomplete>,
87        }
88    }
89}
90
91impl IpfsIndexFileCommitment<Complete> {
92    pub fn new(candidate_data: Vec<u8>, expected_data: Value) -> Self {
93        Self {
94            candidate_data,
95            expected_data: Some(expected_data),
96            _state: PhantomData::<Complete>,
97        }
98    }
99}
100
101impl<T> TrivialCommitment for IpfsIndexFileCommitment<T> {
102    fn hasher(&self) -> fn(&[u8]) -> CommitmentResult<String> {
103        ipfs_hasher()
104    }
105
106    fn candidate_data(&self) -> &[u8] {
107        &self.candidate_data
108    }
109
110    fn decode_candidate_data(&self) -> fn(&[u8]) -> CommitmentResult<Value> {
111        ipfs_decode_candidate_data()
112    }
113
114    fn to_commitment(self: Box<Self>, expected_data: serde_json::Value) -> Box<dyn Commitment> {
115        Box::new(IpfsIndexFileCommitment::<Complete>::new(
116            self.candidate_data,
117            expected_data,
118        ))
119    }
120}
121
122impl Commitment for IpfsIndexFileCommitment<Complete> {
123    fn expected_data(&self) -> &serde_json::Value {
124        // Safe to unwrap as a complete commitment must have expected data
125        self.expected_data.as_ref().unwrap()
126    }
127}
128
129/// A Commitment whose hash is an IPFS content identifier (CID) for an ION chunk file.
130pub struct IpfsChunkFileCommitment<T = Incomplete> {
131    candidate_data: Vec<u8>,
132    delta_index: usize,
133    expected_data: Option<Value>,
134    _state: PhantomData<T>, // Dummy field for type marker
135}
136impl IpfsChunkFileCommitment<Incomplete> {
137    pub fn new(candidate_data: Vec<u8>, delta_index: usize) -> Self {
138        Self {
139            candidate_data,
140            delta_index,
141            expected_data: None,
142            _state: PhantomData::<Incomplete>,
143        }
144    }
145}
146impl IpfsChunkFileCommitment<Complete> {
147    pub fn new(candidate_data: Vec<u8>, delta_index: usize, expected_data: Value) -> Self {
148        Self {
149            candidate_data,
150            delta_index,
151            expected_data: Some(expected_data),
152            _state: PhantomData::<Complete>,
153        }
154    }
155}
156
157impl<T> TrivialCommitment for IpfsChunkFileCommitment<T> {
158    fn hasher(&self) -> fn(&[u8]) -> CommitmentResult<String> {
159        ipfs_hasher()
160    }
161
162    fn candidate_data(&self) -> &[u8] {
163        &self.candidate_data
164    }
165
166    fn filter(&self) -> Option<Box<dyn Fn(&serde_json::Value) -> CommitmentResult<Value>>> {
167        // Ignore all of the deltas in the chunk file except the one at index delta_index
168        // (which is the one corresponding to the relevant DID).
169        let delta_index = self.delta_index;
170        Some(Box::new(move |value| {
171            // Note: check if mix of create, recover and deactivate whether the correct index is used.
172            // TODO: Remove in future releases.
173            if let Value::Object(map) = value {
174                match map.get(DELTAS_KEY) {
175                    Some(Value::Array(deltas)) => Ok(deltas.get(delta_index).unwrap().clone()),
176                    _ => Err(CommitmentError::DataDecodingFailure),
177                }
178            } else {
179                Err(CommitmentError::DataDecodingFailure)
180            }
181        }))
182    }
183    fn decode_candidate_data(&self) -> fn(&[u8]) -> CommitmentResult<Value> {
184        ipfs_decode_candidate_data()
185    }
186    fn to_commitment(self: Box<Self>, expected_data: serde_json::Value) -> Box<dyn Commitment> {
187        Box::new(IpfsChunkFileCommitment::<Complete>::new(
188            self.candidate_data,
189            self.delta_index,
190            expected_data,
191        ))
192    }
193}
194
195impl Commitment for IpfsChunkFileCommitment<Complete> {
196    fn expected_data(&self) -> &serde_json::Value {
197        // Safe to unwrap as a complete commitment must have expected data
198        self.expected_data.as_ref().unwrap()
199    }
200}
201
202/// A Commitment whose hash is a Bitcoin transaction ID.
203pub struct TxCommitment<T = Incomplete> {
204    candidate_data: Vec<u8>,
205    expected_data: Option<Value>,
206    _state: PhantomData<T>, // Dummy field for type marker
207}
208
209impl TxCommitment<Incomplete> {
210    pub fn new(candidate_data: Vec<u8>) -> Self {
211        Self {
212            candidate_data,
213            expected_data: None,
214            _state: PhantomData::<Incomplete>,
215        }
216    }
217}
218
219impl TxCommitment<Complete> {
220    pub fn new(candidate_data: Vec<u8>, expected_data: Value) -> Self {
221        Self {
222            candidate_data,
223            expected_data: Some(expected_data),
224            _state: PhantomData::<Complete>,
225        }
226    }
227}
228
229impl<T> TrivialCommitment for TxCommitment<T> {
230    fn hasher(&self) -> fn(&[u8]) -> CommitmentResult<String> {
231        // Candidate data is a Bitcoin transaction, whose hash is the transaction ID.
232        |x| {
233            let tx: Transaction = match Deserialize::deserialize(x) {
234                Ok(tx) => tx,
235                Err(e) => {
236                    return Err(CommitmentError::FailedToComputeHash(format!(
237                        "Failed to deserialize transaction: {}",
238                        e
239                    )));
240                }
241            };
242            Ok(tx.txid().to_string())
243        }
244    }
245
246    fn candidate_data(&self) -> &[u8] {
247        &self.candidate_data
248    }
249
250    /// Deserializes the candidate data into a Bitcoin transaction, then
251    /// extracts and returns the IPFS content identifier in the OP_RETURN data.
252    fn decode_candidate_data(&self) -> fn(&[u8]) -> CommitmentResult<Value> {
253        |x| {
254            // Deserialize the transaction from the candidate data.
255            let tx: Transaction = match Deserialize::deserialize(x) {
256                Ok(tx) => tx,
257                Err(e) => {
258                    return Err(CommitmentError::DataDecodingError(format!(
259                        "Failed to deserialize transaction: {}",
260                        e
261                    )));
262                }
263            };
264            // // Extract the IPFS content identifier from the ION OP_RETURN data.
265            let cid = tx_to_op_return_cid(&tx)
266                .map_err(|e| CommitmentError::DataDecodingError(e.to_string()))?;
267            Ok(json!({ CID_KEY: cid }))
268        }
269    }
270    fn to_commitment(self: Box<Self>, expected_data: serde_json::Value) -> Box<dyn Commitment> {
271        Box::new(TxCommitment::<Complete>::new(
272            self.candidate_data,
273            expected_data,
274        ))
275    }
276}
277
278impl Commitment for TxCommitment<Complete> {
279    fn expected_data(&self) -> &serde_json::Value {
280        // Safe to unwrap as a complete commitment must have expected data
281        self.expected_data.as_ref().unwrap()
282    }
283}
284
285/// A Commitment whose hash is the root of a Merkle tree of Bitcoin transaction IDs.
286pub struct MerkleRootCommitment<T = Incomplete> {
287    candidate_data: Vec<u8>,
288    expected_data: Option<Value>,
289    _state: PhantomData<T>, // Dummy field for type marker
290}
291
292impl MerkleRootCommitment<Incomplete> {
293    pub fn new(candidate_data: Vec<u8>) -> Self {
294        Self {
295            candidate_data,
296            expected_data: None,
297            _state: PhantomData::<Incomplete>,
298        }
299    }
300}
301impl MerkleRootCommitment<Complete> {
302    pub fn new(candidate_data: Vec<u8>, expected_data: Value) -> Self {
303        Self {
304            candidate_data,
305            expected_data: Some(expected_data),
306            _state: PhantomData::<Complete>,
307        }
308    }
309}
310
311impl<T> TrivialCommitment for MerkleRootCommitment<T> {
312    fn hasher(&self) -> fn(&[u8]) -> CommitmentResult<String> {
313        // Candidate data is a Merkle proof containing a branch of transaction IDs.
314        |x| {
315            let merkle_block: MerkleBlock = match bitcoin::consensus::deserialize(x) {
316                Ok(mb) => mb,
317                Err(e) => {
318                    return Err(CommitmentError::FailedToComputeHash(format!(
319                        "Failed to deserialize MerkleBlock: {:?}",
320                        e
321                    )));
322                }
323            };
324            // Traverse the PartialMerkleTree to obtain the Merkle root.
325            match merkle_block.txn.extract_matches(&mut vec![], &mut vec![]) {
326                Ok(merkle_root) => Ok(merkle_root.to_string()),
327                Err(e) => Err(CommitmentError::FailedToComputeHash(format!(
328                    "Failed to obtain Merkle root from PartialMerkleTree: {:?}",
329                    e
330                ))),
331            }
332        }
333    }
334
335    fn candidate_data(&self) -> &[u8] {
336        &self.candidate_data
337    }
338
339    /// Deserializes the candidate data into a Merkle proof.
340    fn decode_candidate_data(&self) -> fn(&[u8]) -> CommitmentResult<Value> {
341        |x| {
342            let merkle_block: MerkleBlock = match bitcoin::consensus::deserialize(x) {
343                Ok(mb) => mb,
344                Err(e) => {
345                    return Err(CommitmentError::DataDecodingError(format!(
346                        "Failed to deserialize MerkleBlock: {:?}",
347                        e
348                    )));
349                }
350            };
351            // Get the hashes in the Merkle proof as a vector of strings.
352            let hashes_vec: Vec<String> = merkle_block
353                .txn
354                .hashes()
355                .iter()
356                .map(|x| x.to_string())
357                .collect();
358
359            // Convert to a JSON value.
360            Ok(serde_json::json!(hashes_vec))
361        }
362    }
363
364    fn to_commitment(self: Box<Self>, expected_data: serde_json::Value) -> Box<dyn Commitment> {
365        Box::new(MerkleRootCommitment::<Complete>::new(
366            self.candidate_data,
367            expected_data,
368        ))
369    }
370}
371
372impl Commitment for MerkleRootCommitment<Complete> {
373    fn expected_data(&self) -> &serde_json::Value {
374        // Safe to unwrap as a complete commitment must have expected data
375        self.expected_data.as_ref().unwrap()
376    }
377}
378
379/// A Commitment whose hash is the PoW hash of a Bitcoin block.
380pub struct BlockHashCommitment<T = Incomplete> {
381    candidate_data: Vec<u8>,
382    expected_data: Option<Value>,
383    _state: PhantomData<T>, // Dummy field for type marker
384}
385
386impl BlockHashCommitment<Incomplete> {
387    pub fn new(candidate_data: Vec<u8>) -> Self {
388        Self {
389            candidate_data,
390            expected_data: None,
391            _state: PhantomData::<Incomplete>,
392        }
393    }
394}
395
396impl BlockHashCommitment<Complete> {
397    pub fn new(candidate_data: Vec<u8>, expected_data: Value) -> Self {
398        Self {
399            candidate_data,
400            expected_data: Some(expected_data),
401            _state: PhantomData::<Complete>,
402        }
403    }
404}
405
406impl<T> TrivialCommitment for BlockHashCommitment<T> {
407    fn hasher(&self) -> fn(&[u8]) -> CommitmentResult<String> {
408        block_header_hasher()
409    }
410
411    fn candidate_data(&self) -> &[u8] {
412        &self.candidate_data
413    }
414
415    /// Deserializes the candidate data into a Block header (as JSON).
416    fn decode_candidate_data(&self) -> fn(&[u8]) -> CommitmentResult<Value> {
417        block_header_decoder()
418    }
419
420    /// Override the filter method to ensure only the Merkle root content is considered.
421    fn filter(&self) -> Option<Box<dyn Fn(&serde_json::Value) -> CommitmentResult<Value>>> {
422        Some(Box::new(move |value| {
423            if let Value::Object(map) = value {
424                match map.get(MERKLE_ROOT_KEY) {
425                    Some(Value::String(str)) => Ok(Value::String(str.clone())),
426                    _ => Err(CommitmentError::DataDecodingFailure),
427                }
428            } else {
429                Err(CommitmentError::DataDecodingFailure)
430            }
431        }))
432    }
433
434    fn to_commitment(self: Box<Self>, expected_data: serde_json::Value) -> Box<dyn Commitment> {
435        Box::new(BlockHashCommitment::<Complete>::new(
436            self.candidate_data,
437            expected_data,
438        ))
439    }
440}
441
442impl Commitment for BlockHashCommitment<Complete> {
443    fn expected_data(&self) -> &serde_json::Value {
444        // Safe to unwrap as a complete commitment must have expected data
445        self.expected_data.as_ref().unwrap()
446    }
447}
448
449/// A commitment to ION DID Document data.
450pub struct IONCommitment {
451    did_doc: Document,
452    chained_commitment: ChainedCommitment,
453}
454
455impl IONCommitment {
456    pub fn new(
457        did_doc: Document,
458        chunk_file: Vec<u8>,
459        provisional_index_file: Vec<u8>,
460        core_index_file: Vec<u8>,
461        transaction: Vec<u8>,
462        merkle_proof: Vec<u8>,
463        block_header: Vec<u8>,
464    ) -> CommitmentResult<Self> {
465        // Extract the public keys and endpoints as the expected data.
466        let keys = did_doc.get_keys().unwrap_or_default();
467        let endpoints = did_doc.get_endpoints().unwrap_or_default();
468        let expected_data = json!([keys, endpoints]);
469
470        // Construct the core index file commitment first, to get the index of the chunk file delta for this DID.
471        let core_index_file_commitment =
472            IpfsIndexFileCommitment::<Incomplete>::new(core_index_file);
473        let delta_index: usize = serde_json::from_value::<CoreIndexFile>(
474            core_index_file_commitment.commitment_content()?,
475        )?
476        .did_create_operation_index(&did_doc.id)?;
477
478        // Construct the first *full* Commitment, followed by a sequence of TrivialCommitments.
479        let chunk_file_commitment =
480            IpfsChunkFileCommitment::<Incomplete>::new(chunk_file, delta_index);
481        let prov_index_file_commitment =
482            IpfsIndexFileCommitment::<Incomplete>::new(provisional_index_file);
483        let tx_commitment = TxCommitment::<Incomplete>::new(transaction);
484        let merkle_root_commitment = MerkleRootCommitment::<Incomplete>::new(merkle_proof);
485        let block_hash_commitment = BlockHashCommitment::<Incomplete>::new(block_header);
486
487        // The following construction is only possible because each TrivialCommitment
488        // knows how to convert itself to the correct Commitment type.
489        // This explains why the TrivialCommitment trait is necessary.
490        let mut iterated_commitment =
491            ChainedCommitment::new(Box::new(chunk_file_commitment).to_commitment(expected_data));
492        iterated_commitment.append(Box::new(prov_index_file_commitment))?;
493        iterated_commitment.append(Box::new(core_index_file_commitment))?;
494        iterated_commitment.append(Box::new(tx_commitment))?;
495        iterated_commitment.append(Box::new(merkle_root_commitment))?;
496        iterated_commitment.append(Box::new(block_hash_commitment))?;
497
498        Ok(Self {
499            did_doc,
500            chained_commitment: iterated_commitment,
501        })
502    }
503
504    pub fn chained_commitment(&self) -> &ChainedCommitment {
505        &self.chained_commitment
506    }
507}
508
509// Delegate all Commitment trait methods to the wrapped ChainedCommitment.
510impl TrivialCommitment for IONCommitment {
511    fn hasher(&self) -> fn(&[u8]) -> CommitmentResult<String> {
512        self.chained_commitment.hasher()
513    }
514
515    fn hash(&self) -> CommitmentResult<String> {
516        self.chained_commitment.hash()
517    }
518
519    fn candidate_data(&self) -> &[u8] {
520        self.chained_commitment.candidate_data()
521    }
522
523    fn decode_candidate_data(&self) -> fn(&[u8]) -> CommitmentResult<Value> {
524        self.chained_commitment.decode_candidate_data()
525    }
526
527    fn to_commitment(self: Box<Self>, _: serde_json::Value) -> Box<dyn Commitment> {
528        self
529    }
530}
531
532// Delegate all Commitment trait methods to the wrapped ChainedCommitment.
533impl Commitment for IONCommitment {
534    fn expected_data(&self) -> &serde_json::Value {
535        // Safe to unwrap as a complete commitment must have expected data
536        self.chained_commitment.expected_data()
537    }
538    // Essential to override verify otherwise calls will consider last commitment only.
539    fn verify(&self, target: &str) -> CommitmentResult<()> {
540        // Delegate verification to the chained commitment.
541        self.chained_commitment.verify(target)?;
542        Ok(())
543    }
544}
545
546impl DIDCommitment for IONCommitment {
547    fn did(&self) -> &str {
548        &self.did_doc.id
549    }
550
551    fn did_document(&self) -> &Document {
552        &self.did_doc
553    }
554
555    fn as_any(&self) -> &dyn std::any::Any {
556        self
557    }
558}
559
560/// A Commitment whose expected data is a Unix time and hasher
561/// and candidate data are obtained from a given DIDCommitment.
562pub struct BlockTimestampCommitment {
563    candidate_data: Vec<u8>,
564    expected_data: Timestamp,
565}
566
567impl BlockTimestampCommitment {
568    pub fn new(candidate_data: Vec<u8>, expected_data: Timestamp) -> CommitmentResult<Self> {
569        // The decoded candidate data must contain the timestamp such that it is found
570        // by the json_contains function, otherwise the content verification will fail.
571        Ok(Self {
572            candidate_data,
573            expected_data,
574        })
575    }
576}
577
578impl TrivialCommitment<Timestamp> for BlockTimestampCommitment {
579    fn hasher(&self) -> fn(&[u8]) -> CommitmentResult<String> {
580        block_header_hasher()
581    }
582
583    fn candidate_data(&self) -> &[u8] {
584        &self.candidate_data
585    }
586
587    /// Deserializes the candidate data into a Block header (as JSON).
588    fn decode_candidate_data(&self) -> fn(&[u8]) -> CommitmentResult<Value> {
589        block_header_decoder()
590    }
591
592    /// Override the filter method to ensure only timestamp content is considered.
593    fn filter(&self) -> Option<Box<dyn Fn(&serde_json::Value) -> CommitmentResult<Value>>> {
594        Some(Box::new(move |value| {
595            if let Value::Object(map) = value {
596                match map.get(TIMESTAMP_KEY) {
597                    Some(Value::Number(timestamp)) => Ok(Value::Number(timestamp.clone())),
598                    _ => Err(CommitmentError::DataDecodingFailure),
599                }
600            } else {
601                Err(CommitmentError::DataDecodingFailure)
602            }
603        }))
604    }
605
606    fn to_commitment(self: Box<Self>, _: Timestamp) -> Box<dyn Commitment<Timestamp>> {
607        self
608    }
609}
610
611impl Commitment<Timestamp> for BlockTimestampCommitment {
612    fn expected_data(&self) -> &Timestamp {
613        &self.expected_data
614    }
615}
616
617impl TimestampCommitment for BlockTimestampCommitment {}
618
619#[cfg(test)]
620mod tests {
621    use bitcoin::util::psbt::serialize::Serialize;
622    use bitcoin::BlockHash;
623    use ipfs_api_backend_hyper::IpfsClient;
624    use std::str::FromStr;
625    use trustchain_core::{data::TEST_ROOT_DOCUMENT, utils::json_contains};
626
627    use super::*;
628    use crate::{
629        data::TEST_BLOCK_HEADER_HEX,
630        utils::{block_header, merkle_proof, query_ipfs, transaction},
631    };
632
633    #[test]
634    fn test_block_timestamp_commitment() {
635        let expected_data: Timestamp = 1666265405;
636        let candidate_data = hex::decode(TEST_BLOCK_HEADER_HEX).unwrap();
637        let target = BlockTimestampCommitment::new(candidate_data.clone(), expected_data).unwrap();
638        target.verify_content().unwrap();
639        let pow_hash = "000000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f";
640        target.verify(pow_hash).unwrap();
641
642        // Both calls should instead error with incorrect timestamp
643        let bad_expected_data: Timestamp = 1666265406;
644        let target = BlockTimestampCommitment::new(candidate_data, bad_expected_data).unwrap();
645        match target.verify_content() {
646            Err(CommitmentError::FailedContentVerification(s1, s2)) => {
647                assert_eq!(
648                    (s1, s2),
649                    (format!("{bad_expected_data}"), format!("{expected_data}"))
650                )
651            }
652            _ => panic!(),
653        };
654        match target.verify(pow_hash) {
655            Err(CommitmentError::FailedContentVerification(s1, s2)) => {
656                assert_eq!(
657                    (s1, s2),
658                    (format!("{bad_expected_data}"), format!("{expected_data}"))
659                )
660            }
661            _ => panic!(),
662        };
663    }
664
665    #[test]
666    fn test_block_hash_commitment_filter() {
667        // The expected data is the Merkle root inside the block header.
668        // For the testnet block at height 2377445, the Merkle root is:
669        let expected_data =
670            json!("7dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69");
671        let candidate_data = hex::decode(TEST_BLOCK_HEADER_HEX).unwrap();
672        let target = BlockHashCommitment::<Complete>::new(candidate_data, expected_data);
673        target.verify_content().unwrap();
674        let pow_hash = "000000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f";
675        target.verify(pow_hash).unwrap();
676    }
677
678    #[tokio::test]
679    #[ignore = "Integration test requires IPFS"]
680    async fn test_extract_suffix_idx() {
681        let target = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
682        let ipfs_client = IpfsClient::default();
683        let candidate_data = query_ipfs(target, &ipfs_client).await.unwrap();
684        let core_index_file_commitment = IpfsIndexFileCommitment::<Incomplete>::new(candidate_data);
685        let operation_idx = serde_json::from_value::<CoreIndexFile>(
686            core_index_file_commitment.commitment_content().unwrap(),
687        )
688        .unwrap()
689        .did_create_operation_index("did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A");
690
691        assert_eq!(1, operation_idx.unwrap());
692    }
693
694    #[tokio::test]
695    #[ignore = "Integration test requires IPFS"]
696    async fn test_ipfs_commitment() {
697        let target = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
698
699        let ipfs_client = IpfsClient::default();
700
701        let candidate_data_ = query_ipfs(target, &ipfs_client).await.unwrap();
702        let candidate_data = candidate_data_.clone();
703        // In the core index file we expect to find the provisionalIndexFileUri.
704        let expected_data =
705            r#"{"provisionalIndexFileUri":"QmfXAa2MsHspcTSyru4o1bjPQELLi62sr2pAKizFstaxSs"}"#;
706        let expected_data: serde_json::Value = serde_json::from_str(expected_data).unwrap();
707        let commitment = IpfsIndexFileCommitment::<Complete>::new(candidate_data, expected_data);
708        assert!(commitment.verify(target).is_ok());
709
710        // We do *not* expect a different target to succeed.
711        let bad_target = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J98";
712        assert!(commitment.verify(bad_target).is_err());
713        match commitment.verify(bad_target) {
714            Err(CommitmentError::FailedHashVerification(..)) => (),
715            _ => panic!("Expected FailedHashVerification error."),
716        }
717
718        // We do *not* expect to find a different provisionalIndexFileUri.
719        let bad_expected_data =
720            r#"{"provisionalIndexFileUri":"PmfXAa2MsHspcTSyru4o1bjPQELLi62sr2pAKizFstaxSs"}"#;
721        let bad_expected_data = serde_json::from_str(bad_expected_data).unwrap();
722        let candidate_data = candidate_data_;
723        let commitment =
724            IpfsIndexFileCommitment::<Complete>::new(candidate_data, bad_expected_data);
725        assert!(commitment.verify(target).is_err());
726        match commitment.verify(target) {
727            Err(CommitmentError::FailedContentVerification(..)) => (),
728            _ => panic!("Expected FailedContentVerification error."),
729        };
730    }
731
732    #[test]
733    #[ignore = "Integration test requires Bitcoin Core"]
734    fn test_tx_commitment() {
735        let target = "9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c";
736
737        // Get the Bitcoin transaction.
738        let block_hash_str = "000000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f";
739        let block_hash = BlockHash::from_str(block_hash_str).unwrap();
740        let tx = transaction(&block_hash, 3, None).unwrap();
741
742        // We expect to find the IPFS CID for the ION core index file in the OP_RETURN data.
743        let cid_str = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
744        let expected_str = format!(r#"{{"{}":"{}"}}"#, CID_KEY, cid_str);
745        let expected_data: serde_json::Value = serde_json::from_str(&expected_str).unwrap();
746        let candidate_data = Serialize::serialize(&tx);
747
748        let commitment = TxCommitment::<Complete>::new(candidate_data, expected_data);
749        assert!(commitment.verify(target).is_ok());
750
751        // We do *not* expect a different target to succeed.
752        let bad_target = "8dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c";
753        assert!(commitment.verify(bad_target).is_err());
754        match commitment.verify(bad_target) {
755            Err(CommitmentError::FailedHashVerification(..)) => (),
756            _ => panic!("Expected FailedHashVerification error."),
757        };
758
759        // We do *not* expect to find a different IPFS CID in the OP_RETURN data.
760        let bad_cid_str = "PmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
761        let bad_expected_str = format!(r#"{{"{}":"{}"}}"#, CID_KEY, bad_cid_str);
762        let bad_expected_data: serde_json::Value = serde_json::from_str(&bad_expected_str).unwrap();
763        let candidate_data = Serialize::serialize(&tx);
764        let commitment = TxCommitment::<Complete>::new(candidate_data, bad_expected_data);
765        assert!(commitment.verify(target).is_err());
766        match commitment.verify(target) {
767            Err(CommitmentError::FailedContentVerification(..)) => (),
768            _ => panic!("Expected FailedContentVerification error."),
769        };
770    }
771
772    #[test]
773    #[ignore = "Integration test requires Bitcoin Core"]
774    fn test_merkle_root_commitment() {
775        // The commitment target is the Merkle root from the block header.
776        // For the testnet block at height 2377445, the Merkle root is:
777        let target = "7dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69";
778        // and the block hash is:
779        let block_hash_str = "000000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f";
780
781        // We expect to find the transaction ID in the Merkle proof (candidate data):
782        let txid_str = "9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c";
783        let expected_data = serde_json::json!(txid_str);
784
785        // Get the Bitcoin transaction.
786        let block_hash = BlockHash::from_str(block_hash_str).unwrap();
787        let tx_index = 3;
788        let tx = transaction(&block_hash, tx_index, None).unwrap();
789
790        // The candidate data is a serialized Merkle proof.
791        let candidate_data_ = merkle_proof(&tx, &block_hash, None).unwrap();
792        let candidate_data = candidate_data_.clone();
793
794        let commitment = MerkleRootCommitment::<Complete>::new(candidate_data, expected_data);
795        assert!(commitment.verify(target).is_ok());
796
797        // We do *not* expect a different target to succeed.
798        let bad_target = "8dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69";
799        assert!(commitment.verify(bad_target).is_err());
800        match commitment.verify(bad_target) {
801            Err(CommitmentError::FailedHashVerification(..)) => (),
802            _ => panic!("Expected FailedHashVerification error."),
803        };
804
805        // We do *not* expect to find an arbitrary transaction ID.
806        let bad_txid_str = "2dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c";
807        let bad_expected_data = serde_json::json!(bad_txid_str);
808        let candidate_data = candidate_data_;
809        let commitment = MerkleRootCommitment::<Complete>::new(candidate_data, bad_expected_data);
810        assert!(commitment.verify(target).is_err());
811        match commitment.verify(target) {
812            Err(CommitmentError::FailedContentVerification(..)) => (),
813            _ => panic!("Expected FailedContentVerification error."),
814        };
815    }
816
817    #[test]
818    #[ignore = "Integration test requires Bitcoin Core"]
819    fn test_block_hash_commitment() {
820        // The commitment target is the block hash.
821        let target = "000000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f";
822        let block_hash = BlockHash::from_str(target).unwrap();
823
824        // We expect to find the Merkle root in the block header.
825        // For the testnet block at height 2377445, the Merkle root is:
826        let merkle_root_str = "7dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69";
827        let expected_data = json!(merkle_root_str);
828
829        // The candidate data is the serialized block header.
830        let block_header = block_header(&block_hash, None).unwrap();
831        let candidate_data = bitcoin::consensus::serialize(&block_header);
832        let commitment =
833            BlockHashCommitment::<Complete>::new(candidate_data.clone(), expected_data);
834        commitment.verify(target).unwrap();
835
836        // We do *not* expect a different target to succeed.
837        let bad_target = "100000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f";
838        assert!(commitment.verify(bad_target).is_err());
839        match commitment.verify(bad_target) {
840            Err(CommitmentError::FailedHashVerification(..)) => (),
841            _ => panic!("Expected FailedHashVerification error."),
842        };
843
844        // We do *not* expect to find a different Merkle root.
845        let bad_merkle_root_str =
846            "6dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69";
847        let bad_expected_data = json!(bad_merkle_root_str);
848        let commitment =
849            BlockHashCommitment::<Complete>::new(candidate_data.clone(), bad_expected_data);
850        assert!(commitment.verify(target).is_err());
851        match commitment.verify(target) {
852            Err(CommitmentError::FailedContentVerification(..)) => (),
853            _ => panic!("Expected FailedContentVerification error."),
854        };
855
856        // We do *not* expect the (correct) timestamp to be valid expected data,
857        // since the candidate data is filtered to contain only the Merkle root field.
858        let wrong_expected_data_commitment =
859            BlockHashCommitment::<Complete>::new(candidate_data.clone(), json!(1666265405));
860        assert!(wrong_expected_data_commitment.verify(target).is_err());
861
862        // Also test as timestamp commitment
863        let expected_data = 1666265405;
864        let commitment =
865            BlockTimestampCommitment::new(candidate_data.clone(), expected_data).unwrap();
866        commitment.verify_content().unwrap();
867        commitment.verify(target).unwrap();
868        let bad_expected_data = 1666265406;
869        let commitment = BlockTimestampCommitment::new(candidate_data, bad_expected_data).unwrap();
870        assert!(commitment.verify_content().is_err());
871        assert!(commitment.verify(target).is_err());
872    }
873
874    #[tokio::test]
875    #[ignore = "Integration test requires IPFS and Bitcoin Core"]
876    async fn test_ion_commitment() {
877        let did_doc = Document::from_json(TEST_ROOT_DOCUMENT).unwrap();
878
879        let ipfs_client = IpfsClient::default();
880
881        let chunk_file_cid = "QmWeK5PbKASyNjEYKJ629n6xuwmarZTY6prd19ANpt6qyN";
882        let chunk_file = query_ipfs(chunk_file_cid, &ipfs_client).await.unwrap();
883
884        let prov_index_file_cid = "QmfXAa2MsHspcTSyru4o1bjPQELLi62sr2pAKizFstaxSs";
885        let prov_index_file = query_ipfs(prov_index_file_cid, &ipfs_client).await.unwrap();
886
887        let core_index_file_cid = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
888        let core_index_file = query_ipfs(core_index_file_cid, &ipfs_client).await.unwrap();
889
890        let block_hash_str = "000000000000000eaa9e43748768cd8bf34f43aaa03abd9036c463010a0c6e7f";
891        let block_hash = BlockHash::from_str(block_hash_str).unwrap();
892        let tx_index = 3;
893        let tx = transaction(&block_hash, tx_index, None).unwrap();
894        let transaction = Serialize::serialize(&tx);
895
896        let merkle_proof = merkle_proof(&tx, &block_hash, None).unwrap();
897
898        let block_header = block_header(&block_hash, None).unwrap();
899        let block_header = bitcoin::consensus::serialize(&block_header);
900
901        let commitment = IONCommitment::new(
902            did_doc,
903            chunk_file,
904            prov_index_file,
905            core_index_file,
906            transaction,
907            merkle_proof,
908            block_header,
909        )
910        .unwrap();
911
912        let expected_data = commitment.chained_commitment.expected_data();
913
914        println!("{:?}", expected_data);
915        // The expected data contains public keys and service endpoints.
916        match expected_data {
917            serde_json::Value::Array(arr) => {
918                assert_eq!(arr.len(), 2);
919            }
920            _ => panic!("Expected JSON Array."),
921        }
922
923        // Check each individual commitment.
924        let commitments = commitment.chained_commitment.commitments();
925
926        // The first one commits to the chunk file CID and is expected
927        // to contain the same data as the iterated commitment.
928        let chunk_file_commitment = commitments.first().unwrap();
929        assert_eq!(chunk_file_commitment.hash().unwrap(), chunk_file_cid);
930        assert_eq!(expected_data, chunk_file_commitment.expected_data());
931
932        // Verify the chunk file commitment.
933        assert!(&chunk_file_commitment.verify(chunk_file_cid).is_ok());
934
935        // The second one commits to the provisional index file CID
936        // and is expected to contain the chunk file CID.
937        let prov_index_file_commitment = commitments.get(1).unwrap();
938        assert_eq!(
939            prov_index_file_commitment.hash().unwrap(),
940            prov_index_file_cid
941        );
942        assert!(json_contains(
943            &json!(chunk_file_cid),
944            prov_index_file_commitment.expected_data()
945        ));
946
947        // Verify the provisional index file commitment.
948        assert!(&prov_index_file_commitment
949            .verify(prov_index_file_cid)
950            .is_ok());
951
952        // The third one commits to the core index file CID
953        // and is expected to contain the provision index file CID.
954        let core_index_file_commitment = commitments.get(2).unwrap();
955        assert_eq!(
956            core_index_file_commitment.hash().unwrap(),
957            core_index_file_cid
958        );
959        assert!(json_contains(
960            &json!(prov_index_file_cid),
961            core_index_file_commitment.expected_data()
962        ));
963
964        // Verify the core index file commitment.
965        assert!(&core_index_file_commitment
966            .verify(core_index_file_cid)
967            .is_ok());
968
969        // The fourth one commits to the Bitcoin transaction ID
970        // and is expected to contain the core index file CID.
971        let tx_commitment = commitments.get(3).unwrap();
972        let tx_id = "9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c";
973        assert_eq!(tx_commitment.hash().unwrap(), tx_id);
974        assert!(json_contains(
975            &json!(core_index_file_cid),
976            tx_commitment.expected_data()
977        ));
978
979        // Verify the transaction ID commitment.
980        assert!(&tx_commitment.verify(tx_id).is_ok());
981
982        // The fifth one commits to the Merkle root in the block header
983        // and is expected to contain the Bitcoin transaction ID.
984        let merkle_root_commitment = commitments.get(4).unwrap();
985        let merkle_root = "7dce795209d4b5051da3f5f5293ac97c2ec677687098062044654111529cad69";
986        assert_eq!(merkle_root_commitment.hash().unwrap(), merkle_root);
987        assert!(json_contains(
988            &json!(tx_id),
989            merkle_root_commitment.expected_data()
990        ));
991
992        // Verify the Merkle root commitment.
993        assert!(&merkle_root_commitment.verify(merkle_root).is_ok());
994
995        // Finally, the sixth one commits to the block hash (PoW)
996        // and is expected to contain the Merkle root.
997        let block_hash_commitment = commitments.get(5).unwrap();
998        assert_eq!(block_hash_commitment.hash().unwrap(), block_hash_str);
999        assert!(json_contains(
1000            &json!(merkle_root),
1001            block_hash_commitment.expected_data()
1002        ));
1003
1004        // Verify the Merkle root commitment.
1005        assert!(&merkle_root_commitment.verify(merkle_root).is_ok());
1006
1007        // Verify the iterated commitment content (i.e. the expected_data).
1008        assert!(commitment.chained_commitment.verify_content().is_ok());
1009        assert!(commitment.chained_commitment.verify(block_hash_str).is_ok());
1010
1011        // Verify the IONCommitment itself.
1012        assert!(commitment.verify(block_hash_str).is_ok());
1013    }
1014}