newton-core 0.4.16

newton protocol core sdk
//! JSON-RPC wire schemas for the state-commit two-phase protocol.
//!
//! Two operator-exposed methods make up the protocol:
//!
//! - `newt_getStateCommitProposal` — Prepare phase. The orchestrator calls
//!   this per-operator to gather proposed `(new_state_root, da_cert_hash,
//!   pcr0_commitment)` for the next sequence.
//! - `newt_signStateCommit` — Commit phase. The orchestrator sends the
//!   canonical `keccak256(abi.encode(StateCommit))` digest; the operator
//!   verifies its local view matches and returns a BLS G2 partial signature.
//!
//! Both crates that touch this protocol (`newton-prover-operator` server-side,
//! `newton-prover-aggregator` client-side) live as siblings — neither can
//! depend on the other. Hosting the wire schema here in `core` mirrors the
//! established pattern from [`super::signed_read`].
//!
//! See `docs/PRIVATE_DATA_STORAGE.md` §6 (Commit Protocol).

use alloy::{
    primitives::{keccak256, Bytes, B256},
    sol_types::SolValue,
};
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;

use crate::state_commit_registry::IStateRootCommittable::StateCommit;

/// EIP-712 type hash that the on-chain `BN254CertificateVerifier` uses to
/// derive the digest operators must BLS-sign. Bound by:
///
/// ```text
/// keccak256("BN254Certificate(uint32 referenceTimestamp,bytes32 messageHash)")
/// ```
///
/// The constant value is dictated by EigenLayer middleware
/// (`BN254CertificateVerifierStorage.BN254_CERTIFICATE_TYPEHASH`) — Newton
/// inherits it unchanged. Both the operator (signer) and the aggregator
/// (caller of `commitStateRoot`) MUST derive `signableDigest` from the same
/// `(reference_timestamp, message_hash)` pair using this typehash, otherwise
/// the on-chain pairing check fails with a verification revert.
///
/// Per `lessons.md` "Cross-process commitment hashes must use byte-deterministic
/// serialization" and "HKDF derivation chains must use domain-separated salts
/// per cryptographic role" — this is the single source of truth shared
/// between the two crates that touch the protocol.
pub static BN254_CERTIFICATE_TYPEHASH: LazyLock<B256> =
    LazyLock::new(|| keccak256("BN254Certificate(uint32 referenceTimestamp,bytes32 messageHash)".as_bytes()));

/// Compute the EIP-712-style digest the on-chain `BN254CertificateVerifier`
/// pairing-checks against the operator-quorum BLS aggregate.
///
/// ```text
/// signableDigest = keccak256(abi.encode(BN254_CERTIFICATE_TYPEHASH, referenceTimestamp, messageHash))
/// ```
///
/// `message_hash` is `keccak256(abi.encode(StateCommit))` — the canonical
/// digest of the state commit being signed. `reference_timestamp` is the
/// per-`(operator_set, root)` timestamp confirmed on-chain via
/// `confirmGlobalTableRoot`. Both fields are bound into the BLS signing
/// preimage so a captured signature cannot be replayed against a different
/// `referenceTimestamp` (where a different operator-table root applies).
pub fn compute_signable_digest(reference_timestamp: u32, message_hash: B256) -> B256 {
    keccak256((*BN254_CERTIFICATE_TYPEHASH, reference_timestamp, message_hash).abi_encode())
}

/// Request body for `newt_getStateCommitProposal` (Prepare phase).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetStateCommitProposalRequest {
    /// Sequence number for which the orchestrator is building a proposal.
    ///
    /// The operator returns the state root for its current tree version. The
    /// `sequence_no` field is provided for logging and future DA-cert matching;
    /// the actual tree read uses the current committed version regardless.
    pub sequence_no: u64,
}

/// Response body for `newt_getStateCommitProposal`.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetStateCommitProposalResponse {
    /// Proposed JMT state root (the current committed root of the operator's tree).
    pub new_state_root: B256,
    /// DA certificate hash accompanying the state root.
    ///
    /// Returns `B256::ZERO` until the delta-push pipeline is wired (NEWT-1118).
    pub da_cert_hash: B256,
    /// PCR0 commitment from the operator's enclave.
    ///
    /// Returns `B256::ZERO` in stub/dev mode when no `Pcr0Provider` is configured.
    pub pcr0_commitment: B256,
}

/// Request body for `newt_signStateCommit` (Commit phase).
///
/// Carries `digest` AND `commit` redundantly by design — the operator must
/// independently re-derive `keccak256(abi.encode(commit)) == digest` and
/// refuse to sign on mismatch. Without this redundancy, a malicious aggregator
/// could forward `digest = keccak256(abi.encode(victim_commit))` alongside
/// `commit = fabricated_commit`; an operator that trusted the supplied hash
/// would mint a valid BLS partial against arbitrary roots, since the digest
/// is what the BLS pairing actually binds. Defense-in-depth: the operator
/// trusts neither the digest nor the commit alone — only the consistency
/// of the pair, plus its own re-check of `commit.newStateRoot` against its
/// local JMT root.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SignStateCommitRequest {
    /// `keccak256(abi.encode(StateCommit))` — the canonical digest every
    /// operator must verify matches its own local view before signing.
    /// This is the certificate's `messageHash` field, NOT the BLS signing
    /// preimage; the operator derives the EIP-712-typed `signableDigest` via
    /// [`compute_signable_digest`] before BLS-signing.
    pub digest: B256,
    /// The full `StateCommit` that produced `digest`. The operator verifies
    /// `keccak256(abi.encode(commit)) == digest` and that `commit.newStateRoot`
    /// matches its local JMT root, then refuses to sign if either check fails.
    pub commit: StateCommitWire,
    /// EIP-712 `referenceTimestamp` — the per-`(operator_set, root)` timestamp
    /// confirmed on-chain via `confirmGlobalTableRoot`. Bound into the BLS
    /// signing preimage alongside `digest` (the `messageHash`) so the on-chain
    /// `BN254CertificateVerifier` pairing-checks the signature against the
    /// operator-table root that was confirmed at this timestamp.
    /// See [`compute_signable_digest`] for the derivation.
    pub reference_timestamp: u32,
}

