newton-aggregator 0.4.18

newton prover aggregator utils
//! State-commit proposal builder.
//!
//! [`build_state_commit`] takes a fresh [`RegistryView`], a target state
//! root, a DA cert hash, a PCR0 commitment, and a wall-clock timestamp, and
//! returns a [`StateCommit`] ready to feed into BLS aggregation. The
//! function applies the same three pre-submit invariants the registry
//! enforces on-chain — [`InvalidNewStateRoot`](StateCommitError::InvalidNewStateRoot)
//! (`0x5bf0f768`), [`InvalidPcr0Commitment`](StateCommitError::InvalidPcr0Commitment)
//! (`0x6dfbfc74`), and [`TimestampRegression`](StateCommitError::TimestampRegression)
//! (`0x5a612e4c`) — so we never burn a 120s aggregation cycle on a proposal
//! the registry will reject.
//!
//! Sequence-monotonicity (`SequenceGap`) and prev-root consistency
//! (`StateRootMismatch`) are NOT pre-checked: those depend on what other
//! aggregators do between view-read and submit, and the registry catches
//! them at submit time. The orchestrator's poison handling re-reads the
//! view and rebuilds when the registry rejects.

use alloy::primitives::B256;
use newton_core::state_commit_registry::IStateRootCommittable::StateCommit;

use crate::state_commit::{error::StateCommitError, registry_view::RegistryView};

/// Schema version pinned by the on-chain `StateCommitRegistry` (revert
/// `UnsupportedStateCommitVersion` `0xb681668e` if mismatched). Bumping this
/// constant without coordinating a contract migration silently re-interprets
/// previously-signed BLS digests — see
/// `.claude/rules/lessons.md` "Version every cryptographic context".
pub const STATE_COMMIT_V1: u8 = 1;

