newton-core 0.7.1

newton protocol core sdk
//! Digest computation utilities for BLS signature consensus.
//!
//! Provides functions to compute digests for TaskResponse used in BLS
//! signature aggregation and on-chain verification.

use alloy::{
    primitives::{keccak256, FixedBytes},
    sol_types::SolValue,
};

use crate::newton_prover_task_manager::INewtonProverTaskManager::TaskResponse;

/// Computes the consensus digest for a TaskResponse.
///
/// This is the digest that operators BLS-sign. Must match the on-chain
/// `TaskLib.computeConsensusDigest` exactly.
pub fn compute_consensus_digest(task_response: &TaskResponse) -> FixedBytes<32> {
    keccak256(TaskResponse::abi_encode(task_response))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::newton_prover_task_manager::NewtonMessage::{self, PolicyData, PolicyTaskData};
    use alloy::primitives::{Address, Bytes, B256, U256};

    /// One `PolicyTaskData` entry per composed policy; identity is positional, so
    /// `policyTaskData[i]` is the task's policy `i`.
    fn policy_task_data(seed: u8, oracle_output: &[u8]) -> PolicyTaskData {
        PolicyTaskData {
            policyId: B256::repeat_byte(seed),
            policyAddress: Address::repeat_byte(seed),
            policy: Bytes::new(),
            policyData: vec![PolicyData {
                wasmArgs: Bytes::new(),
                data: Bytes::from(oracle_output.to_vec()),
                expireBlock: 0,
            }],
        }
    }

    /// Two-policy fixture.
    fn create_test_task_response() -> TaskResponse {
        TaskResponse {
            taskId: B256::ZERO,
            policyClient: Address::ZERO,
            policyId: B256::ZERO,
            intent: NewtonMessage::Intent {
                from: Address::ZERO,
                to: Address::ZERO,
                value: U256::ZERO,
                data: Bytes::new(),
                chainId: U256::from(1),
                functionSignature: Bytes::new(),
            },
            intentSignature: Bytes::new(),
            allowed: true,
            policyTaskData: vec![
                policy_task_data(1, b"{\"price\": 100}"),
                policy_task_data(2, b"{\"balance\": 50}"),
            ],
            initializationTimestamp: U256::ZERO,
        }
    }

    #[test]
    fn consensus_digest_differs_when_data_differs() {
        let response1 = create_test_task_response();
        let mut response2 = response1.clone();
        response2.policyTaskData[0].policyData[0].data = Bytes::from(b"{\"price\": 200}".to_vec());

        let digest1 = compute_consensus_digest(&response1);
        let digest2 = compute_consensus_digest(&response2);

        assert_ne!(digest1, digest2, "digests should differ when oracle output differs");
    }

    #[test]
    fn digest_is_deterministic() {
        let response = create_test_task_response();

        let digest1 = compute_consensus_digest(&response);
        let digest2 = compute_consensus_digest(&response);

        assert_eq!(digest1, digest2, "Same input should produce same digest");
    }

    /// Empty-set shape fixture: no policies, so `policyTaskData` is empty.
    fn create_empty_policies_response() -> TaskResponse {
        TaskResponse {
            taskId: B256::ZERO,
            policyClient: Address::ZERO,
            policyId: B256::ZERO,
            intent: NewtonMessage::Intent {
                from: Address::ZERO,
                to: Address::ZERO,
                value: U256::ZERO,
                data: Bytes::new(),
                chainId: U256::from(1),
                functionSignature: Bytes::new(),
            },
            intentSignature: Bytes::new(),
            allowed: false,
            policyTaskData: vec![],
            initializationTimestamp: U256::ZERO,
        }
    }

    // GOLDEN: exact ABI encoding of the standard fixture. Pins the wire bytes that
    // feed keccak256 -> must match on-chain TaskLib.computeConsensusDigest input.
    #[test]
    fn golden_abi_encode_standard_fixture() {
        const EXPECTED: &str = "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001e001010101010101010101010101010101010101010101010101010101010101010000000000000000000000000101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e7b227072696365223a203130307d00000000000000000000000000000000000002020202020202020202020202020202020202020202020202020202020202020000000000000000000000000202020202020202020202020202020202020202000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f7b2262616c616e6365223a2035307d0000000000000000000000000000000000";
        let encoded = TaskResponse::abi_encode(&create_test_task_response());
        assert_eq!(
            hex::encode(&encoded),
            EXPECTED,
            "ABI encoding drifted for standard fixture"
        );
    }

    // GOLDEN: keccak256 of the standard fixture's ABI encoding = the consensus digest.
    #[test]
    fn golden_consensus_digest_standard_fixture() {
        const EXPECTED: &str = "0x839c518204ddea35d05c622943599538fba01f08d888c85ef5f21105dc8c6d90";
        let digest = compute_consensus_digest(&create_test_task_response());
        assert_eq!(
            format!("{digest:#x}"),
            EXPECTED,
            "consensus digest drifted for standard fixture"
        );
    }

    // GOLDEN: empty-set shape. Pins both the ABI bytes and the digest.
    #[test]
    fn golden_empty_policies_fixture() {
        const EXPECTED_ABI: &str = "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
        const EXPECTED_DIGEST: &str = "0xcadccda7c9ae43d3c76952debea8fc850c2cad0c9bc8e38437ec601903809d57";
        let fixture = create_empty_policies_response();
        let encoded = TaskResponse::abi_encode(&fixture);
        assert_eq!(
            hex::encode(&encoded),
            EXPECTED_ABI,
            "ABI encoding drifted for empty-policies fixture"
        );
        let digest = compute_consensus_digest(&fixture);
        assert_eq!(
            format!("{digest:#x}"),
            EXPECTED_DIGEST,
            "consensus digest drifted for empty-policies fixture"
        );
    }

    /// A one-policy response and a two-policy response must never digest alike, even when
    /// every other field matches: the arrays are what carry per-policy content.
    #[test]
    fn digest_is_bound_to_policy_count() {
        let two = create_test_task_response();
        let mut one = two.clone();
        one.policyTaskData.pop();

        assert_ne!(compute_consensus_digest(&two), compute_consensus_digest(&one));
    }
}