iop-morpheus-proto 0.0.16

Morpheus protocol definitions used on both node and sdk side of the IOP Stack™
Documentation
use super::*;

use crate::data::auth::Authentication;
use crate::data::{
    did::Did,
    validation::{ValidationIssueSeverity as Severity, ValidationResult},
};

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub enum Right {
    #[serde(rename = "update")]
    Update,
    #[serde(rename = "impersonate")]
    Impersonation,
}

impl Display for Right {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let value =
            serde_json::to_value(self).expect("Implementation error: Right is not serializable");
        match value {
            serde_json::Value::String(s) => write!(f, "{}", s),
            _ => panic!("Implementation error: unexpected Right serialization"),
        }
    }
}

impl FromStr for Right {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(serde_json::from_value(serde_json::Value::String(s.to_owned()))?)
    }
}

impl Right {
    pub fn map_all<T>(f: impl Fn(&Right) -> T) -> HashMap<Right, T> {
        vec![Self::Update, Self::Impersonation].drain(..).map(|r| (r, f(&r))).collect()
    }
}

pub fn is_in_opt_range(
    height: BlockHeight, from_inc: Option<BlockHeight>, until_exc: Option<BlockHeight>,
) -> bool {
    if let Some(from) = from_inc {
        if height < from {
            return false;
        }
    }
    if let Some(until) = until_exc {
        if until <= height {
            return false;
        }
    }
    true
}

