newton-chainio 0.5.2

newton prover chainio
//! Confidential data management: on-chain fetch from ConfidentialDataRegistry.
//!
//! Provider-managed blacklists/allowlists are stored as encrypted blobs referenced
//! by a `data_ref_id` content hash on-chain.
//!
//! ## Flow
//!
//! 1. Call `ConfidentialDataRegistry.getGrantedDomains(policy_client)` to enumerate
//!    all domains with active grants for this policy client.
//! 2. For each domain, call `getConfidentialData(policy_client, domain)` to get
//!    the on-chain `data_ref_id`.
//! 3. Caller resolves each `data_ref_id` to an encrypted blob (off-chain DB), decrypts,
//!    and injects the resulting JSON value into Rego under `data.confidential.<namespace>.*`.
//!
//! ## Adding a New Confidential Domain
//!
//! 1. Add a variant to [`ConfidentialDomain`] enum.
//! 2. Add the domain's keccak256 hash check in [`ConfidentialDomain::from_bytes32`].
//! 3. Implement [`ConfidentialDomain::rego_namespace`] for the new variant.

use alloy::{
    primitives::{Address, Bytes, FixedBytes},
    providers::ProviderBuilder,
    sol,
};
use thiserror::Error;

/// Minimal ABI for the ConfidentialDataRegistry contract.
///
/// Defines only the read function used by operators. The full contract
/// interface will be added to generated bindings once deployed.
sol! {
    #[sol(rpc)]
    #[allow(missing_docs)]
    interface IConfidentialDataRegistry {
        function getConfidentialData(
            address policyClient,
            bytes32 domain
        ) external view returns (address provider, string dataRefId, uint64 version);

        function getGrantedDomains(
            address policyClient
        ) external view returns (bytes32[] memory);
    }
}

/// A resolved confidential data reference from the on-chain registry.
#[derive(Debug, Clone)]
pub struct ConfidentialDataRef {
    /// Content-hash reference to the off-chain encrypted blob (stored in `encrypted_data_refs` table).
    pub data_ref_id: String,
    /// Address of the provider that registered this data.
    pub provider: Address,
    /// Schema version of the confidential data.
    pub version: u64,
}

/// Well-known confidential domain identifiers.
///
/// Each variant corresponds to a `bytes32` domain identifier used by `ConfidentialDataRegistry`.
/// The byte representation is the keccak256 of the domain name string:
/// `bytes32(keccak256("blacklist"))`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfidentialDomain {
    /// Provider-managed address blacklist.
    Blacklist,
    /// Provider-managed address allowlist.
    Allowlist,
}

impl ConfidentialDomain {
    /// Parse a bytes32 domain identifier into a known domain variant.
    ///
    /// Returns `None` for unrecognized domains. Unknown domains use a fallback
    /// namespace derived from the hex-encoded hash prefix.
    pub fn from_bytes32(domain: &FixedBytes<32>) -> Option<Self> {
        let blacklist_hash = alloy::primitives::keccak256(b"blacklist");
        let blacklist_namespaced_hash = alloy::primitives::keccak256(b"newton.confidential.blacklist");
        let allowlist_hash = alloy::primitives::keccak256(b"allowlist");
        let allowlist_namespaced_hash = alloy::primitives::keccak256(b"newton.confidential.allowlist");

        if *domain == blacklist_hash || *domain == blacklist_namespaced_hash {
            return Some(Self::Blacklist);
        }
        if *domain == allowlist_hash || *domain == allowlist_namespaced_hash {
            return Some(Self::Allowlist);
        }

        None
    }

    /// Returns the Rego namespace under `data.confidential.*` for this domain.
    pub fn rego_namespace(&self) -> &'static str {
        match self {
            Self::Blacklist => "blacklist",
            Self::Allowlist => "allowlist",
        }
    }

    /// Returns the human-readable domain name.
    pub fn name(&self) -> &'static str {
        match self {
            Self::Blacklist => "blacklist",
            Self::Allowlist => "allowlist",
        }
    }
}

/// Error types for confidential data operations.
#[derive(Debug, Error)]
pub enum ConfidentialDataError {
    /// On-chain registry call failed.
    #[error("registry call failed: {0}")]
    RegistryCallFailed(String),
    /// No data registered for this policy client + domain.
    #[error("no confidential data registered for policy_client={0} domain={1}")]
    NotFound(String, String),
    /// Refuse to sign over a partial domain set.
    #[error("too many confidential domains for policy_client={policy_client}: got {count}, limit {limit}")]
    TooManyDomains {
        /// Policy client whose domain fanout exceeded the cap.
        policy_client: String,
        /// Reported domain count from the registry.
        count: usize,
        /// Hard per-request domain cap.
        limit: usize,
    },
}

