newton-chainio 0.5.2

newton prover chainio
//! Task EIP712 signing and signature verification utilities

use alloy::{
    primitives::{keccak256, Address, Bytes, Signature, B256, U256},
    providers::Provider,
    sol_types::SolStruct,
};
use newton_core::{
    newton_prover_task_manager::INewtonProverTaskManager::Task,
    operator_registry::OperatorRegistry::OperatorRegistryInstance,
};
use thiserror::Error;

/// Error types for task EIP712 operations
#[derive(Debug, Error)]
pub enum TaskEip712Error {
    /// Invalid signature format
    #[error("Invalid signature format: {0}")]
    InvalidSignature(String),
    /// Failed to recover signer
    #[error("Failed to recover signer: {0}")]
    SignerRecoveryFailed(String),
    /// Signer is not an authorized task generator
    #[error("Signer {0} is not an authorized task generator")]
    NotAuthorizedTaskGenerator(Address),
    /// Failed to check task generator
    #[error("Failed to check task generator: {0}")]
    TaskGeneratorCheckFailed(String),
}

/// EIP712 domain name for the Newton Prover Task Manager contract.
pub const TASK_DOMAIN_NAME: &str = "Newton Prover Task Manager";
/// EIP712 domain version for the Newton Prover Task Manager contract.
pub const TASK_DOMAIN_VERSION: &str = "1";

/// EIP712 domain configuration for Task signing
#[derive(Debug)]
pub struct TaskEip712Domain {
    /// Name of the domain
    pub name: String,
    /// Version of the domain
    pub version: String,
    /// Chain ID of the domain
    pub chain_id: u64,
    /// Verifying contract of the domain
    pub verifying_contract: Address,
}

impl Default for TaskEip712Domain {
    fn default() -> Self {
        Self {
            name: TASK_DOMAIN_NAME.to_string(),
            version: TASK_DOMAIN_VERSION.to_string(),
            chain_id: 0,                       // Must be set explicitly
            verifying_contract: Address::ZERO, // Must be set explicitly
        }
    }
}

impl TaskEip712Domain {
    /// Create a new [`TaskEip712Domain`] with the standard Newton Prover Task Manager name and version.
    pub fn new(chain_id: u64, verifying_contract: Address) -> Self {
        Self {
            name: TASK_DOMAIN_NAME.to_string(),
            version: TASK_DOMAIN_VERSION.to_string(),
            chain_id,
            verifying_contract,
        }
    }
}

/// Compute EIP712 hash for a Task struct
///
/// This computes the EIP712 message hash according to EIP-712 standard:
/// hash = keccak256("\x19\x01" || domain_separator || struct_hash)
pub fn compute_task_eip712_hash(task: &Task, domain: &TaskEip712Domain) -> B256 {
    // Build EIP712 domain separator
    // EIP712 domain hash = keccak256(ABI_ENCODE(
    //   TYPE_HASH,
    //   NAME_HASH,
    //   VERSION_HASH,
    //   CHAIN_ID,
    //   VERIFYING_CONTRACT,
    //   SALT_HASH
    // ))
    let domain_type_hash =
        keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    let name_hash = keccak256(domain.name.as_bytes());
    let version_hash = keccak256(domain.version.as_bytes());
    let chain_id_u256 = U256::from(domain.chain_id);
    let verifying_contract = domain.verifying_contract;

    // Encode domain fields
    let mut domain_data = Vec::new();
    domain_data.extend_from_slice(&domain_type_hash[..]);
    domain_data.extend_from_slice(&name_hash[..]);
    domain_data.extend_from_slice(&version_hash[..]);
    domain_data.extend_from_slice(&chain_id_u256.to_be_bytes::<32>());
    domain_data.extend_from_slice(&verifying_contract.into_array());
    // No salt
    let salt_hash = B256::ZERO;
    domain_data.extend_from_slice(&salt_hash[..]);

    let domain_separator = keccak256(&domain_data);

    // Compute struct hash for Task
    let struct_hash = task.eip712_hash_struct();

    // EIP712 message hash = keccak256("\x19\x01" || domain_separator || struct_hash)
    let mut message_data = Vec::new();
    message_data.push(0x19u8);
    message_data.push(0x01u8);
    message_data.extend_from_slice(&domain_separator[..]);
    message_data.extend_from_slice(&struct_hash[..]);
    keccak256(&message_data)
}

/// Compute the commit-phase auth hash that binds both the Task routing fields
/// AND the consensus data operators will evaluate and BLS-sign.
/// `consensus_data_hash` = keccak256(abi_encode(consensus_policy_task_data)).
pub fn compute_commit_auth_hash(task: &Task, domain: &TaskEip712Domain, consensus_data_hash: B256) -> B256 {
    let task_hash = compute_task_eip712_hash(task, domain);
    let mut buf = Vec::with_capacity(64);
    buf.extend_from_slice(&task_hash[..]);
    buf.extend_from_slice(&consensus_data_hash[..]);
    keccak256(&buf)
}

/// Recover signer address from EIP712 signature
pub fn recover_task_signer(
    task: &Task,
    domain: &TaskEip712Domain,
    signature_bytes: &Bytes,
) -> Result<Address, TaskEip712Error> {
    // Compute EIP712 hash
    let eip712_hash = compute_task_eip712_hash(task, domain);

    // Recover signer from signature
    let signature =
        Signature::try_from(signature_bytes.as_ref()).map_err(|e| TaskEip712Error::InvalidSignature(e.to_string()))?;

    signature
        .recover_address_from_prehash(&eip712_hash)
        .map_err(|e| TaskEip712Error::SignerRecoveryFailed(e.to_string()))
}

/// Verify EIP712 signature and check if signer is an authorized task generator
pub async fn verify_task_generator_signature<P, N>(
    task: &Task,
    domain: &TaskEip712Domain,
    signature_bytes: &Bytes,
    operator_registry: &OperatorRegistryInstance<P, N>,
) -> Result<Address, TaskEip712Error>
where
    P: Provider<N>,
    N: alloy::providers::Network,
{
    // Recover signer from signature
    let recovered_signer = recover_task_signer(task, domain, signature_bytes)?;

    // Verify signer is a task generator
    let is_task_generator = operator_registry
        .isTaskGenerator(recovered_signer)
        .call()
        .await
        .map_err(|e| TaskEip712Error::TaskGeneratorCheckFailed(e.to_string()))?;

    if !is_task_generator {
        return Err(TaskEip712Error::NotAuthorizedTaskGenerator(recovered_signer));
    }

    Ok(recovered_signer)
}