pub fn is_between(height: BlockHeight, after: BlockHeight, until_exc: BlockHeight) -> bool {
    is_in_opt_range(height, Some(after + 1), Some(until_exc))
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct KeyState {
    #[serde(rename = "auth")]
    pub authentication: Authentication,
    #[serde(rename = "validFromHeight")]
    pub valid_from_block: Option<BlockHeight>, // TODO should be timestamp on the long term
    #[serde(rename = "validUntilHeight")]
    pub valid_until_block: Option<BlockHeight>, // TODO should be timestamp on the long term
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct KeyDataDerived {
    pub index: usize,
    pub valid: bool,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct KeyData {
    #[serde(flatten)]
    pub state: KeyState,
    #[serde(flatten)]
    pub derived: KeyDataDerived,
}

impl KeyData {
    fn from_auth(authentication: Authentication) -> Self {
        let state = KeyState { authentication, valid_from_block: None, valid_until_block: None };
        let derived = KeyDataDerived { index: 0, valid: true };
        Self { state, derived }
    }

    fn is_valid_at(&self, height: BlockHeight) -> bool {
        is_in_opt_range(height, self.state.valid_from_block, self.state.valid_until_block)
    }
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub struct KeyRightHistoryItem {
    pub height: Option<BlockHeight>,
    pub valid: bool,
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub struct KeyRightState {
    pub history: Vec<KeyRightHistoryItem>,
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub struct KeyRightDerived {
    #[serde(rename = "keyLink")]
    pub key_link: String, // TODO should be more strictly typed
    pub valid: bool,
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub struct KeyRightHistory {
    #[serde(flatten)]
    pub state: KeyRightState,
    #[serde(flatten)]
    pub derived: KeyRightDerived,
}

impl KeyRightHistory {
    fn ensure_valid_history(&self) -> Result<()> {
        let heights: Vec<_> =
            self.state.history.iter().map(|item| item.height.unwrap_or_default()).collect();
        let mut sorted = heights.clone();
        sorted.sort_unstable(); // equal u32 instances do not differ, so unstable is fine
        ensure!(heights == sorted, "Height of key history items must be strictly increasing");
        Ok(())
    }

    fn is_true_at(&self, height: BlockHeight) -> Result<bool> {
        // All such checks should be done instead when constructing/parsing the whole DidDocument
        self.ensure_valid_history()?;

        let last_state_before_height =
            self.state.history.iter().rev().find(|item| item.height.unwrap_or_default() <= height);
        let valid = match last_state_before_height {
            None => false,
            Some(item) => item.valid,
        };
        Ok(valid)
    }
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub enum ServiceType {
    // TODO
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
pub struct Service {
    #[serde(rename = "type")]
    pub type_: ServiceType,
    pub name: String,
    pub service_endpoint: String, // TODO should we use multiaddr::Multiaddr here and thus add CID-dependency?
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DidDocument {
    #[serde(with = "serde_str")]
    pub did: Did,
    pub keys: Vec<KeyData>,
    #[serde(skip_serializing_if = "HashMap::is_empty", default)]
    pub rights: HashMap<Right, Vec<KeyRightHistory>>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub services: Vec<Service>,
    #[serde(rename = "tombstonedAtHeight")]
    pub tombstoned_at_height: Option<BlockHeight>,
    pub tombstoned: bool,
    #[serde(rename = "queriedAtHeight")]
    pub queried_at_height: BlockHeight,
}

impl DidDocument {
    pub fn implicit(did: &Did) -> Self {
        let default_key = KeyData::from_auth(Authentication::KeyId(did.default_key_id()));
        Self {
            did: did.to_owned(),
            keys: vec![default_key],
            rights: Default::default(),
            services: Default::default(),
            tombstoned_at_height: Default::default(),
            tombstoned: Default::default(),
            queried_at_height: Default::default(),
        }
    }

    fn key(&self, key_link: &str) -> Result<KeyData> {
        ensure!(key_link.starts_with('#'), "Key links for remote DIDs are not supported yet");
        let idx_str: String = key_link.chars().skip(1).collect();
        let idx: usize = idx_str.parse()?;
        let key =
            self.keys.get(idx).ok_or_else(|| anyhow!("No key found for link {}", key_link))?;
        Ok(key.to_owned())
    }

    fn ensure_known_height(&self, height: BlockHeight) -> Result<()> {
        if self.queried_at_height < height {
            bail!("Queried future height {}, present is {}", height, self.queried_at_height);
        }
        Ok(())
    }

    pub fn has_right_at(
        &self, auth: &Authentication, right: Right, height: BlockHeight,
    ) -> Result<bool> {
        self.ensure_known_height(height)?;

        if let Some(tombstoned_at_height) = self.tombstoned_at_height {
            if tombstoned_at_height <= height {
                return Ok(false);
            }
        }
        let keys_with_right = match self.rights.get(&right) {
            Some(key) => key,
            None => return Ok(false),
        };

        for key_right in keys_with_right.iter() {
            let key = self.key(&key_right.derived.key_link)?;
            if !key.is_valid_at(height) {
                continue;
            }
            if key.state.authentication != *auth {
                continue;
            }

            return key_right.is_true_at(height);
        }

        Ok(false)
    }

    pub fn is_tombstoned_at(&self, height: BlockHeight) -> Result<bool> {
        self.ensure_known_height(height)?;

        if let Some(tombstone_height) = self.tombstoned_at_height {
            return Ok(tombstone_height <= height);
        }

        Ok(false)
    }

    // TODO reconsider and thoroughly check if until should be inclusive or exclusive and if implementation matches
    pub fn validate_right(
        &self, auth: &Authentication, right: Right, from: BlockHeight, until: BlockHeight,
    ) -> Result<ValidationResult> {
        ensure!(1 <= from, "Range must not predate genesis block");
        ensure!(from < until, "Invalid block range {}-{}", from, until);
        self.ensure_known_height(until)?;

        let mut result: ValidationResult = Default::default();

        if self.is_tombstoned_at(from)? {
            result.add_issue(Severity::Error, "DID was tombstoned before given period");
        }
        if let Some(tombstone_height) = self.tombstoned_at_height {
            if is_between(tombstone_height, from, until) {
                result.add_issue(Severity::Warning, "DID was tombstoned during given period");
            }
        }

        let keys_with_right = match self.rights.get(&right) {
            Some(entries) => entries,
            None => {
                result
                    .add_issue(Severity::Error, "Right was never granted to given authentication");
                return Ok(result);
            }
        };

        let key_history_opt = keys_with_right.iter().find_map(|right_entry| {
            let key_data = match self.key(&right_entry.derived.key_link) {
                Ok(key_entry) => key_entry,
                Err(e) => {
                    // TODO ideally detected earlier during parsing and should never happen here
                    result.add_issue(Severity::Error, &e.to_string());
                    return None;
                }
            };
            if key_data.state.authentication != *auth {
                return None;
            }
            Some((key_data, right_entry))
        });

        let (key_data, key_right) = match key_history_opt {
            Some(key_history) => key_history,
            None => {
                result.add_issue(Severity::Error, "No matching authentication found in DID");
                return Ok(result);
            }
        };

        if let Some(key_valid_from) = key_data.state.valid_from_block {
            if until < key_valid_from {
                result.add_issue(Severity::Error, "Key was enabled only after given period");
            }
            if is_between(key_valid_from, from, until) {
                result.add_issue(Severity::Warning, "Key was enabled during given period");
            }
        }

        if let Some(key_valid_until) = key_data.state.valid_until_block {
            if key_valid_until < from {
                result.add_issue(Severity::Error, "Key expired before given period");
            }
            if is_between(key_valid_until, from, until) {
                result.add_issue(Severity::Warning, "Key expired during given period");
            }
        }

        let history = &key_right.state.history;
        ensure!(! history.is_empty(), "Implementation error: key related to rights were already filtered, right must be present here");

        let mut right_changes_in_range =
            history.iter().filter(|item| is_between(item.height.unwrap_or_default(), from, until));

        if !key_right.is_true_at(from)? {
            if right_changes_in_range.next().is_none() {
                result.add_issue(Severity::Error, "Required right was never granted for key");
            } else {
                result.add_issue(Severity::Warning, "Required right changed during given period");
            }
        }

        Ok(result)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::data::validation::ValidationStatus;

    #[test]
    fn pretty_json() -> Result<()> {
        test_parsed_did_document(
            r##"{
            "did": "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr",
            "keys": [
              {
                "index": 0,
                "auth": "iezbeWGSY2dqcUBqT8K7R14xr",
                "valid": true
              },
              {
                "index": 1,
                "auth": "iez25N5WZ1Q6TQpgpyYgiu9gTX",
                "valid": true,
                "validFromHeight": 120
              }
            ],
            "rights": {
              "impersonate": [
                {
                  "keyLink": "#0",
                  "history": [
                    {
                      "height": null,
                      "valid": true
                    }
                  ],
                  "valid": true
                },
                {
                  "keyLink": "#1",
                  "history": [
                    {
                      "height": null,
                      "valid": false
                    },
                    {
                      "height": 126,
                      "valid": true
                    }
                  ],
                  "valid": true
                }
              ],
              "update": [
                {
                  "keyLink": "#0",
                  "history": [
                    {
                      "height": null,
                      "valid": true
                    }
                  ],
                  "valid": true
                },
                {
                  "keyLink": "#1",
                  "history": [
                    {
                      "height": null,
                      "valid": false
                    }
                  ],
                  "valid": false
                }
              ]
            },
            "tombstonedAtHeight": null,
            "tombstoned": false,
            "queriedAtHeight": 126
          }"##,
        )
    }

    #[test]
    fn terse_json() -> Result<()> {
        test_parsed_did_document(
            r##"{"did":"did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr","keys":[{"index":0,"auth":"iezbeWGSY2dqcUBqT8K7R14xr","validFromHeight":null,"validUntilHeight":null,"valid":true},{"index":1,"auth":"iez25N5WZ1Q6TQpgpyYgiu9gTX","validFromHeight":120,"validUntilHeight":null,"valid":true}],"rights":{"impersonate":[{"keyLink":"#0","history":[{"height":null,"valid":true}],"valid":true},{"keyLink":"#1","history":[{"height":null,"valid":false},{"height":126,"valid":true}],"valid":true}],"update":[{"keyLink":"#0","history":[{"height":null,"valid":true}],"valid":true},{"keyLink":"#1","history":[{"height":null,"valid":false}],"valid":false}]},"tombstoned":false,"tombstonedAtHeight":null,"queriedAtHeight":126}"##,
        )
    }

    fn test_parsed_did_document(s: &str) -> Result<()> {
        let doc: DidDocument = serde_json::from_str(s)?;

        assert_eq!(doc.did, "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr".parse()?);
        assert_eq!(doc.tombstoned_at_height, None);
        assert_eq!(doc.queried_at_height, 126);
        assert_eq!(doc.tombstoned, false);

        let first_key = &doc.keys[0].state.authentication;
        let second_key = &doc.keys[1].state.authentication;

        assert!(doc.has_right_at(first_key, Right::Impersonation, 1)?);
        assert!(doc.has_right_at(first_key, Right::Impersonation, 2)?);
        assert!(doc.has_right_at(first_key, Right::Impersonation, 125)?);
        assert!(doc.has_right_at(first_key, Right::Impersonation, 126)?);
        assert!(doc.has_right_at(first_key, Right::Impersonation, 127).is_err());

        assert!(!doc.has_right_at(second_key, Right::Impersonation, 1)?);
        assert!(!doc.has_right_at(second_key, Right::Impersonation, 2)?);
        assert!(!doc.has_right_at(second_key, Right::Impersonation, 125)?);
        assert!(doc.has_right_at(second_key, Right::Impersonation, 126)?);
        assert!(doc.has_right_at(second_key, Right::Impersonation, 127).is_err());

        assert!(doc.has_right_at(first_key, Right::Update, 1)?);
        assert!(doc.has_right_at(first_key, Right::Update, 2)?);
        assert!(doc.has_right_at(first_key, Right::Update, 125)?);
        assert!(doc.has_right_at(first_key, Right::Update, 126)?);
        assert!(doc.has_right_at(first_key, Right::Update, 127).is_err());

        assert!(!doc.has_right_at(second_key, Right::Update, 1)?);
        assert!(!doc.has_right_at(second_key, Right::Update, 2)?);
        assert!(!doc.has_right_at(second_key, Right::Update, 125)?);
        assert!(!doc.has_right_at(second_key, Right::Update, 126)?);
        assert!(doc.has_right_at(second_key, Right::Update, 127).is_err());

        Ok(())
    }

    #[test]
    #[allow(clippy::cognitive_complexity)]
    fn has_right_between() -> Result<()> {
        let did_doc_str = r##"{
            "did": "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr",
            "keys": [
              {
                "index": 0,
                "auth": "iezbeWGSY2dqcUBqT8K7R14xr",
                "valid": true
              },
              {
                "index": 1,
                "auth": "iez25N5WZ1Q6TQpgpyYgiu9gTX",
                "valid": true,
                "validFromHeight": 10,
                "validUntilHeight": 90
              }
            ],
            "rights": {
              "impersonate": [
                {
                  "keyLink": "#0",
                  "history": [
                    { "height": null, "valid": true }
                  ],
                  "valid": true
                },
                {
                  "keyLink": "#1",
                  "history": [
                    { "height": null, "valid": false },
                    { "height": 20, "valid": true },
                    { "height": 80, "valid": false }
                  ],
                  "valid": false
                }
              ],
              "update": [
                {
                  "keyLink": "#0",
                  "history": [
                    { "height": null, "valid": true }
                  ],
                  "valid": true
                },
                {
                  "keyLink": "#1",
                  "history": [
                    { "height": null, "valid": false },
                    { "height": 90, "valid": true }
                  ],
                  "valid": true
                }
              ]
            },
            "tombstonedAtHeight": 100,
            "tombstoned": true,
            "queriedAtHeight": 200
          }"##;

        let doc: DidDocument = serde_json::from_str(did_doc_str)?;

        assert_eq!(doc.did, "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr".parse()?);
        assert_eq!(doc.tombstoned_at_height, Some(100));
        assert_eq!(doc.queried_at_height, 200);
        assert_eq!(doc.tombstoned, true);

        let first_key = &doc.keys[0].state.authentication;
        let second_key = &doc.keys[1].state.authentication;
        assert_eq!(*first_key, Authentication::KeyId("iezbeWGSY2dqcUBqT8K7R14xr".parse()?));
        assert_eq!(*second_key, Authentication::KeyId("iez25N5WZ1Q6TQpgpyYgiu9gTX".parse()?));

        use Right::*;
        use ValidationStatus::*;

        assert_eq!(doc.validate_right(first_key, Impersonation, 1, 100)?.status(), Valid);
        assert_eq!(doc.validate_right(first_key, Update, 1, 100)?.status(), Valid);
        assert_eq!(doc.validate_right(first_key, Impersonation, 10, 90)?.status(), Valid);
        assert_eq!(doc.validate_right(first_key, Update, 10, 90)?.status(), Valid);
        assert_eq!(doc.validate_right(first_key, Impersonation, 101, 200)?.status(), Invalid);
        assert_eq!(doc.validate_right(first_key, Update, 101, 200)?.status(), Invalid);
        assert_eq!(doc.validate_right(first_key, Impersonation, 1, 200)?.status(), MaybeValid);
        assert_eq!(doc.validate_right(first_key, Update, 1, 200)?.status(), MaybeValid);
        assert_eq!(doc.validate_right(first_key, Impersonation, 50, 150)?.status(), MaybeValid);
        assert_eq!(doc.validate_right(first_key, Update, 50, 150)?.status(), MaybeValid);

        assert_eq!(doc.validate_right(second_key, Impersonation, 20, 80)?.status(), Valid);
        assert_eq!(doc.validate_right(second_key, Impersonation, 30, 70)?.status(), Valid);
        assert_eq!(doc.validate_right(second_key, Impersonation, 1, 80)?.status(), MaybeValid);
        assert_eq!(doc.validate_right(second_key, Impersonation, 20, 200)?.status(), MaybeValid);
        assert_eq!(doc.validate_right(second_key, Impersonation, 1, 20)?.status(), Invalid);
        assert_eq!(doc.validate_right(second_key, Impersonation, 80, 200)?.status(), Invalid);

        assert_eq!(doc.validate_right(second_key, Update, 90, 100)?.status(), Valid);
        assert_eq!(doc.validate_right(second_key, Update, 1, 90)?.status(), Invalid);
        assert_eq!(doc.validate_right(second_key, Update, 100, 200)?.status(), Invalid);
        assert_eq!(doc.validate_right(second_key, Update, 80, 110)?.status(), MaybeValid);

        Ok(())
    }
}