arcium-core-utils 0.8.0

Arcium core utils
Documentation
/// A serialization optimized representation of a circuit.
use primitives::algebra::elliptic_curve::{Point, Scalar};
use serde::{Deserialize, Serialize};

use super::{errors::CircuitError, Circuit, Gate, GateIndex};
use crate::config::{name_tag, tag_name, MpcConfig};

/// Version of the serialized circuit layout. Bump on any change to the byte format so stale
/// blobs fail with [`CircuitError::UnsupportedFormatVersion`] instead of a parse error.
const CIRCUIT_FORMAT_VERSION: u8 = 3;

/// Serialization/deserialization optimized representation of a circuit.
#[derive(Serialize, Deserialize, Default)]
#[serde(bound(
    serialize = "Scalar<C::Curve>: Serialize, Point<C::Curve>: Serialize",
    deserialize = "Scalar<C::Curve>: Deserialize<'de>, Point<C::Curve>: Deserialize<'de>"
))]
#[repr(C)]
struct CompressedCircuit<C: MpcConfig> {
    /// Serialization format version (see [`CIRCUIT_FORMAT_VERSION`]). First in the layout so a
    /// stale blob fails fast.
    pub format_version: u8,
    /// Identifies the curve the circuit was built for (see [`name_tag`]).
    pub curve_tag: [u8; 32],
    /// Identifies the MPC field the circuit was built for (see [`name_tag`]). The tags precede
    /// the gates so a config mismatch fails before they are parsed.
    pub mpc_field_tag: [u8; 32],
    /// The circuit operations.
    pub ops: Vec<Gate<C>>,
    /// The output gates in order of definition
    pub output_gates: Vec<GateIndex>,
}

impl<C: MpcConfig> From<&Circuit<C>> for CompressedCircuit<C> {
    fn from(value: &Circuit<C>) -> Self {
        CompressedCircuit {
            format_version: CIRCUIT_FORMAT_VERSION,
            curve_tag: name_tag::<C::Curve>(),
            mpc_field_tag: name_tag::<C::Field>(),
            ops: value.iter_gates().cloned().collect(),
            output_gates: value.iter_output_indices().copied().collect(),
        }
    }
}

impl<C: MpcConfig> TryFrom<CompressedCircuit<C>> for Circuit<C> {
    type Error = CircuitError<C>;

    fn try_from(circuit: CompressedCircuit<C>) -> Result<Self, Self::Error> {
        if circuit.format_version != CIRCUIT_FORMAT_VERSION {
            return Err(CircuitError::UnsupportedFormatVersion {
                expected: CIRCUIT_FORMAT_VERSION,
                found: circuit.format_version,
            });
        }
        let expected_curve = name_tag::<C::Curve>();
        if circuit.curve_tag != expected_curve {
            return Err(CircuitError::CurveMismatch {
                expected: tag_name(&expected_curve),
                found: tag_name(&circuit.curve_tag),
            });
        }
        let expected_field = name_tag::<C::Field>();
        if circuit.mpc_field_tag != expected_field {
            return Err(CircuitError::MpcFieldMismatch {
                expected: tag_name(&expected_field),
                found: tag_name(&circuit.mpc_field_tag),
            });
        }

        let mut res = Self {
            gates: Vec::with_capacity(circuit.ops.len()),
            inputs: Vec::new(),
            outputs: Vec::with_capacity(circuit.output_gates.len()),
        };

        for gate in circuit.ops.into_iter() {
            res.add_gate(gate)?;
        }

        for index in circuit.output_gates.into_iter() {
            res.add_output(index)?;
        }

        Ok(res)
    }
}

mod bincode {

    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    use super::{Circuit, CompressedCircuit};
    use crate::config::MpcConfig;

    impl<C: MpcConfig> Serialize for Circuit<C> {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            let circuit_serde: CompressedCircuit<C> = self.into();
            circuit_serde.serialize(serializer)
        }
    }

    impl<'de, C: MpcConfig> Deserialize<'de> for Circuit<C> {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: Deserializer<'de>,
        {
            let circuit_serde = CompressedCircuit::<C>::deserialize(deserializer)
                .map_err(serde::de::Error::custom)?;
            let circuit = circuit_serde.try_into();
            circuit.map_err(serde::de::Error::custom)
        }
    }
}