/// JSON-friendly wire encoding of a [`StateCommit`].
///
/// Field names are snake_case (JSON convention) rather than camelCase (Solidity).
/// The [`From`] impl converts to the alloy-generated [`StateCommit`].
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StateCommitWire {
    /// Schema version; must equal `STATE_COMMIT_V1 = 1`.
    pub version: u8,
    /// Monotonically increasing sequence number.
    pub sequence_no: u64,
    /// Previous JMT state root (hash-chaining anchor).
    pub prev_state_root: B256,
    /// Proposed new JMT state root.
    pub new_state_root: B256,
    /// Unix timestamp (seconds) for this commit.
    pub timestamp: u64,
    /// DA certificate hash.
    pub da_cert_hash: B256,
    /// PCR0 commitment.
    pub pcr0_commitment: B256,
}

impl From<StateCommitWire> for StateCommit {
    fn from(w: StateCommitWire) -> Self {
        StateCommit {
            version: w.version,
            sequenceNo: w.sequence_no,
            prevStateRoot: w.prev_state_root,
            newStateRoot: w.new_state_root,
            timestamp: w.timestamp,
            daCertHash: w.da_cert_hash,
            pcr0Commitment: w.pcr0_commitment,
        }
    }
}

impl From<&StateCommit> for StateCommitWire {
    fn from(c: &StateCommit) -> Self {
        StateCommitWire {
            version: c.version,
            sequence_no: c.sequenceNo,
            prev_state_root: c.prevStateRoot,
            new_state_root: c.newStateRoot,
            timestamp: c.timestamp,
            da_cert_hash: c.daCertHash,
            pcr0_commitment: c.pcr0Commitment,
        }
    }
}

/// Response body for `newt_signStateCommit`.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SignStateCommitResponse {
    /// BLS G1 partial signature as raw bytes.
    ///
    /// The operator BLS-signs the orchestrator's digest with its G1 private
    /// key (`sign_message(digest)`); the aggregator sums these G1 partials
    /// into the `BN254Certificate.signature` field, which the on-chain
    /// `BN254CertificateVerifier` pairing-checks against the signers' G2 APK.
    ///
    /// Encoded as 2 × 32-byte big-endian U256 values: X, Y. This is the
    /// canonical 64-byte uncompressed G1 form used by EigenSDK's
    /// `convert_to_g1_point` and the on-chain verifier.
    pub signature_bytes: Bytes,
}

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

    #[test]
    fn state_commit_wire_round_trips_through_alloy_struct() {
        let wire = StateCommitWire {
            version: 1,
            sequence_no: 42,
            prev_state_root: B256::repeat_byte(0x11),
            new_state_root: B256::repeat_byte(0x22),
            timestamp: 1_700_000_000,
            da_cert_hash: B256::repeat_byte(0x33),
            pcr0_commitment: B256::repeat_byte(0x44),
        };
        let commit: StateCommit = wire.clone().into();
        assert_eq!(commit.version, 1);
        assert_eq!(commit.sequenceNo, 42);
        assert_eq!(commit.prevStateRoot, wire.prev_state_root);
        assert_eq!(commit.newStateRoot, wire.new_state_root);
        assert_eq!(commit.timestamp, 1_700_000_000);
        assert_eq!(commit.daCertHash, wire.da_cert_hash);
        assert_eq!(commit.pcr0Commitment, wire.pcr0_commitment);

        let back: StateCommitWire = (&commit).into();
        assert_eq!(back.version, wire.version);
        assert_eq!(back.sequence_no, wire.sequence_no);
        assert_eq!(back.prev_state_root, wire.prev_state_root);
        assert_eq!(back.new_state_root, wire.new_state_root);
        assert_eq!(back.timestamp, wire.timestamp);
        assert_eq!(back.da_cert_hash, wire.da_cert_hash);
        assert_eq!(back.pcr0_commitment, wire.pcr0_commitment);
    }

    #[test]
    fn snake_case_json_serialization() {
        let wire = StateCommitWire {
            version: 1,
            sequence_no: 7,
            prev_state_root: B256::ZERO,
            new_state_root: B256::repeat_byte(0xab),
            timestamp: 12345,
            da_cert_hash: B256::ZERO,
            pcr0_commitment: B256::ZERO,
        };
        let json = serde_json::to_string(&wire).expect("serialize");
        assert!(json.contains("\"sequence_no\":7"));
        assert!(json.contains("\"prev_state_root\""));
        assert!(json.contains("\"new_state_root\""));
        assert!(json.contains("\"da_cert_hash\""));
        assert!(json.contains("\"pcr0_commitment\""));

        let back: StateCommitWire = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back.sequence_no, 7);
        assert_eq!(back.new_state_root, B256::repeat_byte(0xab));
    }
}