/// Fetch confidential data reference from the on-chain `ConfidentialDataRegistry`.
///
/// Returns the `ConfidentialDataRef` containing the `data_ref_id` to be resolved
/// off-chain, or a `ConfidentialDataError` if the registry call fails or no data
/// is registered for the given policy client and domain.
pub async fn fetch_confidential_data(
    rpc_url: &str,
    confidential_data_registry: &Address,
    policy_client: &Address,
    domain: FixedBytes<32>,
) -> Result<ConfidentialDataRef, ConfidentialDataError> {
    let url: alloy::transports::http::reqwest::Url = rpc_url
        .parse()
        .map_err(|e| ConfidentialDataError::RegistryCallFailed(format!("invalid rpc_url: {e}")))?;
    let provider = ProviderBuilder::new().connect_http(url);
    let registry = IConfidentialDataRegistry::new(*confidential_data_registry, provider);

    let result = registry
        .getConfidentialData(*policy_client, domain)
        .call()
        .await
        .map_err(|e| ConfidentialDataError::RegistryCallFailed(e.to_string()))?;

    if result.dataRefId.is_empty() {
        return Err(ConfidentialDataError::NotFound(
            policy_client.to_string(),
            domain.to_string(),
        ));
    }

    Ok(ConfidentialDataRef {
        data_ref_id: result.dataRefId,
        provider: result.provider,
        version: result.version,
    })
}

/// Fetch ALL confidential data references for a policy client from the on-chain registry.
///
/// Calls `getGrantedDomains(policyClient)` to enumerate all domains with active grants,
/// then `getConfidentialData(policyClient, domain)` for each. Returns a vec of
/// (domain, data_ref) pairs. Returns an empty vec if no domains are granted.
pub async fn fetch_all_confidential_data(
    rpc_url: &str,
    confidential_data_registry: &Address,
    policy_client: &Address,
) -> Result<Vec<(FixedBytes<32>, ConfidentialDataRef)>, ConfidentialDataError> {
    let url: alloy::transports::http::reqwest::Url = rpc_url
        .parse()
        .map_err(|e| ConfidentialDataError::RegistryCallFailed(format!("invalid rpc_url: {e}")))?;
    let provider = ProviderBuilder::new().connect_http(url);
    let registry = IConfidentialDataRegistry::new(*confidential_data_registry, provider);

    let domains = registry
        .getGrantedDomains(*policy_client)
        .call()
        .await
        .map_err(|e| ConfidentialDataError::RegistryCallFailed(format!("getGrantedDomains failed: {e}")))?;

    // Octane #8 sub-finding: cap per-request domain enumeration. An attacker
    // controlling a policy client can grant unbounded domains, which would
    // amplify per-domain RPC + DB lookups into operator DoS.
    const MAX_DOMAINS_PER_REQUEST: usize = 256;
    if domains.len() > MAX_DOMAINS_PER_REQUEST {
        return Err(ConfidentialDataError::TooManyDomains {
            policy_client: policy_client.to_string(),
            count: domains.len(),
            limit: MAX_DOMAINS_PER_REQUEST,
        });
    }

    let mut results = Vec::new();
    for domain in domains {
        let result = registry
            .getConfidentialData(*policy_client, domain)
            .call()
            .await
            .map_err(|e| ConfidentialDataError::RegistryCallFailed(e.to_string()))?;

        if !result.dataRefId.is_empty() {
            results.push((
                domain,
                ConfidentialDataRef {
                    data_ref_id: result.dataRefId,
                    provider: result.provider,
                    version: result.version,
                },
            ));
        }
    }

    Ok(results)
}

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

    #[test]
    fn blacklist_domain_roundtrip() {
        let hash = keccak256(b"blacklist");
        let domain = ConfidentialDomain::from_bytes32(&hash);
        assert_eq!(domain, Some(ConfidentialDomain::Blacklist));
        assert_eq!(domain.unwrap().rego_namespace(), "blacklist");
    }

    #[test]
    fn allowlist_domain_roundtrip() {
        let hash = keccak256(b"allowlist");
        let domain = ConfidentialDomain::from_bytes32(&hash);
        assert_eq!(domain, Some(ConfidentialDomain::Allowlist));
        assert_eq!(domain.unwrap().rego_namespace(), "allowlist");
    }

    #[test]
    fn blacklist_namespaced_domain_roundtrip() {
        let hash = keccak256(b"newton.confidential.blacklist");
        let domain = ConfidentialDomain::from_bytes32(&hash);
        assert_eq!(domain, Some(ConfidentialDomain::Blacklist));
        assert_eq!(domain.unwrap().rego_namespace(), "blacklist");
    }

    #[test]
    fn unknown_domain_returns_none() {
        let hash = keccak256(b"unknown_domain_xyz");
        assert_eq!(ConfidentialDomain::from_bytes32(&hash), None);
    }
}