#[cfg(test)]
mod tests {
    use primitives::utils::codec::bincode_io;

    use super::*;
    use crate::{
        circuit::latest::{
            tests::create_add_tree_circuit,
            AlgebraicType,
            FieldShareBinaryOp,
            Input,
        },
        config::DefaultConfig as C,
    };

    #[test]
    fn valid_circuit() {
        let mut circuit = Circuit::<C>::new();
        let input_gate1 = circuit
            .add_gate(Gate::Input(Input::SecretPlaintext {
                inputer: 0,
                algebraic_type: AlgebraicType::ScalarField,
                batch_size: 1,
            }))
            .unwrap();
        assert_eq!(input_gate1, 0);
        let input_gate2 = circuit
            .add_gate(Gate::Input(Input::SecretPlaintext {
                inputer: 1,
                algebraic_type: AlgebraicType::ScalarField,
                batch_size: 1,
            }))
            .unwrap();
        assert_eq!(input_gate2, 1);
        let add_gate = circuit
            .add_gate(Gate::FieldShareBinaryOp {
                x: input_gate1,
                y: input_gate2,
                op: FieldShareBinaryOp::Add,
            })
            .unwrap();
        assert_eq!(add_gate, 2);
        circuit.add_output(add_gate).unwrap();
        assert_eq!(
            circuit.iter_output_indices().copied().collect::<Vec<_>>(),
            vec![2]
        );
    }

    #[test]
    fn test_ser_circuit_bincode() {
        let circuit = create_add_tree_circuit(18);
        let serialized = bincode_io::serialize(&circuit).unwrap();
        let circuit_de: Circuit<C> = bincode_io::deserialize(&serialized).unwrap();

        assert_eq!(circuit, circuit_de);
    }

    /// A circuit serialized for one MPC field must not deserialize under a config with another.
    #[test]
    fn test_cross_field_rejection() {
        use crate::config::Gf2_128Config;

        let circuit = create_add_tree_circuit::<C>(2);

        let bin = bincode_io::serialize(&circuit).unwrap();
        let err = bincode_io::deserialize::<Circuit<Gf2_128Config>>(&bin).unwrap_err();
        assert!(
            err.to_string().contains("MPC field"),
            "unexpected error: {err}"
        );

        // Same-config roundtrips still pass the tag check.
        assert_eq!(
            bincode_io::deserialize::<Circuit<C>>(&bin).unwrap(),
            circuit
        );
    }

    /// A blob with an unknown format version is rejected before anything else is parsed, and a
    /// corrupted curve tag is rejected before the field tag.
    #[test]
    fn test_version_and_curve_rejection() {
        let circuit = create_add_tree_circuit::<C>(2);
        let bin = bincode_io::serialize(&circuit).unwrap();

        // Layout: format_version (1 byte) | curve_tag (32) | mpc_field_tag (32) | gates...
        let mut wrong_version = bin.clone();
        wrong_version[0] ^= 0xFF;
        let err = bincode_io::deserialize::<Circuit<C>>(&wrong_version).unwrap_err();
        assert!(
            err.to_string().contains("format version"),
            "unexpected error: {err}"
        );

        let mut wrong_curve = bin;
        wrong_curve[1] ^= 0xFF;
        let err = bincode_io::deserialize::<Circuit<C>>(&wrong_curve).unwrap_err();
        assert!(err.to_string().contains("curve"), "unexpected error: {err}");
    }

    /// The circuit id commits to the MPC field: identical shapes under different configs get
    /// different ids.
    #[test]
    fn test_circuit_id_commits_to_field() {
        use crate::{circuit::circuit_id::CircuitId, config::Gf2_128Config};

        let id_default = CircuitId::of(&create_add_tree_circuit::<C>(2));
        let id_gf2_128 = CircuitId::of(&create_add_tree_circuit::<Gf2_128Config>(2));
        assert_ne!(id_default.as_bytes(), id_gf2_128.as_bytes());
    }
}