/// Build a [`StateCommit`] from a fresh registry view + the per-tick inputs.
///
/// # Off-chain pre-submit invariants
///
/// Mirrored 1-to-1 against the on-chain typed reverts:
///
/// | Off-chain check | On-chain selector | Surface as |
/// |---|---|---|
/// | `new_state_root != bytes32(0)` | `InvalidNewStateRoot` `0x5bf0f768` | [`StateCommitError::InvalidNewStateRoot`] |
/// | `pcr0 != bytes32(0)` | `InvalidPcr0Commitment` `0x6dfbfc74` | [`StateCommitError::InvalidPcr0Commitment`] |
/// | `now_ts > view.last_commit_timestamp` | `TimestampRegression` `0x5a612e4c` | [`StateCommitError::TimestampRegression`] |
///
/// `version` is fixed at [`STATE_COMMIT_V1`]. `sequenceNo` is `view.sequence_no + 1`
/// (no overflow guard — at 120s cadence, u64 saturation is ~70 trillion years
/// out, and the registry would also revert in Solidity 0.8.x arithmetic).
///
/// # Why now_ts is a parameter, not `SystemTime::now()` inside the function
///
/// Keeps the function pure for property tests and lets the orchestrator
/// pin a single `now_ts` across the prepare/aggregate/commit pipeline.
/// Production callers pass `SystemTime::now().duration_since(UNIX_EPOCH)`.
// `StateCommitError::OnchainCallFailed(ChainIoError)` exceeds the 128-byte
// `result_large_err` threshold. Boxing would force every orchestrator match
// arm through a deref; the function fires ~once per 120s per chain so the
// stack-size cost is not on any hot path.
#[allow(clippy::result_large_err)]
pub fn build_state_commit(
    view: &RegistryView,
    new_state_root: B256,
    da_cert_hash: B256,
    pcr0: B256,
    now_ts: u64,
) -> Result<StateCommit, StateCommitError> {
    if new_state_root == B256::ZERO {
        return Err(StateCommitError::InvalidNewStateRoot);
    }
    if pcr0 == B256::ZERO {
        return Err(StateCommitError::InvalidPcr0Commitment);
    }
    if now_ts <= view.last_commit_timestamp {
        return Err(StateCommitError::TimestampRegression {
            last: view.last_commit_timestamp,
            got: now_ts,
        });
    }

    Ok(StateCommit {
        version: STATE_COMMIT_V1,
        sequenceNo: view.sequence_no + 1,
        prevStateRoot: view.state_root,
        newStateRoot: new_state_root,
        timestamp: now_ts,
        daCertHash: da_cert_hash,
        pcr0Commitment: pcr0,
    })
}

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

    fn b32(byte: u8) -> B256 {
        B256::repeat_byte(byte)
    }

    fn baseline_view() -> RegistryView {
        RegistryView {
            sequence_no: 5,
            state_root: b32(0xab),
            last_commit_timestamp: 1_000,
        }
    }

    #[test]
    fn happy_path_constructs_commit_correctly() {
        let view = baseline_view();
        let new_root = b32(0xcd);
        let da_cert = b32(0xef);
        let pcr0 = b32(0x42);
        let now_ts = 1_120; // 120s after last commit

        let commit = build_state_commit(&view, new_root, da_cert, pcr0, now_ts).expect("happy path constructs commit");

        assert_eq!(commit.version, STATE_COMMIT_V1);
        assert_eq!(commit.sequenceNo, 6, "sequenceNo = view.sequence_no + 1");
        assert_eq!(commit.prevStateRoot.as_slice(), view.state_root.as_slice());
        assert_eq!(commit.newStateRoot.as_slice(), new_root.as_slice());
        assert_eq!(commit.timestamp, now_ts);
        assert_eq!(commit.daCertHash.as_slice(), da_cert.as_slice());
        assert_eq!(commit.pcr0Commitment.as_slice(), pcr0.as_slice());
    }

    #[test]
    fn zero_new_state_root_rejected_off_chain() {
        let view = baseline_view();
        let result = build_state_commit(&view, B256::ZERO, b32(0xef), b32(0x42), 1_120);
        assert!(matches!(result, Err(StateCommitError::InvalidNewStateRoot)));
    }

    #[test]
    fn zero_pcr0_rejected_off_chain() {
        let view = baseline_view();
        let result = build_state_commit(&view, b32(0xcd), b32(0xef), B256::ZERO, 1_120);
        assert!(matches!(result, Err(StateCommitError::InvalidPcr0Commitment)));
    }

    #[test]
    fn timestamp_equal_to_last_rejected_off_chain() {
        // Strict > required by registry; equality fails.
        let view = baseline_view();
        let result = build_state_commit(&view, b32(0xcd), b32(0xef), b32(0x42), 1_000);
        match result {
            Err(StateCommitError::TimestampRegression { last, got }) => {
                assert_eq!(last, 1_000);
                assert_eq!(got, 1_000);
            }
            other => panic!("expected TimestampRegression, got {other:?}"),
        }
    }

    #[test]
    fn timestamp_in_past_rejected_off_chain() {
        let view = baseline_view();
        let result = build_state_commit(&view, b32(0xcd), b32(0xef), b32(0x42), 999);
        match result {
            Err(StateCommitError::TimestampRegression { last, got }) => {
                assert_eq!(last, 1_000);
                assert_eq!(got, 999);
            }
            other => panic!("expected TimestampRegression, got {other:?}"),
        }
    }

    #[test]
    fn invariant_check_order_zero_new_root_takes_priority_over_zero_pcr0() {
        // If both invariants fail, InvalidNewStateRoot fires first. This
        // matches the contract's check order — caller expects deterministic
        // error attribution when multiple inputs are bad.
        let view = baseline_view();
        let result = build_state_commit(&view, B256::ZERO, b32(0xef), B256::ZERO, 1_120);
        assert!(matches!(result, Err(StateCommitError::InvalidNewStateRoot)));
    }

    #[test]
    fn invariant_check_order_zero_pcr0_takes_priority_over_timestamp_regression() {
        let view = baseline_view();
        // PCR0 zero AND timestamp regression — PCR0 fires first
        let result = build_state_commit(&view, b32(0xcd), b32(0xef), B256::ZERO, 999);
        assert!(matches!(result, Err(StateCommitError::InvalidPcr0Commitment)));
    }

    #[test]
    fn sequence_no_increments_by_one_from_view() {
        let view = RegistryView {
            sequence_no: 99,
            state_root: b32(0xab),
            last_commit_timestamp: 0,
        };
        let commit = build_state_commit(&view, b32(0x01), b32(0x02), b32(0x03), 1).expect("happy path");
        assert_eq!(commit.sequenceNo, 100);
    }

    #[test]
    fn state_commit_v1_constant_is_one() {
        // Sentinel test — if STATE_COMMIT_V1 ever changes, on-chain digest
        // schema must change too. Forces the change to surface in code review.
        assert_eq!(STATE_COMMIT_V1, 1u8);
    }
}