trustchain_ion/
root.rs

1use chrono::NaiveDate;
2use futures::{future, StreamExt};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use trustchain_core::utils::get_did_from_suffix;
6
7use crate::{
8    utils::{
9        block_height_range_on_date, locate_transaction, query_mongodb_on_interval, transaction,
10    },
11    TrustchainBitcoinError, TrustchainMongodbError, ION_TEST_METHOD, MONGO_FILTER_DID_SUFFIX,
12    MONGO_FILTER_TXN_TIME,
13};
14
15/// An error relating to the root DID.
16#[derive(Error, Debug)]
17pub enum TrustchainRootError {
18    /// Bitcoin RPC interface error while processing root event date.
19    #[error("Bitcoin RPC error while processing root event date.")]
20    BitcoinRpcError(TrustchainBitcoinError),
21    /// Mongo DB error while processing root event date.
22    #[error("Mongo DB error while processing root event date.")]
23    MongoDbError(TrustchainMongodbError),
24    /// Failed to identify unique root DID.
25    #[error("No unique root DID on date: {0}")]
26    NoUniqueRootEvent(NaiveDate),
27    /// Invalid date.
28    #[error("Invalid date: {0}-{1}-{2}")]
29    InvalidDate(i32, u32, u32),
30    /// Failed to parse block height.
31    #[error("Failed to parse block height: {0}")]
32    FailedToParseBlockHeight(String),
33}
34
35impl From<TrustchainBitcoinError> for TrustchainRootError {
36    fn from(err: TrustchainBitcoinError) -> Self {
37        TrustchainRootError::BitcoinRpcError(err)
38    }
39}
40
41impl From<TrustchainMongodbError> for TrustchainRootError {
42    fn from(err: TrustchainMongodbError) -> Self {
43        TrustchainRootError::MongoDbError(err)
44    }
45}
46
47/// Struct representing a root DID candidate.
48#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
49#[serde(rename_all = "camelCase")]
50pub struct RootCandidate {
51    pub did: String,
52    pub txid: String,
53    pub block_height: u64,
54}
55
56/// Identifies potential root DIDs whose (UTC) timestamp matches a given date.
57/// Root DID candidates are those that are found in ION create operations with
58/// operation index zero (opIndex = 0). As such, root DIDs must be created in
59/// the first DID operation associated with a particular Bitcoin transaction.
60pub async fn root_did_candidates(
61    date: NaiveDate,
62) -> Result<Vec<RootCandidate>, TrustchainRootError> {
63    let block_height_range = block_height_range_on_date(date, None, None)?;
64    let cursor =
65        query_mongodb_on_interval(block_height_range.0 as u32, block_height_range.1 as u32).await?;
66
67    // The mongodb Cursor instance streams all bson documents that:
68    // - represent ION DID create operations, and
69    // - whose timestamp falls within the given date, and
70    // - whose ION operation index is zero.
71
72    // This Cursor is then filtered by:
73    // - discarding any errors when extracting the opIndex or didSuffix fields, and
74    // - discarding any operations for which the corresponding Bitcoin transaction cannot be located & retrieved.
75
76    // TODO:
77    // Additional filtering should be added to discard any downstream DIDs by resolving and inspecting the DID metadata.
78    // For this the steps are:
79    //  - get the DID suffix from the bson document
80    //  - resolve the DID (using an IONResolver passed in to this function)
81    //  - inspect the document metadata...
82
83    let rpc_client = &crate::utils::rpc_client();
84    let vec = cursor
85        .filter(|x| future::ready(x.is_ok()))
86        .map(|x| x.unwrap())
87        .filter_map(|doc| async move {
88            if doc.get_str(MONGO_FILTER_DID_SUFFIX).is_err() {
89                return None;
90            }
91            let did_suffix = doc.get_str(MONGO_FILTER_DID_SUFFIX).unwrap();
92            // TODO: test vs mainnet needs handling here:
93            let did = get_did_from_suffix(did_suffix, ION_TEST_METHOD);
94            let tx_locator = locate_transaction(&did, rpc_client).await;
95            if tx_locator.is_err() {
96                return None;
97            }
98            let (block_hash, tx_index) = tx_locator.unwrap();
99            let tx = transaction(&block_hash, tx_index, Some(rpc_client));
100            if tx.is_err() {
101                return None;
102            }
103            let txid = tx.unwrap().txid().to_string();
104
105            let block_height = doc
106                .get_i32(MONGO_FILTER_TXN_TIME)
107                .unwrap()
108                .try_into()
109                .unwrap();
110            Some(RootCandidate {
111                did,
112                txid,
113                block_height,
114            })
115        })
116        .collect::<Vec<RootCandidate>>()
117        .await;
118    Ok(vec)
119}
120
121#[cfg(test)]
122mod tests {
123    use itertools::Itertools;
124
125    use super::*;
126
127    #[tokio::test]
128    #[ignore = "Integration test requires Bitcoin & MongoDB"]
129    async fn test_root_did_candidates() {
130        let date = NaiveDate::from_ymd_opt(2022, 10, 20).unwrap();
131        let result = root_did_candidates(date)
132            .await
133            .unwrap()
134            .into_iter()
135            .sorted()
136            .collect_vec();
137
138        // There were 38 testnet ION operations with opIndex 0 on 20th Oct 2022.
139        // The block height range on that date is (2377360, 2377519).
140        // The relevant mongosh query is:
141        // db.operations.find({type: 'create', opIndex: 0, txnTime: { $gt: 2377359, $lt: 2377520}}).count()
142        assert_eq!(result.len(), 38);
143
144        assert_eq!(
145            result[0].did,
146            "did:ion:test:EiA6m4-V4fW_l1xEu3jH9xvXt1JyynmO7I_rkBpFulEAuQ"
147        );
148        assert_eq!(
149            result[0].txid,
150            "b698c0919a91a161bc141cd395788296edb85d19415a6d29a13a220a8f2249e0"
151        );
152        assert_eq!(result[0].block_height, 2377410);
153
154        // This is the root DID used in testing:
155        assert_eq!(
156            result[26].did,
157            "did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"
158        );
159        assert_eq!(
160            result[26].txid,
161            "9dc43cca950d923442445340c2e30bc57761a62ef3eaf2417ec5c75784ea9c2c"
162        );
163        assert_eq!(result[26].block_height, 2377445);
164
165        assert_eq!(
166            result[37].did,
167            "did:ion:test:EiDz_zvUa2FUIgLUvBia9wUJakhrrW889nDdGlr1-RTAWw"
168        );
169        assert_eq!(
170            result[37].txid,
171            "c369dd566a0dd5c2f381c1ab9c8e96b4f6b4fd323f5c1ed68dbb2a1bfb9cb48f"
172        );
173        assert_eq!(result[37].block_height, 2377416);
174    }
175}