newton-enclave 0.4.15

newton prover enclave compute
//! domain data construction for policy evaluation inside the enclave.
//!
//! the enclave needs its own domain mapping layer rather than reusing
//! `crates/operator/src/core.rs` because the operator's resolution is tightly coupled
//! to on-chain registry calls and database lookups — neither of which the enclave can
//! perform (no network access, no DB connection). the enclave receives pre-fetched
//! encrypted envelopes and decrypts them locally, then maps the plaintext JSON into
//! the `PolicyDomainData` types that the rego engine expects.

use std::collections::BTreeMap;

use chrono::{TimeZone, Utc};
use newton_chainio::{
    confidential_data::ConfidentialDomain,
    identity_data::{deserialize_identity_data, IdentityDomain},
};
use newton_core::rego::{AllowlistData, BlacklistData, PolicyDomainData, Value};

use crate::{decrypt::DecryptedEnvelope, error::EnclaveError};

pub(crate) fn merge_additional(
    additional_data: Option<serde_json::Value>,
    ephemeral_data: Option<serde_json::Value>,
) -> Option<serde_json::Value> {
    let mut obj = match additional_data {
        Some(serde_json::Value::Object(map)) => map,
        _ => serde_json::Map::new(),
    };

    if let Some(ephemeral) = ephemeral_data {
        obj.insert("privacy".to_string(), ephemeral);
    }

    (!obj.is_empty()).then_some(serde_json::Value::Object(obj))
}

pub(crate) fn identity_data(
    values: &[DecryptedEnvelope],
    timestamp: u64,
) -> Result<Vec<Box<dyn PolicyDomainData>>, EnclaveError> {
    let date = Utc
        .timestamp_opt(
            timestamp
                .try_into()
                .map_err(|_| EnclaveError::InvalidRequest("timestamp".to_string()))?,
            0,
        )
        .single()
        .ok_or_else(|| EnclaveError::InvalidRequest("timestamp".to_string()))?
        .format("%Y-%m-%d")
        .to_string();

    values
        .iter()
        .enumerate()
        .map(|(index, item)| {
            let json = item.value.to_string();
            if let Some(domain) = item.domain {
                match deserialize_identity_data(&domain, &json, date.clone()) {
                    Ok(data) => Ok(data),
                    Err(_) => {
                        let domain_name = IdentityDomain::from_bytes32(&domain)
                            .map(|d| d.name().to_string())
                            .unwrap_or_else(|| format!("0x{}", hex::encode(&domain[..8])));
                        Ok(Box::new(GenericDomainData {
                            domain: domain_name,
                            prefix: "identity".to_string(),
                            fields: item.value.clone(),
                        }) as Box<dyn PolicyDomainData>)
                    }
                }
            } else {
                Ok(Box::new(GenericDomainData {
                    domain: format!("identity_domain_{index}"),
                    prefix: "identity".to_string(),
                    fields: item.value.clone(),
                }) as Box<dyn PolicyDomainData>)
            }
        })
        .collect()
}

pub(crate) fn confidential_data(values: &[DecryptedEnvelope]) -> Result<Vec<Box<dyn PolicyDomainData>>, EnclaveError> {
    values
        .iter()
        .enumerate()
        .map(|(index, item)| {
            let rego_ns = item
                .domain
                .and_then(|domain| ConfidentialDomain::from_bytes32(&domain).map(|d| d.rego_namespace().to_string()))
                .unwrap_or_else(|| format!("confidential_domain_{index}"));

            let data: Box<dyn PolicyDomainData> = match rego_ns.as_str() {
                "blacklist" => Box::new(BlacklistData {
                    addresses: string_array(&item.value, "addresses"),
                }),
                "allowlist" => Box::new(AllowlistData {
                    addresses: string_array(&item.value, "addresses"),
                }),
                _ => Box::new(GenericDomainData {
                    domain: rego_ns,
                    prefix: "confidential".to_string(),
                    fields: item.value.clone(),
                }),
            };
            Ok(data)
        })
        .collect()
}

fn string_array(value: &serde_json::Value, key: &str) -> Vec<String> {
    value
        .get(key)
        .and_then(|v| v.as_array())
        .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
        .unwrap_or_default()
}

#[derive(Debug)]
struct GenericDomainData {
    domain: String,
    prefix: String,
    fields: serde_json::Value,
}

impl PolicyDomainData for GenericDomainData {
    fn domain_name(&self) -> &str {
        &self.domain
    }

    fn rego_prefix(&self) -> &str {
        &self.prefix
    }

    fn to_field_map(&self) -> BTreeMap<String, Value> {
        let mut map = BTreeMap::new();
        if let Some(obj) = self.fields.as_object() {
            for (k, v) in obj {
                map.insert(k.clone(), json_to_regorus_value(v));
            }
        }
        map
    }
}

fn json_to_regorus_value(v: &serde_json::Value) -> Value {
    newton_core::rego::json_to_rego_value(v)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn nested_json_objects_are_preserved() {
        let value = serde_json::json!({
            "profile": {
                "country": "us",
                "flags": ["kyc", "accredited"]
            }
        });

        let rego = json_to_regorus_value(&value);
        let json = newton_core::rego::value_to_json(&rego);

        assert_eq!(json, value);
    }
}