arcium-core-utils 0.7.3

Arcium core utils
Documentation
//! Deterministic, cross-machine identity for a [`v2`](crate::circuit::v2) circuit.
//!
//! [`CircuitId`] is a blake3 digest over the canonical little-endian `bincode` encoding of a
//! circuit's MPC-field tag, gates (in definition order), and outputs. Two circuits map to the
//! same id iff they were built for the same MPC field and have the same gates in the same order
//! and the same outputs, which makes it a cheap stand-in for full circuit equality (e.g. when
//! parties must agree on which circuit to run).
//!
//! The encoding is byte-identical across machines because every value reachable inside a
//! [`Gate`](crate::circuit::v2::Gate) serializes to a fixed-width little-endian representation and
//! `bincode` itself encodes lengths as `u64` and enum discriminants as `u32` with no dependence on
//! pointer width or endianness. The id is *not* stable across code changes that reorder or insert
//! gate/op enum variants, since the bincode encoding embeds the variant index — this is acceptable
//! (and desirable) for agreement, where parties must run compatible code regardless.

use serde::{Deserialize, Serialize};
use wincode::{SchemaRead, SchemaWrite};

use crate::{circuit::v2::Circuit, config::MpcConfig};

/// A deterministic 256-bit identity of a [`v2`](crate::circuit::v2) circuit, used for inexpensive
/// circuit equality checks (e.g. agreeing on which circuit to run across parties).
#[derive(
    Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, SchemaRead, SchemaWrite,
)]
#[repr(C)]
pub struct CircuitId([u8; 32]);

impl CircuitId {
    /// Computes the id of the given circuit.
    ///
    /// Hashes the circuit's canonical `bincode` encoding (its `CompressedCircuit` form: gates in
    /// order, then outputs), reusing the same representation circuits are serialized with on the
    /// wire.
    pub fn of<C: MpcConfig>(circuit: &Circuit<C>) -> Self {
        let bytes =
            bincode::serialize(circuit).expect("circuit bincode serialization is infallible");
        Self(*blake3::hash(&bytes).as_bytes())
    }

    /// Returns the raw 32-byte digest.
    pub fn as_bytes(&self) -> &[u8; 32] {
        &self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        circuit::v2::{AlgebraicType, FieldShareBinaryOp, Gate, Input},
        config::DefaultConfig as C,
    };

    fn input<C: MpcConfig>() -> Gate<C> {
        Gate::Input(Input::SecretPlaintext {
            inputer: 0,
            algebraic_type: AlgebraicType::ScalarField,
            batch_size: 1,
        })
    }

    /// Builds a small `(x0 + x1) + (x2 + x3)` circuit using only the public API.
    fn sample_circuit() -> Circuit<C> {
        let mut circuit = Circuit::new();
        let i0 = circuit.add_gate(input()).unwrap();
        let i1 = circuit.add_gate(input()).unwrap();
        let i2 = circuit.add_gate(input()).unwrap();
        let i3 = circuit.add_gate(input()).unwrap();
        let l = circuit
            .add_gate(Gate::FieldShareBinaryOp {
                x: i0,
                y: i1,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();
        let r = circuit
            .add_gate(Gate::FieldShareBinaryOp {
                x: i2,
                y: i3,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();
        let top = circuit
            .add_gate(Gate::FieldShareBinaryOp {
                x: l,
                y: r,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();
        circuit.add_output(top).unwrap();
        circuit
    }

    /// The same circuit always maps to the same id.
    #[test]
    fn deterministic() {
        assert_eq!(
            CircuitId::of(&sample_circuit()),
            CircuitId::of(&sample_circuit())
        );
    }

    /// A bincode round-trip preserves the id.
    #[test]
    fn stable_across_serialization_roundtrip() {
        let circuit = sample_circuit();
        let bytes = bincode::serialize(&circuit).unwrap();
        let restored: Circuit<C> = bincode::deserialize(&bytes).unwrap();
        assert_eq!(CircuitId::of(&circuit), CircuitId::of(&restored));
    }

    /// Reordering gates (same multiset, different order) changes the id.
    #[test]
    fn order_sensitive() {
        // Two distinct inputers so the two input gates are not identical, letting us build the
        // same set of gates in two different orders.
        let mut a = Circuit::<C>::new();
        let a0 = a.add_gate(input()).unwrap();
        let a1 = a
            .add_gate(Gate::Input(Input::SecretPlaintext {
                inputer: 1,
                algebraic_type: AlgebraicType::ScalarField,
                batch_size: 1,
            }))
            .unwrap();
        let a2 = a
            .add_gate(Gate::FieldShareBinaryOp {
                x: a0,
                y: a1,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();
        a.add_output(a2).unwrap();

        let mut b = Circuit::<C>::new();
        let b0 = b
            .add_gate(Gate::Input(Input::SecretPlaintext {
                inputer: 1,
                algebraic_type: AlgebraicType::ScalarField,
                batch_size: 1,
            }))
            .unwrap();
        let b1 = b.add_gate(input()).unwrap();
        let b2 = b
            .add_gate(Gate::FieldShareBinaryOp {
                x: b0,
                y: b1,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();
        b.add_output(b2).unwrap();

        assert_ne!(CircuitId::of(&a), CircuitId::of(&b));
    }

    /// Editing a gate changes the id.
    #[test]
    fn gate_sensitive() {
        let mut add = Circuit::<C>::new();
        let x = add.add_gate(input()).unwrap();
        let y = add.add_gate(input()).unwrap();
        let z = add
            .add_gate(Gate::FieldShareBinaryOp {
                x,
                y,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();
        add.add_output(z).unwrap();

        let mut mul = Circuit::<C>::new();
        let x = mul.add_gate(input()).unwrap();
        let y = mul.add_gate(input()).unwrap();
        let z = mul
            .add_gate(Gate::FieldShareBinaryOp {
                x,
                y,
                op: FieldShareBinaryOp::Mul,
            })
            .unwrap();
        mul.add_output(z).unwrap();

        assert_ne!(CircuitId::of(&add), CircuitId::of(&mul));
    }

    /// Different outputs over identical gates change the id.
    #[test]
    fn output_sensitive() {
        let mut base = Circuit::<C>::new();
        let x = base.add_gate(input()).unwrap();
        let y = base.add_gate(input()).unwrap();
        let z = base
            .add_gate(Gate::FieldShareBinaryOp {
                x,
                y,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();

        let mut out_z = base.clone();
        out_z.add_output(z).unwrap();

        let mut out_x = base.clone();
        out_x.add_output(x).unwrap();

        assert_ne!(CircuitId::of(&out_z), CircuitId::of(&out_x));
    }
}